@voltro/plugin-matchmaking 0.0.0-stage → 0.88.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/CHANGELOG.md +24492 -0
  2. package/LICENSE +61 -0
  3. package/README.md +272 -2
  4. package/SECURITY.md +56 -0
  5. package/THIRD-PARTY-NOTICES.md +8333 -0
  6. package/dist/admission.entity.js +17 -0
  7. package/dist/assignment.js +125 -0
  8. package/dist/avoidPairs.js +4 -0
  9. package/dist/contract.js +105 -0
  10. package/dist/director-app/matchmaking.director.workflow.server.tsx +5 -0
  11. package/dist/director-app/matchmaking.director.workflow.tsx +13 -0
  12. package/dist/director.js +72 -0
  13. package/dist/directorContract.js +15 -0
  14. package/dist/directorWorkflow.js +5 -0
  15. package/dist/errors.d.ts +13 -0
  16. package/dist/errors.js +20 -0
  17. package/dist/index.d.ts +523 -0
  18. package/dist/index.js +12 -0
  19. package/dist/inspect.js +40 -0
  20. package/dist/packing.js +71 -0
  21. package/dist/parties.js +316 -0
  22. package/dist/party.entity.js +33 -0
  23. package/dist/partyComponents.js +176 -0
  24. package/dist/partyContract.js +24 -0
  25. package/dist/plugin.js +257 -0
  26. package/dist/port.d.ts +116 -0
  27. package/dist/port.js +4 -0
  28. package/dist/queue.d.ts +91 -0
  29. package/dist/queue.js +79 -0
  30. package/dist/queueDeclarations.js +27 -0
  31. package/dist/roomsAllocator.d.ts +67 -0
  32. package/dist/roomsAllocator.js +75 -0
  33. package/dist/rpc.d.ts +247 -0
  34. package/dist/rpc.js +201 -0
  35. package/dist/server.d.ts +337 -0
  36. package/dist/server.js +8 -0
  37. package/dist/service.js +369 -0
  38. package/dist/strategies.d.ts +97 -0
  39. package/dist/strategies.js +140 -0
  40. package/dist/testing.d.ts +296 -0
  41. package/dist/testing.js +151 -0
  42. package/dist/ticketContract.d.ts +52 -0
  43. package/dist/ticketContract.js +30 -0
  44. package/dist/tickets.entity.js +49 -0
  45. package/dist/web.d.ts +279 -0
  46. package/dist/web.js +7 -0
  47. package/package.json +104 -4
@@ -0,0 +1,369 @@
1
+ import { MatchmakingError as e, matchmakingError as t } from "./errors.js";
2
+ import { captureJsonData as n, identifier as r, sameJson as i } from "./contract.js";
3
+ import { defaultMatchmakingAvoidPairs as a } from "./avoidPairs.js";
4
+ import { MatchmakingTicketSchema as o } from "./ticketContract.js";
5
+ import { matchmakingAssignmentsTable as s, matchmakingTicketsTable as c } from "./tickets.entity.js";
6
+ import { matchmakingQueuesTable as l, matchmakingSubjectsTable as u } from "./admission.entity.js";
7
+ import { readMatchmakingAssignments as d } from "./assignment.js";
8
+ import { captureQueueDeclarations as f } from "./queueDeclarations.js";
9
+ import { dataStoreMatchmakingPlanner as p } from "./director.js";
10
+ import { Effect as m, Schema as h } from "effect";
11
+ import { and as g, eq as _, gt as v, queryFor as y } from "@voltro/database";
12
+ import { createHash as b } from "node:crypto";
13
+ // src/service.ts
14
+ var x = (e) => b("sha256").update(JSON.stringify(e, (e, t) => typeof t == "object" && t && !Array.isArray(t) ? Object.fromEntries(Object.entries(t).sort(([e], [t]) => e < t ? -1 : +(e > t))) : t)).digest("hex"), S = (e, t) => "mms_" + x([e, t]).slice(0, 56), C = (n) => n instanceof e ? n : t("unavailable"), w = (e) => e === !0 || e === 1, T = (e) => m.tryPromise({
15
+ try: e,
16
+ catch: C
17
+ }), E = (e, n) => {
18
+ let r = e[n];
19
+ if (typeof r != "string") throw t("unavailable");
20
+ return r;
21
+ }, D = (e) => {
22
+ let n = e instanceof Date ? e.getTime() : typeof e == "string" ? Date.parse(e) : NaN;
23
+ if (!Number.isSafeInteger(n) || n < 0) throw t("unavailable");
24
+ return n;
25
+ }, O = (e) => h.decodeUnknownSync(o)(structuredClone({
26
+ id: e.id,
27
+ queue: e.queue,
28
+ state: e.state,
29
+ members: e.members,
30
+ attributes: e.attributes,
31
+ createdAt: D(e.createdAt),
32
+ expiresAt: D(e.expiresAt),
33
+ closedAt: e.closedAt === null ? null : D(e.closedAt),
34
+ metadata: e.metadata
35
+ })), k = (e) => {
36
+ if (!e || e.type !== "user") throw t("identity_required");
37
+ if (!r(e.id) || !r(e.tenantId)) throw t("invalid_input");
38
+ return n(Object.fromEntries(Object.entries(e).filter(([, e]) => e !== void 0)), 65536);
39
+ }, A = (e) => {
40
+ if (!Array.isArray(e.queues) || e.queues.length > 256 || typeof e.prepareTicket != "function" || typeof e.admit != "function") throw t("invalid_queue");
41
+ let n = f(e.queues), r = e.prepareTicket, i = e.admit, o = e.now ?? Date.now, s = e.avoidPairs ?? a, c = e.maxPreferencesBytes ?? 4096, l = e.maxExportRows ?? 1e4;
42
+ if (!Number.isSafeInteger(c) || c < 1 || c > 65536 || !Number.isSafeInteger(l) || l < 1 || l > 1e5 || typeof o != "function" || typeof s != "function") throw t("invalid_queue");
43
+ return Object.freeze({
44
+ queues: Object.freeze(n),
45
+ prepareTicket: r,
46
+ admit: i,
47
+ avoidPairs: s,
48
+ now: o,
49
+ maxPreferencesBytes: c,
50
+ maxExportRows: l
51
+ });
52
+ }, j = 18e5, M = 18e5, N = 50, P = 200, F = (e, a) => {
53
+ let o = A(a), f = new Map(o.queues.map((e) => [e.name, e])), b = o.prepareTicket, F = o.admit, I = o.now, L = o.maxPreferencesBytes, R = o.maxExportRows, z = () => {
54
+ let e = I();
55
+ if (!Number.isSafeInteger(e) || e < 0 || e > 864e13) throw t("unavailable");
56
+ return e;
57
+ }, B = S, V = (e, t) => "mmq_" + x([e, t]).slice(0, 56), H = (e, t, n) => "mmt_" + x([
58
+ e,
59
+ t,
60
+ n
61
+ ]).slice(0, 56), U = (e) => e.partyId !== null && e.partyId !== void 0, W = async (e, n, r, i) => {
62
+ let a = (await e.query(y(c).where(_("id", i)).limit(1).descriptor))[0];
63
+ if (a && (a.tenantId !== n || a.subjectId !== r || !Array.isArray(a.members) || !a.members.includes(r) || !U(a) && (a.members.length !== 1 || a.members[0] !== r))) throw t("not_found");
64
+ return a;
65
+ }, G = async (e, n, r, i) => {
66
+ let a = (await e.query(y(c).where(_("id", i)).limit(1).descriptor))[0];
67
+ if (a) {
68
+ if (a.tenantId !== n || !Array.isArray(a.members) || !a.members.includes(r) || !U(a) && (a.subjectId !== r || a.members.length !== 1)) throw t("not_found");
69
+ return a;
70
+ }
71
+ }, K = async (t, n) => {
72
+ let r = B(t, n);
73
+ try {
74
+ await e.insertIgnore(u.tableName, {
75
+ id: r,
76
+ tenantId: t,
77
+ subjectId: n,
78
+ activeTicketId: null,
79
+ admission: 0,
80
+ erased: !1
81
+ }, { conflictColumns: ["id"] });
82
+ } catch (i) {
83
+ let a = (await e.query(y(u).where(_("id", r)).limit(1).descriptor))[0];
84
+ if (!a || a.tenantId !== t || a.subjectId !== n && !w(a.erased)) throw i;
85
+ }
86
+ }, q = async (t, n) => {
87
+ let r = V(t, n);
88
+ try {
89
+ await e.insertIgnore(l.tableName, {
90
+ id: r,
91
+ tenantId: t,
92
+ queue: n,
93
+ admission: 0
94
+ }, { conflictColumns: ["id"] });
95
+ } catch (i) {
96
+ let a = (await e.query(y(l).where(_("id", r)).limit(1).descriptor))[0];
97
+ if (!a || a.tenantId !== t || a.queue !== n) throw i;
98
+ }
99
+ }, J = async (n, r, i, a, o = !1) => (await K(n, r), i !== void 0 && await q(n, i), e.transactional(async (e) => {
100
+ let s = B(n, r);
101
+ if (await e.updateMany(u.tableName, { admission: 0 }, { where: _("id", s) }) !== 1) throw t("unavailable");
102
+ let c = (await e.query(y(u).where(_("id", s)).limit(1).descriptor))[0];
103
+ if (!c || c.tenantId !== n || c.subjectId !== r && !(o && c.subjectId === null && w(c.erased))) throw t(w(c?.erased) ? "erased" : "unavailable");
104
+ if (!o && w(c.erased)) throw t("erased");
105
+ if (i !== void 0) {
106
+ let r = V(n, i);
107
+ if (await e.updateMany(l.tableName, { admission: 0 }, { where: _("id", r) }) !== 1) throw t("unavailable");
108
+ let a = (await e.query(y(l).where(_("id", r)).limit(1).descriptor))[0];
109
+ if (!a || a.tenantId !== n || a.queue !== i) throw t("unavailable");
110
+ }
111
+ return a(e, c);
112
+ })), Y = (e) => m.suspend(e).pipe(m.mapError(C), m.catchDefect(() => m.fail(t("unavailable")))), X = {
113
+ enqueue: (e, a) => {
114
+ let o, s, l;
115
+ try {
116
+ if (o = k(e), !r(a.queue) || !r(a.requestId) || !a.preferences || typeof a.preferences != "object" || Array.isArray(a.preferences)) throw t("invalid_input");
117
+ let i = f.get(a.queue);
118
+ if (!i) throw t("not_found");
119
+ l = i, s = Object.freeze({
120
+ queue: a.queue,
121
+ requestId: a.requestId,
122
+ preferences: n(a.preferences, L)
123
+ });
124
+ } catch (e) {
125
+ return m.fail(C(e));
126
+ }
127
+ return m.gen(function* () {
128
+ let e = H(o.tenantId, o.id, s.requestId), r = x([s.queue, s.preferences]), a = yield* T(() => J(o.tenantId, o.id, void 0, (t) => W(t, o.tenantId, o.id, e)));
129
+ if (a) return a.requestKey !== r || a.queue !== s.queue ? yield* m.fail(t("conflict")) : O(a);
130
+ let d = yield* Y(() => b(o, l, s.preferences)), f = yield* m.try({
131
+ try: () => {
132
+ if (!d || typeof d != "object" || Array.isArray(d)) throw t("invalid_input");
133
+ let e = n(d, l.maxAttributeBytes), r = h.decodeUnknownSync(l.ticket, { onExcessProperty: "error" })(e);
134
+ if (!i(e, r)) throw t("invalid_input");
135
+ return n(r, l.maxAttributeBytes);
136
+ },
137
+ catch: C
138
+ });
139
+ return yield* T(() => J(o.tenantId, o.id, s.queue, async (n, i) => {
140
+ let a = await W(n, o.tenantId, o.id, e);
141
+ if (a) {
142
+ if (a.requestKey !== r || a.queue !== s.queue) throw t("conflict");
143
+ return O(a);
144
+ }
145
+ let d = z();
146
+ if (i.activeTicketId !== null) {
147
+ let e = await G(n, o.tenantId, o.id, E(i, "activeTicketId"));
148
+ if (!e) throw t("unavailable");
149
+ if (e.state === "open" && D(e.expiresAt) > d) throw t("already_searching");
150
+ e.state === "open" && await n.update(c.tableName, E(e, "id"), {
151
+ state: "expired",
152
+ closedAt: new Date(d)
153
+ });
154
+ }
155
+ let p = await n.query(y(c).where(g(_("tenantId", o.tenantId), _("queue", s.queue), _("state", "open"), v("expiresAt", new Date(d)))).limit(l.maxTickets + 1).forUpdate().descriptor);
156
+ if (p.some((e) => e.tenantId !== o.tenantId || e.queue !== s.queue)) throw t("unavailable");
157
+ if (p.length >= l.maxTickets) throw t("queue_full");
158
+ if (await m.runPromise(Y(() => F(o, l, n))), d + l.ticketTtlMs > 864e13 || !Number.isSafeInteger(d + l.ticketTtlMs)) throw t("invalid_input");
159
+ let h = await n.insert(c.tableName, {
160
+ id: e,
161
+ tenantId: o.tenantId,
162
+ subjectId: o.id,
163
+ queue: s.queue,
164
+ requestKey: r,
165
+ partyId: null,
166
+ members: [o.id],
167
+ attributes: f,
168
+ state: "open",
169
+ createdAt: new Date(d),
170
+ expiresAt: new Date(d + l.ticketTtlMs),
171
+ closedAt: null,
172
+ metadata: {}
173
+ });
174
+ return await n.update(u.tableName, E(i, "id"), { activeTicketId: e }), O(h);
175
+ }));
176
+ }).pipe(m.catchDefect(() => m.fail(t("unavailable"))));
177
+ },
178
+ cancel: (e, n) => {
179
+ let i;
180
+ try {
181
+ if (i = k(e), !r(n)) throw t("invalid_input");
182
+ } catch (e) {
183
+ return m.fail(C(e));
184
+ }
185
+ return T(() => J(i.tenantId, i.id, void 0, async (e, r) => {
186
+ let a = await W(e, i.tenantId, i.id, n);
187
+ if (!a) throw t("not_found");
188
+ if (U(a)) throw t("forbidden");
189
+ a.state === "open" && await e.update(c.tableName, n, {
190
+ state: "cancelled",
191
+ closedAt: new Date(z())
192
+ }), r.activeTicketId === n && await e.update(u.tableName, E(r, "id"), { activeTicketId: null });
193
+ }));
194
+ },
195
+ status: (n, i) => {
196
+ let a;
197
+ try {
198
+ if (a = k(n), !r(i)) throw t("invalid_input");
199
+ } catch (e) {
200
+ return m.fail(C(e));
201
+ }
202
+ return T(async () => {
203
+ let n = await G(e, a.tenantId, a.id, i);
204
+ if (!n) throw t("not_found");
205
+ return O(n);
206
+ });
207
+ },
208
+ estimate: (n, i) => {
209
+ let a;
210
+ try {
211
+ if (a = k(n), !r(i) || !f.has(i)) throw t("invalid_queue");
212
+ } catch (e) {
213
+ return m.fail(C(e));
214
+ }
215
+ return T(async () => {
216
+ let t = z() - j, n = await e.query(y(c).where(g(_("tenantId", a.tenantId), _("queue", i), v("createdAt", /* @__PURE__ */ new Date(t - M)))).orderBy("createdAt", "desc").limit(P).descriptor), r = [];
217
+ for (let e of n) {
218
+ if (e.tenantId !== a.tenantId || e.queue !== i || e.state !== "matched" && e.state !== "assigned" || !e.matchedAt) continue;
219
+ let n = new Date(e.matchedAt).getTime(), o = new Date(e.createdAt).getTime();
220
+ if (Number.isFinite(n) && Number.isFinite(o) && n >= t && n >= o && r.push(n - o), r.length >= N) break;
221
+ }
222
+ if (r.length < 3) return {
223
+ queue: i,
224
+ waitMs: null,
225
+ samples: r.length
226
+ };
227
+ r.sort((e, t) => e - t);
228
+ let o = r.length >> 1;
229
+ return {
230
+ queue: i,
231
+ waitMs: r.length % 2 ? r[o] : Math.round((r[o - 1] + r[o]) / 2),
232
+ samples: r.length
233
+ };
234
+ });
235
+ }
236
+ }, Z = (e) => {
237
+ if (!r(e.tenantId) || !r(e.subjectId) || !["user", "guest"].includes(e.kind)) throw t("invalid_input");
238
+ return {
239
+ tenant: e.tenantId,
240
+ owner: e.subjectId
241
+ };
242
+ }, Q = async (e, n, r) => {
243
+ let i = await e.query(y(c).where(g(_("tenantId", n), _("subjectId", r))).orderBy("id", "asc").limit(R + 1).descriptor);
244
+ if (i.length > R) throw t("export_limit");
245
+ if (i.some((e) => e.tenantId !== n || e.subjectId !== r)) throw t("unavailable");
246
+ return i;
247
+ };
248
+ return {
249
+ service: X,
250
+ enqueueGroup: (a, o, s, d, p) => {
251
+ let S, A, j;
252
+ try {
253
+ if (S = k(a), !r(s) || !r(o.queue) || !r(o.requestId) || !o.preferences || typeof o.preferences != "object" || Array.isArray(o.preferences)) throw t("invalid_input");
254
+ let e = f.get(o.queue);
255
+ if (!e) throw t("not_found");
256
+ j = e, A = Object.freeze({
257
+ queue: o.queue,
258
+ requestId: o.requestId,
259
+ preferences: n(o.preferences, L)
260
+ });
261
+ } catch (e) {
262
+ return m.fail(C(e));
263
+ }
264
+ return m.gen(function* () {
265
+ let r = H(S.tenantId, S.id, "party:" + s + ":" + A.requestId), a = x([
266
+ A.queue,
267
+ A.preferences,
268
+ s
269
+ ]), o = yield* Y(() => b(S, j, A.preferences)), f = yield* m.try({
270
+ try: () => {
271
+ if (!o || typeof o != "object" || Array.isArray(o)) throw t("invalid_input");
272
+ let e = n(o, j.maxAttributeBytes), r = h.decodeUnknownSync(j.ticket, { onExcessProperty: "error" })(e);
273
+ if (!i(e, r)) throw t("invalid_input");
274
+ return n(r, j.maxAttributeBytes);
275
+ },
276
+ catch: C
277
+ });
278
+ return yield* T(async () => {
279
+ let n = new Set(await p());
280
+ for (let e of n) await K(S.tenantId, e);
281
+ return await q(S.tenantId, A.queue), e.transactional(async (e) => {
282
+ let i = [...await d(e)].sort();
283
+ if (!i.includes(S.id) || i.some((e) => !n.has(e))) throw t("conflict");
284
+ let o = await W(e, S.tenantId, S.id, r);
285
+ if (o) {
286
+ if (o.requestKey !== a || o.queue !== A.queue) throw t("conflict");
287
+ return O(o);
288
+ }
289
+ let p = z(), h = [];
290
+ for (let n of i) {
291
+ let r = B(S.tenantId, n);
292
+ if (await e.updateMany(u.tableName, { admission: 0 }, { where: _("id", r) }) !== 1) throw t("unavailable");
293
+ let i = (await e.query(y(u).where(_("id", r)).limit(1).descriptor))[0];
294
+ if (!i || i.tenantId !== S.tenantId || i.subjectId !== n) throw t(w(i?.erased) ? "erased" : "unavailable");
295
+ if (i.activeTicketId !== null) {
296
+ let n = (await e.query(y(c).where(_("id", E(i, "activeTicketId"))).limit(1).descriptor))[0];
297
+ if (n && n.tenantId !== S.tenantId) throw t("unavailable");
298
+ if (n && n.state === "open" && D(n.expiresAt) > p) throw t("already_searching");
299
+ n && n.state === "open" && await e.update(c.tableName, E(n, "id"), {
300
+ state: "expired",
301
+ closedAt: new Date(p)
302
+ });
303
+ }
304
+ h.push(i);
305
+ }
306
+ let b = V(S.tenantId, A.queue);
307
+ if (await e.updateMany(l.tableName, { admission: 0 }, { where: _("id", b) }) !== 1) throw t("unavailable");
308
+ let x = await e.query(y(c).where(g(_("tenantId", S.tenantId), _("queue", A.queue), _("state", "open"), v("expiresAt", new Date(p)))).limit(j.maxTickets + 1).forUpdate().descriptor);
309
+ if (x.some((e) => e.tenantId !== S.tenantId || e.queue !== A.queue)) throw t("unavailable");
310
+ if (x.length >= j.maxTickets) throw t("queue_full");
311
+ if (await m.runPromise(Y(() => F(S, j, e))), p + j.ticketTtlMs > 864e13 || !Number.isSafeInteger(p + j.ticketTtlMs)) throw t("invalid_input");
312
+ let C = await e.insert(c.tableName, {
313
+ id: r,
314
+ tenantId: S.tenantId,
315
+ subjectId: S.id,
316
+ queue: A.queue,
317
+ requestKey: a,
318
+ partyId: s,
319
+ members: i,
320
+ attributes: f,
321
+ state: "open",
322
+ createdAt: new Date(p),
323
+ expiresAt: new Date(p + j.ticketTtlMs),
324
+ closedAt: null,
325
+ metadata: {}
326
+ });
327
+ for (let t of h) await e.update(u.tableName, E(t, "id"), { activeTicketId: r });
328
+ return O(C);
329
+ });
330
+ });
331
+ }).pipe(m.catchDefect(() => m.fail(t("unavailable"))));
332
+ },
333
+ director: p(e, {
334
+ queues: o.queues,
335
+ avoidPairs: o.avoidPairs,
336
+ now: I
337
+ }),
338
+ exportSubject: (e) => {
339
+ let t;
340
+ try {
341
+ t = Z(e);
342
+ } catch (e) {
343
+ return m.fail(C(e));
344
+ }
345
+ return T(() => J(t.tenant, t.owner, void 0, async (e) => ({
346
+ tickets: (await Q(e, t.tenant, t.owner)).map((e) => O(e)),
347
+ assignments: await d(e, t.tenant, t.owner, R)
348
+ }), !0));
349
+ },
350
+ eraseSubject: (e) => {
351
+ let t;
352
+ try {
353
+ t = Z(e);
354
+ } catch (e) {
355
+ return m.fail(C(e));
356
+ }
357
+ return T(() => J(t.tenant, t.owner, void 0, async (e, n) => {
358
+ let r = await Q(e, t.tenant, t.owner);
359
+ for (let t of r) await e.delete(c.tableName, E(t, "id"));
360
+ await e.deleteMany(s.tableName, { where: g(_("tenantId", t.tenant), _("subjectId", t.owner)) }), await e.update(u.tableName, E(n, "id"), {
361
+ subjectId: null,
362
+ activeTicketId: null,
363
+ erased: !0
364
+ });
365
+ }, !0));
366
+ }
367
+ };
368
+ };
369
+ export { F as dataStoreMatchmakingTickets, S as matchmakingSubjectKey, A as resolveTicketAdmissionOptions };
@@ -0,0 +1,97 @@
1
+ import { DataStore } from '@voltro/database';
2
+ import { Effect } from 'effect';
3
+ import { Schema } from 'effect';
4
+ import { YieldableError } from 'effect/Cause';
5
+
6
+ declare type AvoidPair = readonly [string, string];
7
+
8
+ /** Ordered policies consume disjoint proposals, sharing the original branch budget. */
9
+ export declare const composite: <A = Readonly<Record<string, unknown>>>(functions: ReadonlyArray<MatchFunction<A>>) => MatchFunction<A>;
10
+
11
+ /** Empty policy for an app without a block provider. Automatic optional Social
12
+ * port binding is separate integration work; it is not inferred here. */
13
+ export declare const defaultMatchmakingAvoidPairs: MatchmakingAvoidPairs;
14
+
15
+ export declare const fifo: <A = Readonly<Record<string, unknown>>>() => MatchFunction<A>;
16
+
17
+ declare type MatchFunction<A = Readonly<Record<string, unknown>>> = (input: MatchInput<A>) => MatchOutput<A>;
18
+
19
+ declare interface MatchInput<A = Readonly<Record<string, unknown>>> {
20
+ readonly tickets: ReadonlyArray<MatchTicket<A>>;
21
+ readonly now: number;
22
+ readonly queue: MatchQueueRules;
23
+ readonly avoid: ReadonlyArray<AvoidPair>;
24
+ }
25
+
26
+ export declare interface MatchmakingAvoidContext {
27
+ readonly tenantId: string;
28
+ readonly queue: string;
29
+ readonly tickets: ReadonlyArray<MatchTicket>;
30
+ readonly maxPairs: number;
31
+ readonly query: DataStore['query'];
32
+ }
33
+
34
+ export declare type MatchmakingAvoidPairs = (context: MatchmakingAvoidContext) => Effect.Effect<ReadonlyArray<AvoidPair>, MatchmakingError>;
35
+
36
+ declare class MatchmakingError extends MatchmakingError_base {
37
+ }
38
+
39
+ declare const MatchmakingError_base: Schema.Class<MatchmakingError, Schema.TaggedStruct<"MatchmakingError", {
40
+ readonly code: Schema.Literals<readonly ["invalid_input", "invalid_queue", "invalid_proposal", "unavailable", "identity_required", "not_found", "already_searching", "conflict", "queue_full", "erased", "export_limit", "forbidden", "party_full", "already_in_party", "party_not_ready"]>;
41
+ }>, YieldableError>;
42
+
43
+ declare interface MatchOutput<A = Readonly<Record<string, unknown>>> {
44
+ readonly proposals: ReadonlyArray<MatchProposal<A>>;
45
+ readonly leftover: ReadonlyArray<MatchTicket<A>>;
46
+ /** Deterministic branch inspections, not a runtime or throughput measurement. */
47
+ readonly searchNodes: number;
48
+ readonly exhausted: boolean;
49
+ }
50
+
51
+ declare interface MatchProposal<A = Readonly<Record<string, unknown>>> {
52
+ readonly teams: ReadonlyArray<ReadonlyArray<MatchTicket<A>>>;
53
+ readonly quality: number;
54
+ }
55
+
56
+ declare interface MatchQueueRules {
57
+ readonly name: string;
58
+ readonly teams: {
59
+ readonly count: number;
60
+ readonly size: number;
61
+ };
62
+ readonly maxTickets: number;
63
+ readonly maxSearchNodes: number;
64
+ readonly maxAvoidPairs: number;
65
+ readonly maxAttributeBytes: number;
66
+ readonly minQuality: {
67
+ readonly initial: number;
68
+ readonly after: ReadonlyArray<readonly [number, number]>;
69
+ };
70
+ }
71
+
72
+ declare interface MatchTicket<A = Readonly<Record<string, unknown>>> {
73
+ readonly id: string;
74
+ readonly tenantId: string;
75
+ readonly queue: string;
76
+ readonly members: ReadonlyArray<string>;
77
+ readonly createdAt: number;
78
+ readonly attributes: A;
79
+ }
80
+
81
+ export declare const regionLatency: (options: {
82
+ readonly latencyMaxMs: number;
83
+ }) => <A extends Readonly<Record<string, unknown>>>(input: MatchInput<A>) => MatchOutput<A>;
84
+
85
+ export declare const skillWindow: (options: SkillWindowOptions) => <A extends Readonly<Record<string, unknown>>>(input: MatchInput<A>) => MatchOutput<A>;
86
+
87
+ export declare interface SkillWindowOptions {
88
+ readonly initialWindow: number;
89
+ readonly growPerSecond: number;
90
+ readonly maxWindow: number;
91
+ readonly latencyMaxMs?: number;
92
+ }
93
+
94
+ /** Reject custom slot fabrication, overlapping assignments and omitted leftovers. */
95
+ export declare const validateMatchOutput: <A>(raw: MatchInput<A>, output: MatchOutput<A>) => MatchOutput<A>;
96
+
97
+ export { }
@@ -0,0 +1,140 @@
1
+ import { matchmakingError as e } from "./errors.js";
2
+ import { captureMatchInput as t, minimumQuality as n, sameJson as r } from "./contract.js";
3
+ import { packCapturedTeams as i } from "./packing.js";
4
+ import { defaultMatchmakingAvoidPairs as a } from "./avoidPairs.js";
5
+ // src/strategies.ts
6
+ var o = (e, t) => e.now - Math.min(...t.flat().map((e) => e.createdAt)), s = (r, a) => {
7
+ let s = t(r), c = [], l = s.tickets, u = 0, d = !1;
8
+ for (; l.length > 0;) {
9
+ if (u === s.queue.maxSearchNodes) {
10
+ d = !0;
11
+ break;
12
+ }
13
+ let t = {
14
+ ...s,
15
+ tickets: l,
16
+ queue: {
17
+ ...s.queue,
18
+ maxSearchNodes: s.queue.maxSearchNodes - u
19
+ }
20
+ }, r = 0, f = i(t, (t) => {
21
+ let i = a(s, t);
22
+ if (i === null) return !1;
23
+ if (!Number.isFinite(i) || i < 0 || i > 1) throw e("invalid_proposal");
24
+ return i < n(s.queue, o(s, t)) ? !1 : (r = i, !0);
25
+ });
26
+ if (u += f.searchNodes, f.teams === null) {
27
+ d = f.exhausted;
28
+ break;
29
+ }
30
+ c.push(Object.freeze({
31
+ teams: f.teams,
32
+ quality: r
33
+ }));
34
+ let p = new Set(f.teams.flat().map((e) => e.id));
35
+ l = Object.freeze(l.filter((e) => !p.has(e.id)));
36
+ }
37
+ return Object.freeze({
38
+ proposals: Object.freeze(c),
39
+ leftover: l,
40
+ searchNodes: u,
41
+ exhausted: d
42
+ });
43
+ }, c = () => (e) => s(e, () => 1), l = (t, n) => {
44
+ let r = t.attributes?.[n];
45
+ if (typeof r != "number" || !Number.isFinite(r)) throw e("invalid_input");
46
+ return r;
47
+ }, u = (t, n) => {
48
+ let r = t.flat(), i = r[0]?.attributes.region;
49
+ if (typeof i != "string" || !i.trim()) throw e("invalid_input");
50
+ let a = 0;
51
+ for (let t of r) {
52
+ let r = t.attributes.region, o = t.attributes.latencies;
53
+ if (typeof r != "string" || !r.trim() || !o || typeof o != "object" || Array.isArray(o)) throw e("invalid_input");
54
+ let s = o[r];
55
+ if (typeof s != "number" || !Number.isFinite(s) || s < 0) throw e("invalid_input");
56
+ if (r !== i || s > n) return null;
57
+ a = Math.max(a, s);
58
+ }
59
+ return a;
60
+ }, d = (n) => {
61
+ if (!n || typeof n != "object") throw e("invalid_queue");
62
+ let r = n.initialWindow, i = n.growPerSecond, a = n.maxWindow, c = n.latencyMaxMs;
63
+ if (![
64
+ r,
65
+ i,
66
+ a
67
+ ].every((e) => Number.isFinite(e) && e >= 0 && e <= 0xe8d4a51000) || r > a || c !== void 0 && (!Number.isFinite(c) || c < 0 || c > 6e4)) throw e("invalid_queue");
68
+ return (e) => {
69
+ let n = t(e);
70
+ return n.tickets.forEach((e) => l(e, "skill")), s(n, (e, t) => {
71
+ let n = t.flat().map((e) => l(e, "skill")), s = Math.max(...n) - Math.min(...n);
72
+ return s > Math.min(a, r + i * (o(e, t) / 1e3)) || c !== void 0 && u(t, c) === null ? null : a === 0 ? 1 : Math.max(0, 1 - s / a);
73
+ });
74
+ };
75
+ }, f = (t) => {
76
+ if (!t || typeof t != "object") throw e("invalid_queue");
77
+ let n = t.latencyMaxMs;
78
+ if (!Number.isFinite(n) || n < 0 || n > 6e4) throw e("invalid_queue");
79
+ return (e) => s(e, (e, t) => {
80
+ let r = u(t, n);
81
+ return r === null ? null : n === 0 ? 1 : Math.max(0, 1 - r / n);
82
+ });
83
+ }, p = (i, a) => {
84
+ let s = t(i), c = new Map(s.tickets.map((e) => [e.id, e])), l = /* @__PURE__ */ new Set();
85
+ if (!a || !Array.isArray(a.proposals) || a.proposals.length > s.tickets.length || !Array.isArray(a.leftover) || !Number.isSafeInteger(a.searchNodes) || a.searchNodes < 0 || a.searchNodes > s.queue.maxSearchNodes || typeof a.exhausted != "boolean") throw e("invalid_proposal");
86
+ let u = (t) => {
87
+ let n = c.get(t?.id);
88
+ if (!n || l.has(n.id) || !r(n, t)) throw e("invalid_proposal");
89
+ return l.add(n.id), n;
90
+ }, d = Array.from(a.proposals, (t) => {
91
+ if (!t || !Array.isArray(t.teams) || t.teams.length !== s.queue.teams.count || !Number.isFinite(t.quality) || t.quality < 0 || t.quality > 1) throw e("invalid_proposal");
92
+ let r = Array.from(t.teams, (t) => {
93
+ if (!Array.isArray(t) || t.length < 1 || t.length > s.queue.teams.size) throw e("invalid_proposal");
94
+ let n = Array.from(t, u);
95
+ if (n.reduce((e, t) => e + t.members.length, 0) !== s.queue.teams.size) throw e("invalid_proposal");
96
+ return Object.freeze(n);
97
+ }), i = new Set(r.flat().flatMap((e) => [...e.members]));
98
+ if (s.avoid.some(([e, t]) => i.has(e) && i.has(t)) || t.quality < n(s.queue, o(s, r))) throw e("invalid_proposal");
99
+ return Object.freeze({
100
+ teams: Object.freeze(r),
101
+ quality: t.quality
102
+ });
103
+ }), f = Array.from(a.leftover, u);
104
+ if (l.size !== s.tickets.length) throw e("invalid_proposal");
105
+ return f.sort((e, t) => e.createdAt - t.createdAt || (e.id < t.id ? -1 : +(e.id > t.id))), Object.freeze({
106
+ proposals: Object.freeze(d),
107
+ leftover: Object.freeze(f),
108
+ searchNodes: a.searchNodes,
109
+ exhausted: a.exhausted
110
+ });
111
+ }, m = (n) => {
112
+ if (!Array.isArray(n) || n.length < 1 || n.length > 32 || n.some((e) => typeof e != "function")) throw e("invalid_queue");
113
+ let r = Object.freeze([...n]);
114
+ return (e) => {
115
+ let n = t(e), i = [], a = n.tickets, o = 0, s = !1;
116
+ for (let e of r) {
117
+ if (a.length === 0) break;
118
+ if (o === n.queue.maxSearchNodes) {
119
+ s = !0;
120
+ break;
121
+ }
122
+ let t = Object.freeze({
123
+ ...n,
124
+ tickets: a,
125
+ queue: Object.freeze({
126
+ ...n.queue,
127
+ maxSearchNodes: n.queue.maxSearchNodes - o
128
+ })
129
+ }), r = p(t, e(t));
130
+ i.push(...r.proposals), a = r.leftover, o += r.searchNodes, s ||= r.exhausted;
131
+ }
132
+ return Object.freeze({
133
+ proposals: Object.freeze(i),
134
+ leftover: a,
135
+ searchNodes: o,
136
+ exhausted: s
137
+ });
138
+ };
139
+ };
140
+ export { m as composite, a as defaultMatchmakingAvoidPairs, c as fifo, f as regionLatency, d as skillWindow, p as validateMatchOutput };