Spaces:
Paused
Paused
edited match making
Browse files- src/jobs/matcher.js +72 -28
src/jobs/matcher.js
CHANGED
|
@@ -6,7 +6,6 @@ const runWeeklyMatching = async () => {
|
|
| 6 |
console.log(`[${new Date().toISOString()}] Running automated connection matching algorithm...`);
|
| 7 |
try {
|
| 8 |
// Fetch workspaces that belong ONLY to non-admin users
|
| 9 |
-
// A workspace has teamMembers, we check if they are owned by an ADMIN
|
| 10 |
const workspaces = await prisma.workspace.findMany({
|
| 11 |
where: {
|
| 12 |
teamMembers: {
|
|
@@ -32,8 +31,33 @@ const runWeeklyMatching = async () => {
|
|
| 32 |
if (settings && settings.matchAmount) matchAmount = settings.matchAmount;
|
| 33 |
} catch(e) {}
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
for (const ws of workspaces) {
|
| 36 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
const allInteractions = await prisma.exchangeThread.findMany({
|
| 38 |
where: {
|
| 39 |
OR: [
|
|
@@ -43,24 +67,29 @@ const runWeeklyMatching = async () => {
|
|
| 43 |
}
|
| 44 |
});
|
| 45 |
|
| 46 |
-
const
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
let poolExhaustedWarningSent = false;
|
| 54 |
-
|
| 55 |
-
// Count current active (NEW/PENDING) giving threads to avoid spamming
|
| 56 |
-
const activeGivingCount = allInteractions.filter(t =>
|
| 57 |
-
t.giverWorkspaceId === ws.id && (t.status === 'PENDING' || t.stage === 'NEW')
|
| 58 |
-
).length;
|
| 59 |
|
| 60 |
-
const neededGiving = Math.max(0, matchAmount -
|
| 61 |
|
| 62 |
if (neededGiving > 0) {
|
| 63 |
-
const potentialReceivers = workspaces.filter(w => !
|
| 64 |
if (potentialReceivers.length === 0 && !poolExhaustedWarningSent) {
|
| 65 |
wsManager.sendNotification(ws.id, {
|
| 66 |
type: 'system',
|
|
@@ -70,7 +99,13 @@ const runWeeklyMatching = async () => {
|
|
| 70 |
poolExhaustedWarningSent = true;
|
| 71 |
}
|
| 72 |
|
| 73 |
-
const receivers = potentialReceivers
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 74 |
|
| 75 |
for (const rec of receivers) {
|
| 76 |
const thread = await prisma.exchangeThread.create({
|
|
@@ -83,8 +118,12 @@ const runWeeklyMatching = async () => {
|
|
| 83 |
include: { giverWorkspace: true, receiverWorkspace: true }
|
| 84 |
});
|
| 85 |
|
| 86 |
-
//
|
| 87 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
wsManager.sendNotification(ws.id, {
|
| 90 |
type: 'new_thread',
|
|
@@ -104,15 +143,10 @@ const runWeeklyMatching = async () => {
|
|
| 104 |
}
|
| 105 |
}
|
| 106 |
|
| 107 |
-
|
| 108 |
-
const activeReceivingCount = allInteractions.filter(t =>
|
| 109 |
-
t.receiverWorkspaceId === ws.id && (t.status === 'PENDING' || t.stage === 'NEW')
|
| 110 |
-
).length;
|
| 111 |
-
|
| 112 |
-
const neededReceiving = Math.max(0, matchAmount - activeReceivingCount);
|
| 113 |
|
| 114 |
if (neededReceiving > 0) {
|
| 115 |
-
const potentialGivers = workspaces.filter(w => !
|
| 116 |
if (potentialGivers.length === 0 && !poolExhaustedWarningSent) {
|
| 117 |
wsManager.sendNotification(ws.id, {
|
| 118 |
type: 'system',
|
|
@@ -122,7 +156,13 @@ const runWeeklyMatching = async () => {
|
|
| 122 |
poolExhaustedWarningSent = true;
|
| 123 |
}
|
| 124 |
|
| 125 |
-
const givers = potentialGivers
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
|
| 127 |
for (const giv of givers) {
|
| 128 |
const thread = await prisma.exchangeThread.create({
|
|
@@ -135,8 +175,12 @@ const runWeeklyMatching = async () => {
|
|
| 135 |
include: { giverWorkspace: true, receiverWorkspace: true }
|
| 136 |
});
|
| 137 |
|
| 138 |
-
//
|
| 139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
|
| 141 |
wsManager.sendNotification(giv.id, {
|
| 142 |
type: 'new_thread',
|
|
|
|
| 6 |
console.log(`[${new Date().toISOString()}] Running automated connection matching algorithm...`);
|
| 7 |
try {
|
| 8 |
// Fetch workspaces that belong ONLY to non-admin users
|
|
|
|
| 9 |
const workspaces = await prisma.workspace.findMany({
|
| 10 |
where: {
|
| 11 |
teamMembers: {
|
|
|
|
| 31 |
if (settings && settings.matchAmount) matchAmount = settings.matchAmount;
|
| 32 |
} catch(e) {}
|
| 33 |
|
| 34 |
+
// Pre-calculate active giving and receiving counts for all workspaces for fairness weighting
|
| 35 |
+
const allActiveThreads = await prisma.exchangeThread.findMany({
|
| 36 |
+
where: {
|
| 37 |
+
OR: [
|
| 38 |
+
{ status: 'PENDING' },
|
| 39 |
+
{ stage: 'NEW' }
|
| 40 |
+
]
|
| 41 |
+
}
|
| 42 |
+
});
|
| 43 |
+
|
| 44 |
+
const activeGivingCounts = {};
|
| 45 |
+
const activeReceivingCounts = {};
|
| 46 |
for (const ws of workspaces) {
|
| 47 |
+
activeGivingCounts[ws.id] = 0;
|
| 48 |
+
activeReceivingCounts[ws.id] = 0;
|
| 49 |
+
}
|
| 50 |
+
for (const t of allActiveThreads) {
|
| 51 |
+
if (activeGivingCounts[t.giverWorkspaceId] !== undefined) {
|
| 52 |
+
activeGivingCounts[t.giverWorkspaceId]++;
|
| 53 |
+
}
|
| 54 |
+
if (activeReceivingCounts[t.receiverWorkspaceId] !== undefined) {
|
| 55 |
+
activeReceivingCounts[t.receiverWorkspaceId]++;
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
for (const ws of workspaces) {
|
| 60 |
+
// Find all existing interactions for this workspace
|
| 61 |
const allInteractions = await prisma.exchangeThread.findMany({
|
| 62 |
where: {
|
| 63 |
OR: [
|
|
|
|
| 67 |
}
|
| 68 |
});
|
| 69 |
|
| 70 |
+
const excludedReceivers = new Set([ws.id]);
|
| 71 |
+
const excludedGivers = new Set([ws.id]);
|
| 72 |
+
|
| 73 |
+
for (const t of allInteractions) {
|
| 74 |
+
if (t.giverWorkspaceId === ws.id) {
|
| 75 |
+
// ws already gave to them, don't try again
|
| 76 |
+
excludedReceivers.add(t.receiverWorkspaceId);
|
| 77 |
+
// Block reverse direction (reciprocal) only if it wasn't rejected
|
| 78 |
+
if (t.status !== 'REJECTED') excludedGivers.add(t.receiverWorkspaceId);
|
| 79 |
+
} else {
|
| 80 |
+
// someone already gave to ws, they can't give to ws again
|
| 81 |
+
excludedGivers.add(t.giverWorkspaceId);
|
| 82 |
+
// Block reverse direction (reciprocal) only if it wasn't rejected
|
| 83 |
+
if (t.status !== 'REJECTED') excludedReceivers.add(t.giverWorkspaceId);
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
|
| 87 |
let poolExhaustedWarningSent = false;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
|
| 89 |
+
const neededGiving = Math.max(0, matchAmount - activeGivingCounts[ws.id]);
|
| 90 |
|
| 91 |
if (neededGiving > 0) {
|
| 92 |
+
const potentialReceivers = workspaces.filter(w => !excludedReceivers.has(w.id));
|
| 93 |
if (potentialReceivers.length === 0 && !poolExhaustedWarningSent) {
|
| 94 |
wsManager.sendNotification(ws.id, {
|
| 95 |
type: 'system',
|
|
|
|
| 99 |
poolExhaustedWarningSent = true;
|
| 100 |
}
|
| 101 |
|
| 102 |
+
const receivers = potentialReceivers
|
| 103 |
+
.sort((a, b) => {
|
| 104 |
+
const diff = activeReceivingCounts[a.id] - activeReceivingCounts[b.id];
|
| 105 |
+
if (diff === 0) return 0.5 - Math.random(); // tie-breaker
|
| 106 |
+
return diff; // prefer receivers with lowest activeReceivingCounts
|
| 107 |
+
})
|
| 108 |
+
.slice(0, neededGiving);
|
| 109 |
|
| 110 |
for (const rec of receivers) {
|
| 111 |
const thread = await prisma.exchangeThread.create({
|
|
|
|
| 118 |
include: { giverWorkspace: true, receiverWorkspace: true }
|
| 119 |
});
|
| 120 |
|
| 121 |
+
// Block them in both directions for the rest of this execution since it's active now
|
| 122 |
+
excludedReceivers.add(rec.id);
|
| 123 |
+
excludedGivers.add(rec.id);
|
| 124 |
+
|
| 125 |
+
activeGivingCounts[ws.id]++;
|
| 126 |
+
activeReceivingCounts[rec.id]++;
|
| 127 |
|
| 128 |
wsManager.sendNotification(ws.id, {
|
| 129 |
type: 'new_thread',
|
|
|
|
| 143 |
}
|
| 144 |
}
|
| 145 |
|
| 146 |
+
const neededReceiving = Math.max(0, matchAmount - activeReceivingCounts[ws.id]);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
if (neededReceiving > 0) {
|
| 149 |
+
const potentialGivers = workspaces.filter(w => !excludedGivers.has(w.id));
|
| 150 |
if (potentialGivers.length === 0 && !poolExhaustedWarningSent) {
|
| 151 |
wsManager.sendNotification(ws.id, {
|
| 152 |
type: 'system',
|
|
|
|
| 156 |
poolExhaustedWarningSent = true;
|
| 157 |
}
|
| 158 |
|
| 159 |
+
const givers = potentialGivers
|
| 160 |
+
.sort((a, b) => {
|
| 161 |
+
const diff = activeGivingCounts[a.id] - activeGivingCounts[b.id];
|
| 162 |
+
if (diff === 0) return 0.5 - Math.random(); // tie-breaker
|
| 163 |
+
return diff; // prefer givers with lowest activeGivingCounts
|
| 164 |
+
})
|
| 165 |
+
.slice(0, neededReceiving);
|
| 166 |
|
| 167 |
for (const giv of givers) {
|
| 168 |
const thread = await prisma.exchangeThread.create({
|
|
|
|
| 175 |
include: { giverWorkspace: true, receiverWorkspace: true }
|
| 176 |
});
|
| 177 |
|
| 178 |
+
// Block them in both directions
|
| 179 |
+
excludedGivers.add(giv.id);
|
| 180 |
+
excludedReceivers.add(giv.id);
|
| 181 |
+
|
| 182 |
+
activeReceivingCounts[ws.id]++;
|
| 183 |
+
activeGivingCounts[giv.id]++;
|
| 184 |
|
| 185 |
wsManager.sendNotification(giv.id, {
|
| 186 |
type: 'new_thread',
|