@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,337 @@
1
+ import { ColumnBuilder } from '@voltro/database';
2
+ import { Context } from 'effect';
3
+ import { DataStore } from '@voltro/database';
4
+ import { Effect } from 'effect';
5
+ import { FieldDefinitions } from '@voltro/database';
6
+ import { PluginPrivacyTarget } from '@voltro/protocol';
7
+ import { Schema } from 'effect';
8
+ import { Subject } from '@voltro/protocol';
9
+ import { Table } from '@voltro/database';
10
+ import { YieldableError } from 'effect/Cause';
11
+
12
+ declare type AvoidPair = readonly [string, string];
13
+
14
+ /**
15
+ * The Director's write stage (Plan 04 §3). Per accepted proposal: fence every member,
16
+ * claim all its tickets `open → matched` at once, allocate seats outside SQL, then record
17
+ * the assignment and close the tickets `assigned`. A proposal with any changed ticket is
18
+ * skipped whole; a failed allocation reopens its tickets with `attempts + 1`.
19
+ */
20
+ export declare const dataStoreMatchmakingAssigner: (store: DataStore, options: MatchmakingAssignerOptions) => {
21
+ assign: (subject: Subject, plan: MatchmakingPlan) => Effect.Effect<MatchmakingAssignResult, MatchmakingError>;
22
+ };
23
+
24
+ /** Trusted read/planning stage of a Director. It borrows the installed SQL store
25
+ * and calls the real queue function. Proposals claim no tickets or Room seats;
26
+ * a later writer must recheck and atomically claim every original ticket. */
27
+ export declare const dataStoreMatchmakingPlanner: (store: DataStore, options: MatchmakingPlanningOptions) => {
28
+ plan: (subject: Subject, original: MatchmakingPlanInput) => Effect.Effect<MatchmakingPlan, MatchmakingError>;
29
+ };
30
+
31
+ /** SQL tickets: solo searches plus leader-owned party searches. Matching/assignment and HotStore indexing remain separate work. */
32
+ export declare const dataStoreMatchmakingTickets: (store: DataStore, input: TicketAdmissionOptions) => {
33
+ service: MatchmakingService;
34
+ enqueueGroup: (subject: Subject, intent: EnqueueIntent, partyId: string, verify: (tx: DataStore) => Promise<ReadonlyArray<string>>, preview: () => Promise<ReadonlyArray<string>>) => Effect.Effect<MatchmakingTicket, MatchmakingError>;
35
+ director: {
36
+ plan: (subject: Subject, original: MatchmakingPlanInput) => Effect.Effect< MatchmakingPlan, MatchmakingError>;
37
+ };
38
+ exportSubject: (target: PluginPrivacyTarget) => Effect.Effect<{
39
+ tickets: {
40
+ readonly id: string;
41
+ readonly queue: string;
42
+ readonly state: "open" | "expired" | "matched" | "cancelled" | "assigned";
43
+ readonly members: readonly string[];
44
+ readonly attributes: {
45
+ readonly [x: string]: unknown;
46
+ };
47
+ readonly createdAt: number;
48
+ readonly expiresAt: number;
49
+ readonly closedAt: number | null;
50
+ readonly metadata: {
51
+ readonly [x: string]: unknown;
52
+ };
53
+ }[];
54
+ assignments: readonly {
55
+ readonly ticketId: string;
56
+ readonly queue: string;
57
+ readonly roomId: string;
58
+ readonly sessionId: string;
59
+ readonly createdAt: number;
60
+ }[];
61
+ }, MatchmakingError, never>;
62
+ eraseSubject: (target: PluginPrivacyTarget) => Effect.Effect<void, MatchmakingError, never>;
63
+ };
64
+
65
+ /** Empty policy for an app without a block provider. Automatic optional Social
66
+ * port binding is separate integration work; it is not inferred here. */
67
+ export declare const defaultMatchmakingAvoidPairs: MatchmakingAvoidPairs;
68
+
69
+ /** Players send a queue preference intent; never skill, members, state or assignments. */
70
+ declare interface EnqueueIntent {
71
+ readonly queue: string;
72
+ readonly requestId: string;
73
+ readonly preferences: Readonly<Record<string, unknown>>;
74
+ }
75
+
76
+ declare type MatchFunction<A = Readonly<Record<string, unknown>>> = (input: MatchInput<A>) => MatchOutput<A>;
77
+
78
+ declare interface MatchInput<A = Readonly<Record<string, unknown>>> {
79
+ readonly tickets: ReadonlyArray<MatchTicket<A>>;
80
+ readonly now: number;
81
+ readonly queue: MatchQueueRules;
82
+ readonly avoid: ReadonlyArray<AvoidPair>;
83
+ }
84
+
85
+ export declare interface MatchmakingAllocation {
86
+ readonly roomId: string;
87
+ /** Every member's seat session id; tokens stay with Rooms. */
88
+ readonly seats: Readonly<Record<string, string>>;
89
+ }
90
+
91
+ /** What the allocator must create for one accepted proposal: a Room with a seat per member. */
92
+ export declare interface MatchmakingAllocationRequest {
93
+ readonly tenantId: string;
94
+ readonly queue: string;
95
+ /** Stable per proposal: the same tickets always yield the same key, so allocation can be idempotent. */
96
+ readonly requestId: string;
97
+ /** Member subject ids per team, in proposal order. */
98
+ readonly teams: ReadonlyArray<ReadonlyArray<string>>;
99
+ }
100
+
101
+ export declare interface MatchmakingAssignerOptions {
102
+ readonly allocator: MatchmakingRoomAllocator;
103
+ /** Failed allocations before a ticket expires. Default 3. */
104
+ readonly maxAttempts?: number;
105
+ readonly now?: () => number;
106
+ }
107
+
108
+ declare type MatchmakingAssignment = typeof MatchmakingAssignmentSchema.Type;
109
+
110
+ declare const MatchmakingAssignmentSchema: Schema.Struct<{
111
+ readonly ticketId: Schema.String;
112
+ readonly queue: Schema.String;
113
+ readonly roomId: Schema.String;
114
+ readonly sessionId: Schema.String;
115
+ readonly createdAt: Schema.Number;
116
+ }>;
117
+
118
+ /** One row per member of a matched ticket: the Room and that member's seat session (never a token). */
119
+ export declare const matchmakingAssignmentsTable: Table<"_voltro_mm_assignments", FieldDefinitions<{
120
+ readonly id: ColumnBuilder<string, "id", boolean>;
121
+ readonly tenantId: ColumnBuilder<string, "text", boolean>;
122
+ readonly subjectId: ColumnBuilder<string, "text", boolean>;
123
+ readonly ticketId: ColumnBuilder<string, "text", boolean>;
124
+ readonly queue: ColumnBuilder<string, "text", boolean>;
125
+ readonly roomId: ColumnBuilder<string, "text", boolean>;
126
+ readonly sessionId: ColumnBuilder<string, "text", boolean>;
127
+ readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
128
+ }>, true, never>;
129
+
130
+ export declare interface MatchmakingAssignResult {
131
+ readonly assigned: ReadonlyArray<{
132
+ readonly requestId: string;
133
+ readonly roomId: string;
134
+ readonly tickets: ReadonlyArray<string>;
135
+ }>;
136
+ /** Proposals whose tickets changed before the claim (cancelled, expired, taken by another Director). */
137
+ readonly skipped: number;
138
+ /** Proposals whose allocation failed; their tickets are open again (or expired at the limit). */
139
+ readonly failed: number;
140
+ }
141
+
142
+ export declare interface MatchmakingAvoidContext {
143
+ readonly tenantId: string;
144
+ readonly queue: string;
145
+ readonly tickets: ReadonlyArray<MatchTicket>;
146
+ readonly maxPairs: number;
147
+ readonly query: DataStore['query'];
148
+ }
149
+
150
+ export declare type MatchmakingAvoidPairs = (context: MatchmakingAvoidContext) => Effect.Effect<ReadonlyArray<AvoidPair>, MatchmakingError>;
151
+
152
+ declare class MatchmakingError extends MatchmakingError_base {
153
+ }
154
+
155
+ declare const MatchmakingError_base: Schema.Class<MatchmakingError, Schema.TaggedStruct<"MatchmakingError", {
156
+ 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"]>;
157
+ }>, YieldableError>;
158
+
159
+ declare type MatchmakingEstimate = typeof MatchmakingEstimateSchema.Type;
160
+
161
+ /** Expected wait in a queue: the median time recent searches of this tenant waited until matched, from up to
162
+ * `samples` searches matched in the last 30 minutes. `waitMs` is null with fewer than three such searches. */
163
+ declare const MatchmakingEstimateSchema: Schema.Struct<{
164
+ readonly queue: Schema.String;
165
+ readonly waitMs: Schema.NullOr<Schema.Number>;
166
+ readonly samples: Schema.Number;
167
+ }>;
168
+
169
+ export declare interface MatchmakingPlan extends MatchOutput {
170
+ readonly tenantId: string;
171
+ readonly queue: string;
172
+ readonly observedAt: number;
173
+ }
174
+
175
+ export declare interface MatchmakingPlanInput {
176
+ readonly tenantId: string;
177
+ readonly queue: string;
178
+ }
179
+
180
+ export declare interface MatchmakingPlanningOptions {
181
+ readonly queues: ReadonlyArray<QueueDescriptor<any>>;
182
+ readonly avoidPairs: MatchmakingAvoidPairs;
183
+ readonly now: () => number;
184
+ }
185
+
186
+ export declare const matchmakingQueuesTable: Table<"_voltro_mm_queue_admission", FieldDefinitions<{
187
+ readonly id: ColumnBuilder<string, "id", boolean>;
188
+ readonly tenantId: ColumnBuilder<string, "text", boolean>;
189
+ readonly queue: ColumnBuilder<string, "text", boolean>;
190
+ readonly admission: ColumnBuilder<number, "integer", boolean>;
191
+ }>, true, never>;
192
+
193
+ /** Room seats for a match. `roomsAllocator` (`@voltro/plugin-matchmaking/rooms`) is the Rooms implementation. */
194
+ export declare interface MatchmakingRoomAllocator {
195
+ readonly allocate: (request: MatchmakingAllocationRequest) => Effect.Effect<MatchmakingAllocation, MatchmakingError>;
196
+ /** Undo a completed allocation the assignment could not record. */
197
+ readonly release: (request: MatchmakingAllocationRequest, allocation: MatchmakingAllocation) => Effect.Effect<void, MatchmakingError>;
198
+ }
199
+
200
+ /** Queries wrap status reactively; this port owns no transport or poll timer. */
201
+ declare interface MatchmakingService {
202
+ readonly enqueue: (subject: Subject, input: EnqueueIntent) => Effect.Effect<MatchmakingTicket, MatchmakingError>;
203
+ readonly cancel: (subject: Subject, ticketId: string) => Effect.Effect<void, MatchmakingError>;
204
+ readonly status: (subject: Subject, ticketId: string) => Effect.Effect<MatchmakingTicket, MatchmakingError>;
205
+ /** Median recent wait in a declared queue for the caller's tenant. Optional for replacement ports: without it
206
+ * `matchmaking.estimate` answers `unavailable`. */
207
+ readonly estimate?: (subject: Subject, queue: string) => Effect.Effect<MatchmakingEstimate, MatchmakingError>;
208
+ }
209
+
210
+ export declare const matchmakingSubjectsTable: Table<"_voltro_mm_subject_admission", FieldDefinitions<{
211
+ readonly id: ColumnBuilder<string, "id", boolean>;
212
+ readonly tenantId: ColumnBuilder<string, "text", boolean>;
213
+ readonly subjectId: ColumnBuilder<string | null, "text", boolean>;
214
+ readonly activeTicketId: ColumnBuilder<string | null, "text", boolean>;
215
+ readonly admission: ColumnBuilder<number, "integer", boolean>;
216
+ readonly erased: ColumnBuilder<boolean, "boolean", boolean>;
217
+ }>, true, never>;
218
+
219
+ declare type MatchmakingTicket = typeof MatchmakingTicketSchema.Type;
220
+
221
+ declare const MatchmakingTicketSchema: Schema.Struct<{
222
+ readonly id: Schema.String;
223
+ readonly queue: Schema.String;
224
+ readonly state: Schema.Literals<readonly ["open", "matched", "assigned", "cancelled", "expired"]>;
225
+ readonly members: Schema.$Array<Schema.String>;
226
+ readonly attributes: Schema.$Record<Schema.String, Schema.Unknown>;
227
+ readonly createdAt: Schema.Number;
228
+ readonly expiresAt: Schema.Number;
229
+ readonly closedAt: Schema.NullOr<Schema.Number>;
230
+ readonly metadata: Schema.$Record<Schema.String, Schema.Unknown>;
231
+ }>;
232
+
233
+ export declare const matchmakingTicketsTable: Table<"_voltro_mm_tickets", FieldDefinitions<{
234
+ readonly id: ColumnBuilder<string, "id", boolean>;
235
+ readonly tenantId: ColumnBuilder<string, "text", boolean>;
236
+ readonly subjectId: ColumnBuilder<string, "text", boolean>;
237
+ readonly queue: ColumnBuilder<string, "text", boolean>;
238
+ readonly requestKey: ColumnBuilder<string, "text", boolean>;
239
+ /** Set for a party search: the leader owns the row, every member's claim points at it. */
240
+ readonly partyId: ColumnBuilder<string | null, "text", boolean>;
241
+ readonly members: ColumnBuilder<unknown, "json", boolean>;
242
+ readonly attributes: ColumnBuilder<unknown, "json", boolean>;
243
+ readonly state: ColumnBuilder<"open" | "expired" | "matched" | "cancelled" | "assigned", "text", boolean>;
244
+ /** Failed allocations; a ticket expires after the queue's limit. */
245
+ readonly attempts: ColumnBuilder<number, "integer", true>;
246
+ readonly matchedAt: ColumnBuilder<Date | null, "timestamp", boolean>;
247
+ readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
248
+ readonly expiresAt: ColumnBuilder<Date, "timestamp", boolean>;
249
+ readonly closedAt: ColumnBuilder<Date | null, "timestamp", boolean>;
250
+ readonly metadata: ColumnBuilder<unknown, "json", boolean>;
251
+ }>, true, never>;
252
+
253
+ declare interface MatchOutput<A = Readonly<Record<string, unknown>>> {
254
+ readonly proposals: ReadonlyArray<MatchProposal<A>>;
255
+ readonly leftover: ReadonlyArray<MatchTicket<A>>;
256
+ /** Deterministic branch inspections, not a runtime or throughput measurement. */
257
+ readonly searchNodes: number;
258
+ readonly exhausted: boolean;
259
+ }
260
+
261
+ declare interface MatchProposal<A = Readonly<Record<string, unknown>>> {
262
+ readonly teams: ReadonlyArray<ReadonlyArray<MatchTicket<A>>>;
263
+ readonly quality: number;
264
+ }
265
+
266
+ declare interface MatchQueueRules {
267
+ readonly name: string;
268
+ readonly teams: {
269
+ readonly count: number;
270
+ readonly size: number;
271
+ };
272
+ readonly maxTickets: number;
273
+ readonly maxSearchNodes: number;
274
+ readonly maxAvoidPairs: number;
275
+ readonly maxAttributeBytes: number;
276
+ readonly minQuality: {
277
+ readonly initial: number;
278
+ readonly after: ReadonlyArray<readonly [number, number]>;
279
+ };
280
+ }
281
+
282
+ declare interface MatchTicket<A = Readonly<Record<string, unknown>>> {
283
+ readonly id: string;
284
+ readonly tenantId: string;
285
+ readonly queue: string;
286
+ readonly members: ReadonlyArray<string>;
287
+ readonly createdAt: number;
288
+ readonly attributes: A;
289
+ }
290
+
291
+ /** A detached current SQL roster. It contains IDs, never inferred Subject kinds. */
292
+ export declare interface PartyRoster {
293
+ readonly tenantId: string;
294
+ readonly partyId: string;
295
+ readonly leaderId: string;
296
+ readonly memberIds: ReadonlyArray<string>;
297
+ }
298
+
299
+ export declare class PartyRosterPort extends PartyRosterPort_base {
300
+ }
301
+
302
+ declare const PartyRosterPort_base: Context.ServiceClass<PartyRosterPort, "@voltro/PartyRosterPort", PartyRosterService>;
303
+
304
+ /** Trusted server composition only. The caller must authorize the operation;
305
+ * this read is not a player route or authority to move the returned members. */
306
+ export declare interface PartyRosterService {
307
+ readonly read: (tenantId: string, partyId: string) => Effect.Effect<PartyRoster | null, MatchmakingError>;
308
+ }
309
+
310
+ declare interface QueueDescriptor<S extends Schema.Codec<unknown, unknown>> extends MatchQueueRules {
311
+ readonly kind: 'matchmaking.queue';
312
+ readonly room: string;
313
+ readonly ticket: S;
314
+ readonly matchFunction: MatchFunction<(S)["Type"]>;
315
+ readonly pools: {
316
+ readonly byRegion: (attributes: (S)["Type"]) => string;
317
+ } | undefined;
318
+ readonly cycleMs: number;
319
+ readonly ticketTtlMs: number;
320
+ }
321
+
322
+ /** The caller's own seats from assignments, newest first. */
323
+ export declare const readMatchmakingAssignments: (store: Pick<DataStore, "query">, tenantId: string, subjectId: string, limit?: number) => Promise<ReadonlyArray<MatchmakingAssignment>>;
324
+
325
+ export declare interface TicketAdmissionOptions {
326
+ readonly queues: ReadonlyArray<QueueDescriptor<any>>;
327
+ /** Resolve trusted skill/latency/policy attributes before SQL; no client state is copied. */
328
+ readonly prepareTicket: (subject: Subject, queue: QueueDescriptor<any>, preferences: EnqueueIntent['preferences']) => Effect.Effect<Readonly<Record<string, unknown>>, MatchmakingError>;
329
+ /** Re-check durable eligibility in the exact borrowed transaction after writer admission. */
330
+ readonly admit: (subject: Subject, queue: QueueDescriptor<any>, tx: DataStore) => Effect.Effect<void, MatchmakingError>;
331
+ readonly maxPreferencesBytes?: number;
332
+ readonly maxExportRows?: number;
333
+ readonly avoidPairs?: MatchmakingAvoidPairs;
334
+ readonly now?: () => number;
335
+ }
336
+
337
+ export { }
package/dist/server.js ADDED
@@ -0,0 +1,8 @@
1
+ import { defaultMatchmakingAvoidPairs as e } from "./avoidPairs.js";
2
+ import { PartyRosterPort as t } from "./port.js";
3
+ import { matchmakingAssignmentsTable as n, matchmakingTicketsTable as r } from "./tickets.entity.js";
4
+ import { matchmakingQueuesTable as i, matchmakingSubjectsTable as a } from "./admission.entity.js";
5
+ import { dataStoreMatchmakingAssigner as o, readMatchmakingAssignments as s } from "./assignment.js";
6
+ import { dataStoreMatchmakingPlanner as c } from "./director.js";
7
+ import { dataStoreMatchmakingTickets as l } from "./service.js";
8
+ export { t as PartyRosterPort, o as dataStoreMatchmakingAssigner, c as dataStoreMatchmakingPlanner, l as dataStoreMatchmakingTickets, e as defaultMatchmakingAvoidPairs, n as matchmakingAssignmentsTable, i as matchmakingQueuesTable, a as matchmakingSubjectsTable, r as matchmakingTicketsTable, s as readMatchmakingAssignments };