@voltro/plugin-matchmaking 0.0.0-stage → 0.87.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 +24329 -0
  2. package/LICENSE +61 -0
  3. package/README.md +272 -2
  4. package/SECURITY.md +56 -0
  5. package/THIRD-PARTY-NOTICES.md +7737 -0
  6. package/dist/admission.entity.js +18 -0
  7. package/dist/assignment.js +126 -0
  8. package/dist/avoidPairs.js +5 -0
  9. package/dist/contract.js +106 -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 +73 -0
  13. package/dist/directorContract.js +16 -0
  14. package/dist/directorWorkflow.js +6 -0
  15. package/dist/errors.d.ts +13 -0
  16. package/dist/errors.js +21 -0
  17. package/dist/index.d.ts +523 -0
  18. package/dist/index.js +12 -0
  19. package/dist/inspect.js +41 -0
  20. package/dist/packing.js +72 -0
  21. package/dist/parties.js +317 -0
  22. package/dist/party.entity.js +34 -0
  23. package/dist/partyComponents.js +177 -0
  24. package/dist/partyContract.js +25 -0
  25. package/dist/plugin.js +258 -0
  26. package/dist/port.d.ts +116 -0
  27. package/dist/port.js +5 -0
  28. package/dist/queue.d.ts +91 -0
  29. package/dist/queue.js +80 -0
  30. package/dist/queueDeclarations.js +28 -0
  31. package/dist/roomsAllocator.d.ts +67 -0
  32. package/dist/roomsAllocator.js +76 -0
  33. package/dist/rpc.d.ts +247 -0
  34. package/dist/rpc.js +202 -0
  35. package/dist/server.d.ts +337 -0
  36. package/dist/server.js +8 -0
  37. package/dist/service.js +370 -0
  38. package/dist/strategies.d.ts +97 -0
  39. package/dist/strategies.js +141 -0
  40. package/dist/testing.d.ts +296 -0
  41. package/dist/testing.js +152 -0
  42. package/dist/ticketContract.d.ts +52 -0
  43. package/dist/ticketContract.js +31 -0
  44. package/dist/tickets.entity.js +50 -0
  45. package/dist/web.d.ts +279 -0
  46. package/dist/web.js +8 -0
  47. package/package.json +104 -4
@@ -0,0 +1,523 @@
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 { VoltroPlugin } from '@voltro/protocol';
11
+ import { YieldableError } from 'effect/Cause';
12
+
13
+ export declare type AvoidPair = readonly [string, string];
14
+
15
+ /** Ordered policies consume disjoint proposals, sharing the original branch budget. */
16
+ export declare const composite: <A = Readonly<Record<string, unknown>>>(functions: ReadonlyArray<MatchFunction<A>>) => MatchFunction<A>;
17
+
18
+ /** Intentions only: the server derives leader, members, capacity and expiry. */
19
+ export declare interface CreatePartyIntent {
20
+ readonly requestId: string;
21
+ readonly maxSize?: number;
22
+ readonly approval?: 'open' | 'leader';
23
+ }
24
+
25
+ /** SQL-owned ephemeral parties: one party per subject, leader-governed, gone with its last member. */
26
+ export declare const dataStoreParties: (store: DataStore, offered?: PartyOptions) => {
27
+ service: PartyService;
28
+ exportSubject: (target: PluginPrivacyTarget) => Effect.Effect<unknown, MatchmakingError, never>;
29
+ eraseSubject: (target: PluginPrivacyTarget) => Effect.Effect<void, MatchmakingError, never>;
30
+ prepareSearch: (tx: DataStore, subject: Subject, partyId: string) => Promise<ReadonlyArray<string>>;
31
+ previewRoster: (subject: Subject, partyId: string) => Promise<ReadonlyArray<string>>;
32
+ readRoster: (tenantId: string, partyId: string) => Effect.Effect<PartyRoster | null, MatchmakingError>;
33
+ };
34
+
35
+ /** Empty policy for an app without a block provider. Automatic optional Social
36
+ * port binding is separate integration work; it is not inferred here. */
37
+ export declare const defaultMatchmakingAvoidPairs: MatchmakingAvoidPairs;
38
+
39
+ export declare const defineQueue: <S extends Schema.Codec<unknown, unknown>>(input: QueueDefinition<S>) => QueueDescriptor<S>;
40
+
41
+ /** Players send a queue preference intent; never skill, members, state or assignments. */
42
+ export declare interface EnqueueIntent {
43
+ readonly queue: string;
44
+ readonly requestId: string;
45
+ readonly preferences: Readonly<Record<string, unknown>>;
46
+ }
47
+
48
+ export declare const fifo: <A = Readonly<Record<string, unknown>>>() => MatchFunction<A>;
49
+
50
+ export declare type MatchFunction<A = Readonly<Record<string, unknown>>> = (input: MatchInput<A>) => MatchOutput<A>;
51
+
52
+ export declare interface MatchInput<A = Readonly<Record<string, unknown>>> {
53
+ readonly tickets: ReadonlyArray<MatchTicket<A>>;
54
+ readonly now: number;
55
+ readonly queue: MatchQueueRules;
56
+ readonly avoid: ReadonlyArray<AvoidPair>;
57
+ }
58
+
59
+ export declare interface MatchmakingAllocation {
60
+ readonly roomId: string;
61
+ /** Every member's seat session id; tokens stay with Rooms. */
62
+ readonly seats: Readonly<Record<string, string>>;
63
+ }
64
+
65
+ /** What the allocator must create for one accepted proposal: a Room with a seat per member. */
66
+ export declare interface MatchmakingAllocationRequest {
67
+ readonly tenantId: string;
68
+ readonly queue: string;
69
+ /** Stable per proposal: the same tickets always yield the same key, so allocation can be idempotent. */
70
+ readonly requestId: string;
71
+ /** Member subject ids per team, in proposal order. */
72
+ readonly teams: ReadonlyArray<ReadonlyArray<string>>;
73
+ }
74
+
75
+ export declare type MatchmakingAssignment = typeof MatchmakingAssignmentSchema.Type;
76
+
77
+ declare const MatchmakingAssignmentSchema: Schema.Struct<{
78
+ readonly ticketId: Schema.String;
79
+ readonly queue: Schema.String;
80
+ readonly roomId: Schema.String;
81
+ readonly sessionId: Schema.String;
82
+ readonly createdAt: Schema.Number;
83
+ }>;
84
+
85
+ export declare interface MatchmakingAssignResult {
86
+ readonly assigned: ReadonlyArray<{
87
+ readonly requestId: string;
88
+ readonly roomId: string;
89
+ readonly tickets: ReadonlyArray<string>;
90
+ }>;
91
+ /** Proposals whose tickets changed before the claim (cancelled, expired, taken by another Director). */
92
+ readonly skipped: number;
93
+ /** Proposals whose allocation failed; their tickets are open again (or expired at the limit). */
94
+ readonly failed: number;
95
+ }
96
+
97
+ export declare interface MatchmakingAvoidContext {
98
+ readonly tenantId: string;
99
+ readonly queue: string;
100
+ readonly tickets: ReadonlyArray<MatchTicket>;
101
+ readonly maxPairs: number;
102
+ readonly query: DataStore['query'];
103
+ }
104
+
105
+ export declare type MatchmakingAvoidPairs = (context: MatchmakingAvoidContext) => Effect.Effect<ReadonlyArray<AvoidPair>, MatchmakingError>;
106
+
107
+ /** A scheduled Director run: `runKey` makes the run idempotent; `maxPairs` bounds tenant×queue work per run. */
108
+ export declare const MatchmakingDirectorPayload: Schema.Struct<{
109
+ readonly runKey: Schema.NonEmptyString;
110
+ readonly maxPairs: Schema.Number;
111
+ }>;
112
+
113
+ export declare type MatchmakingDirectorPayload = typeof MatchmakingDirectorPayload.Type;
114
+
115
+ export declare class MatchmakingDirectorPort extends MatchmakingDirectorPort_base {
116
+ }
117
+
118
+ declare const MatchmakingDirectorPort_base: Context.ServiceClass<MatchmakingDirectorPort, "@voltro/MatchmakingDirectorPort", MatchmakingDirectorService>;
119
+
120
+ export declare const MatchmakingDirectorResult: Schema.Struct<{
121
+ readonly cycles: Schema.Number;
122
+ readonly assigned: Schema.Number;
123
+ readonly skipped: Schema.Number;
124
+ readonly failed: Schema.Number;
125
+ }>;
126
+
127
+ export declare type MatchmakingDirectorResult = typeof MatchmakingDirectorResult.Type;
128
+
129
+ /** One Director pass over every tenant and queue with open tickets: plan, then assign. */
130
+ export declare interface MatchmakingDirectorService {
131
+ readonly run: (input: MatchmakingDirectorPayload) => Effect.Effect<MatchmakingDirectorResult, MatchmakingError>;
132
+ }
133
+
134
+ /** The `matchmaking.director` workflow body: one Director run through the installed port. */
135
+ export declare const matchmakingDirectorWorkflowExecutor: (payload: MatchmakingDirectorPayload) => Effect.Effect<MatchmakingDirectorResult, MatchmakingError, MatchmakingDirectorPort>;
136
+
137
+ export declare class MatchmakingError extends MatchmakingError_base {
138
+ }
139
+
140
+ declare const MatchmakingError_base: Schema.Class<MatchmakingError, Schema.TaggedStruct<"MatchmakingError", {
141
+ 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"]>;
142
+ }>, YieldableError>;
143
+
144
+ declare type MatchmakingEstimate = typeof MatchmakingEstimateSchema.Type;
145
+
146
+ /** Expected wait in a queue: the median time recent searches of this tenant waited until matched, from up to
147
+ * `samples` searches matched in the last 30 minutes. `waitMs` is null with fewer than three such searches. */
148
+ declare const MatchmakingEstimateSchema: Schema.Struct<{
149
+ readonly queue: Schema.String;
150
+ readonly waitMs: Schema.NullOr<Schema.Number>;
151
+ readonly samples: Schema.Number;
152
+ }>;
153
+
154
+ /** Ephemeral groups: a party disappears with its last member. `admission` is the party writer fence. */
155
+ export declare const matchmakingPartiesTable: Table<"_voltro_mm_parties", FieldDefinitions<{
156
+ readonly id: ColumnBuilder<string, "id", boolean>;
157
+ readonly tenantId: ColumnBuilder<string, "text", boolean>;
158
+ readonly leaderId: ColumnBuilder<string, "text", boolean>;
159
+ readonly requestKey: ColumnBuilder<string, "text", boolean>;
160
+ readonly maxSize: ColumnBuilder<number, "integer", boolean>;
161
+ readonly approval: ColumnBuilder<"open" | "leader", "text", boolean>;
162
+ readonly admission: ColumnBuilder<number, "integer", boolean>;
163
+ readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
164
+ }>, true, never>;
165
+
166
+ export declare const matchmakingPartyInvitesTable: Table<"_voltro_mm_party_invites", FieldDefinitions<{
167
+ readonly id: ColumnBuilder<string, "id", boolean>;
168
+ readonly tenantId: ColumnBuilder<string, "text", boolean>;
169
+ readonly partyId: ColumnBuilder<string, "text", boolean>;
170
+ readonly inviteeId: ColumnBuilder<string, "text", boolean>;
171
+ readonly inviterId: ColumnBuilder<string, "text", boolean>;
172
+ readonly expiresAt: ColumnBuilder<Date, "timestamp", boolean>;
173
+ }>, true, never>;
174
+
175
+ /** One row per (tenant, subject): the ID omits the party, so a subject is in at most one party. */
176
+ export declare const matchmakingPartyMembersTable: Table<"_voltro_mm_party_members", FieldDefinitions<{
177
+ readonly id: ColumnBuilder<string, "id", boolean>;
178
+ readonly tenantId: ColumnBuilder<string, "text", boolean>;
179
+ readonly partyId: ColumnBuilder<string, "text", boolean>;
180
+ readonly subjectId: ColumnBuilder<string, "text", boolean>;
181
+ readonly role: ColumnBuilder<"member" | "leader", "text", boolean>;
182
+ readonly ready: ColumnBuilder<boolean, "boolean", boolean>;
183
+ readonly joinedAt: ColumnBuilder<Date, "timestamp", boolean>;
184
+ }>, true, never>;
185
+
186
+ export declare interface MatchmakingPlan extends MatchOutput {
187
+ readonly tenantId: string;
188
+ readonly queue: string;
189
+ readonly observedAt: number;
190
+ }
191
+
192
+ export declare interface MatchmakingPlanInput {
193
+ readonly tenantId: string;
194
+ readonly queue: string;
195
+ }
196
+
197
+ /** Ticket installation; the Director cycle (plan, then assign) is called by trusted server code. */
198
+ export declare interface MatchmakingPlugin extends VoltroPlugin {
199
+ /** Trusted Director facade of this installed adapter, never a player RPC. */
200
+ readonly matchmaking: {
201
+ readonly plan: (subject: Subject, input: MatchmakingPlanInput) => Effect.Effect<MatchmakingPlan, MatchmakingError>;
202
+ /** Claim, allocate and record the plan's proposals. Requires `allocator`. */
203
+ readonly assign: (subject: Subject, plan: MatchmakingPlan) => Effect.Effect<MatchmakingAssignResult, MatchmakingError>;
204
+ /** One Director cycle for a queue: `plan` then `assign`. */
205
+ readonly cycle: (subject: Subject, input: MatchmakingPlanInput) => Effect.Effect<MatchmakingAssignResult, MatchmakingError>;
206
+ };
207
+ }
208
+
209
+ export declare const matchmakingPlugin: (input: MatchmakingPluginOptions) => MatchmakingPlugin;
210
+
211
+ export declare interface MatchmakingPluginOptions extends TicketAdmissionOptions {
212
+ /** Party sizes and invite lifetime; parties share the ticket clock unless `now` is given. */
213
+ readonly parties?: PartyOptions;
214
+ /** Room seats for accepted matches (`roomsAllocator` from `@voltro/plugin-matchmaking/rooms`). Omitted: the Director only plans. */
215
+ readonly allocator?: MatchmakingRoomAllocator;
216
+ /** Failed allocations before a ticket expires. Default 3. */
217
+ readonly maxAllocationAttempts?: number;
218
+ }
219
+
220
+ export declare class MatchmakingPort extends MatchmakingPort_base {
221
+ }
222
+
223
+ declare const MatchmakingPort_base: Context.ServiceClass<MatchmakingPort, "@voltro/MatchmakingPort", MatchmakingService>;
224
+
225
+ /** Room seats for a match. `roomsAllocator` (`@voltro/plugin-matchmaking/rooms`) is the Rooms implementation. */
226
+ export declare interface MatchmakingRoomAllocator {
227
+ readonly allocate: (request: MatchmakingAllocationRequest) => Effect.Effect<MatchmakingAllocation, MatchmakingError>;
228
+ /** Undo a completed allocation the assignment could not record. */
229
+ readonly release: (request: MatchmakingAllocationRequest, allocation: MatchmakingAllocation) => Effect.Effect<void, MatchmakingError>;
230
+ }
231
+
232
+ /** Queries wrap status reactively; this port owns no transport or poll timer. */
233
+ export declare interface MatchmakingService {
234
+ readonly enqueue: (subject: Subject, input: EnqueueIntent) => Effect.Effect<MatchmakingTicket, MatchmakingError>;
235
+ readonly cancel: (subject: Subject, ticketId: string) => Effect.Effect<void, MatchmakingError>;
236
+ readonly status: (subject: Subject, ticketId: string) => Effect.Effect<MatchmakingTicket, MatchmakingError>;
237
+ /** Median recent wait in a declared queue for the caller's tenant. Optional for replacement ports: without it
238
+ * `matchmaking.estimate` answers `unavailable`. */
239
+ readonly estimate?: (subject: Subject, queue: string) => Effect.Effect<MatchmakingEstimate, MatchmakingError>;
240
+ }
241
+
242
+ /** System21 inventory: replaceable rules and ports with their fakes. */
243
+ export declare const matchmakingSystemContract: {
244
+ readonly slots: {
245
+ readonly avoidPairs: {
246
+ readonly defaultExport: "defaultMatchmakingAvoidPairs";
247
+ readonly fixtureExport: "matchmakingAvoidPairsSlotFixture";
248
+ };
249
+ };
250
+ readonly ports: {
251
+ readonly MatchmakingPort: {
252
+ readonly tagExport: "MatchmakingPort";
253
+ readonly fakeLayerExport: "matchmakingFakeLayer";
254
+ readonly fixtureExport: "matchmakingPortFixture";
255
+ };
256
+ readonly PartyMembershipPort: {
257
+ readonly tagExport: "PartyMembershipPort";
258
+ readonly fakeLayerExport: "partyMembershipFakeLayer";
259
+ readonly fixtureExport: "partyMembershipPortFixture";
260
+ };
261
+ readonly PartyRosterPort: {
262
+ readonly tagExport: "PartyRosterPort";
263
+ readonly fakeLayerExport: "partyRosterFakeLayer";
264
+ readonly fixtureExport: "partyRosterPortFixture";
265
+ };
266
+ readonly MatchmakingDirectorPort: {
267
+ readonly tagExport: "MatchmakingDirectorPort";
268
+ readonly fakeLayerExport: "matchmakingDirectorFakeLayer";
269
+ readonly fixtureExport: "matchmakingDirectorPortFixture";
270
+ };
271
+ };
272
+ readonly events: readonly [];
273
+ };
274
+
275
+ export declare type MatchmakingTicket = typeof MatchmakingTicketSchema.Type;
276
+
277
+ declare const MatchmakingTicketSchema: Schema.Struct<{
278
+ readonly id: Schema.String;
279
+ readonly queue: Schema.String;
280
+ readonly state: Schema.Literals<readonly ["open", "matched", "assigned", "cancelled", "expired"]>;
281
+ readonly members: Schema.$Array<Schema.String>;
282
+ readonly attributes: Schema.$Record<Schema.String, Schema.Unknown>;
283
+ readonly createdAt: Schema.Number;
284
+ readonly expiresAt: Schema.Number;
285
+ readonly closedAt: Schema.NullOr<Schema.Number>;
286
+ readonly metadata: Schema.$Record<Schema.String, Schema.Unknown>;
287
+ }>;
288
+
289
+ export declare interface MatchOutput<A = Readonly<Record<string, unknown>>> {
290
+ readonly proposals: ReadonlyArray<MatchProposal<A>>;
291
+ readonly leftover: ReadonlyArray<MatchTicket<A>>;
292
+ /** Deterministic branch inspections, not a runtime or throughput measurement. */
293
+ readonly searchNodes: number;
294
+ readonly exhausted: boolean;
295
+ }
296
+
297
+ export declare interface MatchProposal<A = Readonly<Record<string, unknown>>> {
298
+ readonly teams: ReadonlyArray<ReadonlyArray<MatchTicket<A>>>;
299
+ readonly quality: number;
300
+ }
301
+
302
+ export declare interface MatchQueueRules {
303
+ readonly name: string;
304
+ readonly teams: {
305
+ readonly count: number;
306
+ readonly size: number;
307
+ };
308
+ readonly maxTickets: number;
309
+ readonly maxSearchNodes: number;
310
+ readonly maxAvoidPairs: number;
311
+ readonly maxAttributeBytes: number;
312
+ readonly minQuality: {
313
+ readonly initial: number;
314
+ readonly after: ReadonlyArray<readonly [number, number]>;
315
+ };
316
+ }
317
+
318
+ export declare interface MatchTicket<A = Readonly<Record<string, unknown>>> {
319
+ readonly id: string;
320
+ readonly tenantId: string;
321
+ readonly queue: string;
322
+ readonly members: ReadonlyArray<string>;
323
+ readonly createdAt: number;
324
+ readonly attributes: A;
325
+ }
326
+
327
+ /** Exact whole-party packing with an explicit search budget. No IO or recursion. */
328
+ export declare const packTeams: <A>(input: MatchInput<A>, accept?: TeamAdmission<A>) => PackTeamsResult<A>;
329
+
330
+ export declare interface PackTeamsResult<A> {
331
+ readonly teams: ReadonlyArray<ReadonlyArray<MatchTicket<A>>> | null;
332
+ readonly searchNodes: number;
333
+ readonly exhausted: boolean;
334
+ }
335
+
336
+ export declare type Party = typeof PartySchema.Type;
337
+
338
+ export declare type PartyInvite = typeof PartyInviteSchema.Type;
339
+
340
+ declare const PartyInviteSchema: Schema.Struct<{
341
+ readonly partyId: Schema.String;
342
+ readonly inviterId: Schema.String;
343
+ readonly expiresAt: Schema.Number;
344
+ }>;
345
+
346
+ export declare class PartyMembershipPort extends PartyMembershipPort_base {
347
+ }
348
+
349
+ declare const PartyMembershipPort_base: Context.ServiceClass<PartyMembershipPort, "@voltro/PartyMembershipPort", PartyMembershipService>;
350
+
351
+ /** Chat and other plugins ask current party membership here, never party SQL. */
352
+ export declare interface PartyMembershipService {
353
+ readonly isMember: (subject: Subject, partyId: string) => Effect.Effect<boolean, MatchmakingError>;
354
+ }
355
+
356
+ export declare interface PartyOptions {
357
+ /** Default and upper size for new parties (1..256). */
358
+ readonly maxPartySize?: number;
359
+ /** Invite lifetime in milliseconds (default 10 minutes, max 1 day). */
360
+ readonly partyInviteTtlMs?: number;
361
+ readonly now?: () => number;
362
+ }
363
+
364
+ /** A detached current SQL roster. It contains IDs, never inferred Subject kinds. */
365
+ export declare interface PartyRoster {
366
+ readonly tenantId: string;
367
+ readonly partyId: string;
368
+ readonly leaderId: string;
369
+ readonly memberIds: ReadonlyArray<string>;
370
+ }
371
+
372
+ export declare class PartyRosterPort extends PartyRosterPort_base {
373
+ }
374
+
375
+ declare const PartyRosterPort_base: Context.ServiceClass<PartyRosterPort, "@voltro/PartyRosterPort", PartyRosterService>;
376
+
377
+ /** Trusted server composition only. The caller must authorize the operation;
378
+ * this read is not a player route or authority to move the returned members. */
379
+ export declare interface PartyRosterService {
380
+ readonly read: (tenantId: string, partyId: string) => Effect.Effect<PartyRoster | null, MatchmakingError>;
381
+ }
382
+
383
+ declare const PartySchema: Schema.Struct<{
384
+ readonly id: Schema.String;
385
+ readonly leaderId: Schema.String;
386
+ readonly maxSize: Schema.Number;
387
+ readonly approval: Schema.Literals<readonly ["open", "leader"]>;
388
+ readonly createdAt: Schema.Number;
389
+ readonly members: Schema.$Array<Schema.Struct<{
390
+ readonly subjectId: Schema.String;
391
+ readonly role: Schema.Literals<readonly ["leader", "member"]>;
392
+ readonly ready: Schema.Boolean;
393
+ readonly joinedAt: Schema.Number;
394
+ }>>;
395
+ /** The open party search ticket, readable by every member through `matchmaking.status`. */
396
+ readonly searchTicketId: Schema.NullOr<Schema.String>;
397
+ }>;
398
+
399
+ export declare interface PartyService {
400
+ readonly create: (subject: Subject, intent: CreatePartyIntent) => Effect.Effect<Party, MatchmakingError>;
401
+ readonly mine: (subject: Subject) => Effect.Effect<Party | null, MatchmakingError>;
402
+ readonly invites: (subject: Subject) => Effect.Effect<ReadonlyArray<PartyInvite>, MatchmakingError>;
403
+ readonly invite: (subject: Subject, intent: PartyTargetIntent) => Effect.Effect<PartyInvite, MatchmakingError>;
404
+ readonly join: (subject: Subject, partyId: string) => Effect.Effect<Party, MatchmakingError>;
405
+ readonly leave: (subject: Subject, partyId: string) => Effect.Effect<void, MatchmakingError>;
406
+ readonly kick: (subject: Subject, intent: PartyTargetIntent) => Effect.Effect<Party, MatchmakingError>;
407
+ readonly setReady: (subject: Subject, input: {
408
+ readonly partyId: string;
409
+ readonly ready: boolean;
410
+ }) => Effect.Effect<Party, MatchmakingError>;
411
+ readonly transferLeader: (subject: Subject, intent: PartyTargetIntent) => Effect.Effect<Party, MatchmakingError>;
412
+ /** Trusted port read: true only while `subject` currently belongs to `partyId` in its own tenant. */
413
+ readonly isMember: (subject: Subject, partyId: string) => Effect.Effect<boolean, MatchmakingError>;
414
+ /** Any member stops the party's open search; every member claim is released with it. */
415
+ readonly cancelSearch: (subject: Subject, partyId: string) => Effect.Effect<Party, MatchmakingError>;
416
+ }
417
+
418
+ export declare const partyTables: readonly [ Table<"_voltro_mm_parties", FieldDefinitions<{
419
+ readonly id: ColumnBuilder<string, "id", boolean>;
420
+ readonly tenantId: ColumnBuilder<string, "text", boolean>;
421
+ readonly leaderId: ColumnBuilder<string, "text", boolean>;
422
+ readonly requestKey: ColumnBuilder<string, "text", boolean>;
423
+ readonly maxSize: ColumnBuilder<number, "integer", boolean>;
424
+ readonly approval: ColumnBuilder<"open" | "leader", "text", boolean>;
425
+ readonly admission: ColumnBuilder<number, "integer", boolean>;
426
+ readonly createdAt: ColumnBuilder<Date, "timestamp", boolean>;
427
+ }>, true, never>, Table<"_voltro_mm_party_members", FieldDefinitions<{
428
+ readonly id: ColumnBuilder<string, "id", boolean>;
429
+ readonly tenantId: ColumnBuilder<string, "text", boolean>;
430
+ readonly partyId: ColumnBuilder<string, "text", boolean>;
431
+ readonly subjectId: ColumnBuilder<string, "text", boolean>;
432
+ readonly role: ColumnBuilder<"member" | "leader", "text", boolean>;
433
+ readonly ready: ColumnBuilder<boolean, "boolean", boolean>;
434
+ readonly joinedAt: ColumnBuilder<Date, "timestamp", boolean>;
435
+ }>, true, never>, Table<"_voltro_mm_party_invites", FieldDefinitions<{
436
+ readonly id: ColumnBuilder<string, "id", boolean>;
437
+ readonly tenantId: ColumnBuilder<string, "text", boolean>;
438
+ readonly partyId: ColumnBuilder<string, "text", boolean>;
439
+ readonly inviteeId: ColumnBuilder<string, "text", boolean>;
440
+ readonly inviterId: ColumnBuilder<string, "text", boolean>;
441
+ readonly expiresAt: ColumnBuilder<Date, "timestamp", boolean>;
442
+ }>, true, never>];
443
+
444
+ export declare interface PartyTargetIntent {
445
+ readonly partyId: string;
446
+ readonly subjectId: string;
447
+ }
448
+
449
+ export declare interface QueueDefinition<S extends Schema.Codec<unknown, unknown>> {
450
+ readonly name: string;
451
+ readonly room: string;
452
+ readonly ticket: S;
453
+ readonly teams: MatchQueueRules['teams'];
454
+ readonly matchFunction: MatchFunction<(S)["Type"]>;
455
+ readonly pools?: {
456
+ readonly byRegion: (attributes: (S)["Type"]) => string;
457
+ } | undefined;
458
+ readonly minQuality?: MatchQueueRules['minQuality'];
459
+ readonly maxTickets?: number;
460
+ readonly maxSearchNodes?: number;
461
+ readonly maxAvoidPairs?: number;
462
+ readonly maxAttributeBytes?: number;
463
+ readonly cycleMs?: number;
464
+ readonly ticketTtlMs?: number;
465
+ }
466
+
467
+ export declare interface QueueDescriptor<S extends Schema.Codec<unknown, unknown>> extends MatchQueueRules {
468
+ readonly kind: 'matchmaking.queue';
469
+ readonly room: string;
470
+ readonly ticket: S;
471
+ readonly matchFunction: MatchFunction<(S)["Type"]>;
472
+ readonly pools: {
473
+ readonly byRegion: (attributes: (S)["Type"]) => string;
474
+ } | undefined;
475
+ readonly cycleMs: number;
476
+ readonly ticketTtlMs: number;
477
+ }
478
+
479
+ export declare const regionLatency: (options: {
480
+ readonly latencyMaxMs: number;
481
+ }) => <A extends Readonly<Record<string, unknown>>>(input: MatchInput<A>) => MatchOutput<A>;
482
+
483
+ /** Capture and bound party options before any store is bound. */
484
+ export declare const resolvePartyOptions: (offered?: PartyOptions) => Readonly<{
485
+ maxPartySize: number;
486
+ partyInviteTtlMs: number;
487
+ now: () => number;
488
+ }>;
489
+
490
+ /** Pure schema-bound partition and slot admission. It does not acquire SQL tickets or reserve Rooms. */
491
+ export declare const runQueueMatch: <S extends Schema.Codec<unknown, unknown>>(queue: QueueDescriptor<S>, input: {
492
+ readonly tickets: ReadonlyArray<MatchTicket<S["Type"]>>;
493
+ readonly now: number;
494
+ readonly avoid: ReadonlyArray<AvoidPair>;
495
+ }) => MatchOutput<S["Type"]>;
496
+
497
+ export declare const skillWindow: (options: SkillWindowOptions) => <A extends Readonly<Record<string, unknown>>>(input: MatchInput<A>) => MatchOutput<A>;
498
+
499
+ export declare interface SkillWindowOptions {
500
+ readonly initialWindow: number;
501
+ readonly growPerSecond: number;
502
+ readonly maxWindow: number;
503
+ readonly latencyMaxMs?: number;
504
+ }
505
+
506
+ export declare type TeamAdmission<A> = (teams: ReadonlyArray<ReadonlyArray<MatchTicket<A>>>) => boolean;
507
+
508
+ export declare interface TicketAdmissionOptions {
509
+ readonly queues: ReadonlyArray<QueueDescriptor<any>>;
510
+ /** Resolve trusted skill/latency/policy attributes before SQL; no client state is copied. */
511
+ readonly prepareTicket: (subject: Subject, queue: QueueDescriptor<any>, preferences: EnqueueIntent['preferences']) => Effect.Effect<Readonly<Record<string, unknown>>, MatchmakingError>;
512
+ /** Re-check durable eligibility in the exact borrowed transaction after writer admission. */
513
+ readonly admit: (subject: Subject, queue: QueueDescriptor<any>, tx: DataStore) => Effect.Effect<void, MatchmakingError>;
514
+ readonly maxPreferencesBytes?: number;
515
+ readonly maxExportRows?: number;
516
+ readonly avoidPairs?: MatchmakingAvoidPairs;
517
+ readonly now?: () => number;
518
+ }
519
+
520
+ /** Reject custom slot fabrication, overlapping assignments and omitted leftovers. */
521
+ export declare const validateMatchOutput: <A>(raw: MatchInput<A>, output: MatchOutput<A>) => MatchOutput<A>;
522
+
523
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,12 @@
1
+ import { MatchmakingError as e } from "./errors.js";
2
+ import { packTeams as t } from "./packing.js";
3
+ import { defaultMatchmakingAvoidPairs as n } from "./avoidPairs.js";
4
+ import { composite as r, fifo as i, regionLatency as a, skillWindow as o, validateMatchOutput as s } from "./strategies.js";
5
+ import { defineQueue as c, runQueueMatch as l } from "./queue.js";
6
+ import { MatchmakingDirectorPort as u, MatchmakingPort as d, PartyMembershipPort as f, PartyRosterPort as p } from "./port.js";
7
+ import { matchmakingPartiesTable as m, matchmakingPartyInvitesTable as h, matchmakingPartyMembersTable as g, partyTables as _ } from "./party.entity.js";
8
+ import { dataStoreParties as v, resolvePartyOptions as y } from "./parties.js";
9
+ import { MatchmakingDirectorPayload as b, MatchmakingDirectorResult as x } from "./directorContract.js";
10
+ import { matchmakingPlugin as S, matchmakingSystemContract as C } from "./plugin.js";
11
+ import { matchmakingDirectorWorkflowExecutor as w } from "./directorWorkflow.js";
12
+ export { b as MatchmakingDirectorPayload, u as MatchmakingDirectorPort, x as MatchmakingDirectorResult, e as MatchmakingError, d as MatchmakingPort, f as PartyMembershipPort, p as PartyRosterPort, r as composite, v as dataStoreParties, n as defaultMatchmakingAvoidPairs, c as defineQueue, i as fifo, w as matchmakingDirectorWorkflowExecutor, m as matchmakingPartiesTable, h as matchmakingPartyInvitesTable, g as matchmakingPartyMembersTable, S as matchmakingPlugin, C as matchmakingSystemContract, t as packTeams, _ as partyTables, a as regionLatency, y as resolvePartyOptions, l as runQueueMatch, o as skillWindow, s as validateMatchOutput };
@@ -0,0 +1,41 @@
1
+ import { matchmakingError as e } from "./errors.js";
2
+ import { defaultMatchmakingAvoidPairs as t } from "./avoidPairs.js";
3
+ //#region src/inspect.ts
4
+ var n = (n, r, i = Date.now()) => {
5
+ if (!Number.isSafeInteger(i) || i < 0 || i > 864e13) throw e("unavailable");
6
+ return {
7
+ observedAt: i,
8
+ bound: r,
9
+ directorInstalled: !1,
10
+ maxPreferencesBytes: n.maxPreferencesBytes ?? 4096,
11
+ maxExportRows: n.maxExportRows ?? 1e4,
12
+ slots: {
13
+ prepareTicket: "custom",
14
+ admit: "custom",
15
+ avoidPairs: n.avoidPairs === void 0 || n.avoidPairs === t ? "default" : "custom"
16
+ },
17
+ queues: n.queues.map((e) => ({
18
+ name: e.name,
19
+ room: e.room,
20
+ teams: e.teams,
21
+ maxTickets: e.maxTickets,
22
+ maxSearchNodes: e.maxSearchNodes,
23
+ maxAvoidPairs: e.maxAvoidPairs,
24
+ maxAttributeBytes: e.maxAttributeBytes,
25
+ cycleMs: e.cycleMs,
26
+ ticketTtlMs: e.ticketTtlMs,
27
+ matchFunction: "custom",
28
+ partitioned: e.pools !== void 0
29
+ })),
30
+ tables: [
31
+ "_voltro_mm_tickets",
32
+ "_voltro_mm_subject_admission",
33
+ "_voltro_mm_queue_admission",
34
+ "_voltro_mm_parties",
35
+ "_voltro_mm_party_members",
36
+ "_voltro_mm_party_invites"
37
+ ]
38
+ };
39
+ };
40
+ //#endregion
41
+ export { n as matchmakingStatus };
@@ -0,0 +1,72 @@
1
+ import { matchmakingError as e } from "./errors.js";
2
+ import { captureMatchInput as t } from "./contract.js";
3
+ //#region src/packing.ts
4
+ var n = (t, n) => {
5
+ let { count: r, size: i } = t.queue.teams, a = r * i, o = t.tickets, s = /* @__PURE__ */ new Map();
6
+ for (let [e, n] of t.avoid) s.has(e) || s.set(e, /* @__PURE__ */ new Set()), s.has(n) || s.set(n, /* @__PURE__ */ new Set()), s.get(e).add(n), s.get(n).add(e);
7
+ let c = Array(o.length + 1).fill(0);
8
+ for (let e = o.length - 1; e >= 0; e--) c[e] = c[e + 1] + o[e].members.length;
9
+ let l = Array.from({ length: r }, () => []), u = Array(r).fill(0), d = /* @__PURE__ */ new Set(), f = [{
10
+ index: 0,
11
+ next: 0,
12
+ added: null
13
+ }], p = 0, m = 0, h = () => {
14
+ let e = f.pop();
15
+ if (e.added !== null) {
16
+ let { team: t, ticket: n } = e.added;
17
+ l[t].pop(), u[t] = u[t] - n.members.length, p -= n.members.length, n.members.forEach((e) => d.delete(e));
18
+ }
19
+ };
20
+ for (; f.length > 0;) {
21
+ let g = f[f.length - 1];
22
+ if (p === a) {
23
+ let t = Object.freeze(l.map((e) => Object.freeze([...e]))), r = n === void 0 || n(t);
24
+ if (typeof r != "boolean") throw e("invalid_proposal");
25
+ if (r) return Object.freeze({
26
+ teams: t,
27
+ searchNodes: m,
28
+ exhausted: !1
29
+ });
30
+ h();
31
+ continue;
32
+ }
33
+ if (g.index >= o.length || c[g.index] < a - p || g.next > r) {
34
+ h();
35
+ continue;
36
+ }
37
+ if (m === t.queue.maxSearchNodes) return Object.freeze({
38
+ teams: null,
39
+ searchNodes: m,
40
+ exhausted: !0
41
+ });
42
+ let _ = g.next++, v = o[g.index];
43
+ if (m++, _ === r) {
44
+ f.push({
45
+ index: g.index + 1,
46
+ next: 0,
47
+ added: null
48
+ });
49
+ continue;
50
+ }
51
+ if (u[_] + v.members.length > i) continue;
52
+ let y = new Set(v.members);
53
+ v.members.some((e) => [...s.get(e) ?? []].some((e) => d.has(e) || y.has(e))) || (l[_].push(v), u[_] = u[_] + v.members.length, p += v.members.length, v.members.forEach((e) => d.add(e)), f.push({
54
+ index: g.index + 1,
55
+ next: 0,
56
+ added: {
57
+ team: _,
58
+ ticket: v
59
+ }
60
+ }));
61
+ }
62
+ return Object.freeze({
63
+ teams: null,
64
+ searchNodes: m,
65
+ exhausted: !1
66
+ });
67
+ }, r = (r, i) => {
68
+ if (i !== void 0 && typeof i != "function") throw e("invalid_input");
69
+ return n(t(r), i);
70
+ };
71
+ //#endregion
72
+ export { n as packCapturedTeams, r as packTeams };