@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.
- package/CHANGELOG.md +24329 -0
- package/LICENSE +61 -0
- package/README.md +272 -2
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +7737 -0
- package/dist/admission.entity.js +18 -0
- package/dist/assignment.js +126 -0
- package/dist/avoidPairs.js +5 -0
- package/dist/contract.js +106 -0
- package/dist/director-app/matchmaking.director.workflow.server.tsx +5 -0
- package/dist/director-app/matchmaking.director.workflow.tsx +13 -0
- package/dist/director.js +73 -0
- package/dist/directorContract.js +16 -0
- package/dist/directorWorkflow.js +6 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +21 -0
- package/dist/index.d.ts +523 -0
- package/dist/index.js +12 -0
- package/dist/inspect.js +41 -0
- package/dist/packing.js +72 -0
- package/dist/parties.js +317 -0
- package/dist/party.entity.js +34 -0
- package/dist/partyComponents.js +177 -0
- package/dist/partyContract.js +25 -0
- package/dist/plugin.js +258 -0
- package/dist/port.d.ts +116 -0
- package/dist/port.js +5 -0
- package/dist/queue.d.ts +91 -0
- package/dist/queue.js +80 -0
- package/dist/queueDeclarations.js +28 -0
- package/dist/roomsAllocator.d.ts +67 -0
- package/dist/roomsAllocator.js +76 -0
- package/dist/rpc.d.ts +247 -0
- package/dist/rpc.js +202 -0
- package/dist/server.d.ts +337 -0
- package/dist/server.js +8 -0
- package/dist/service.js +370 -0
- package/dist/strategies.d.ts +97 -0
- package/dist/strategies.js +141 -0
- package/dist/testing.d.ts +296 -0
- package/dist/testing.js +152 -0
- package/dist/ticketContract.d.ts +52 -0
- package/dist/ticketContract.js +31 -0
- package/dist/tickets.entity.js +50 -0
- package/dist/web.d.ts +279 -0
- package/dist/web.js +8 -0
- package/package.json +104 -4
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import { Context } from 'effect';
|
|
2
|
+
import { DataStore } from '@voltro/database';
|
|
3
|
+
import { Effect } from 'effect';
|
|
4
|
+
import { Layer } from 'effect';
|
|
5
|
+
import { Schema } from 'effect';
|
|
6
|
+
import { Subject } from '@voltro/protocol';
|
|
7
|
+
import { YieldableError } from 'effect/Cause';
|
|
8
|
+
|
|
9
|
+
declare type AvoidPair = readonly [string, string];
|
|
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
|
+
declare const defaultMatchmakingAvoidPairs: MatchmakingAvoidPairs;
|
|
14
|
+
|
|
15
|
+
/** Players send a queue preference intent; never skill, members, state or assignments. */
|
|
16
|
+
declare interface EnqueueIntent {
|
|
17
|
+
readonly queue: string;
|
|
18
|
+
readonly requestId: string;
|
|
19
|
+
readonly preferences: Readonly<Record<string, unknown>>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A Director that reports a fixed pass; for code that schedules or observes Director runs. */
|
|
23
|
+
export declare const fakeMatchmakingDirectorLayer: (result?: {
|
|
24
|
+
readonly cycles: number;
|
|
25
|
+
readonly assigned: number;
|
|
26
|
+
readonly skipped: number;
|
|
27
|
+
readonly failed: number;
|
|
28
|
+
}) => Layer.Layer<MatchmakingDirectorPort, never, never>;
|
|
29
|
+
|
|
30
|
+
export declare const fakeMatchmakingLayer: (overrides?: Partial<MatchmakingService>) => Layer.Layer<MatchmakingPort, never, never>;
|
|
31
|
+
|
|
32
|
+
export declare const fakeMatchmakingService: (overrides?: Partial<MatchmakingService>) => MatchmakingService;
|
|
33
|
+
|
|
34
|
+
/** Fake party roster: `members` maps party IDs to subject IDs. */
|
|
35
|
+
export declare const fakePartyMembershipLayer: (members?: Readonly<Record<string, ReadonlyArray<string>>>) => Layer.Layer<PartyMembershipPort, never, never>;
|
|
36
|
+
|
|
37
|
+
/** Supply a complete server roster implementation; absent fixtures fail closed. */
|
|
38
|
+
export declare const fakePartyRosterLayer: (read?: PartyRosterService["read"]) => Layer.Layer<PartyRosterPort, never, never>;
|
|
39
|
+
|
|
40
|
+
declare type MatchFunction<A = Readonly<Record<string, unknown>>> = (input: MatchInput<A>) => MatchOutput<A>;
|
|
41
|
+
|
|
42
|
+
/** Real pure implementations and source probes; this is not whole-system conformance. */
|
|
43
|
+
export declare const matchFunctionFixture: {
|
|
44
|
+
fifo: <A = Readonly<Record<string, unknown>>>() => MatchFunction<A>;
|
|
45
|
+
skillWindow: (options: SkillWindowOptions) => <A extends Readonly<Record<string, unknown>>>(input: MatchInput<A>) => MatchOutput<A>;
|
|
46
|
+
regionLatency: (options: {
|
|
47
|
+
readonly latencyMaxMs: number;
|
|
48
|
+
}) => <A extends Readonly<Record<string, unknown>>>(input: MatchInput<A>) => MatchOutput<A>;
|
|
49
|
+
input: () => MatchInput;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
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
|
+
declare interface MatchmakingAvoidContext {
|
|
60
|
+
readonly tenantId: string;
|
|
61
|
+
readonly queue: string;
|
|
62
|
+
readonly tickets: ReadonlyArray<MatchTicket>;
|
|
63
|
+
readonly maxPairs: number;
|
|
64
|
+
readonly query: DataStore['query'];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
declare type MatchmakingAvoidPairs = (context: MatchmakingAvoidContext) => Effect.Effect<ReadonlyArray<AvoidPair>, MatchmakingError>;
|
|
68
|
+
|
|
69
|
+
export declare const matchmakingAvoidPairsFixture: (query: DataStore["query"]) => {
|
|
70
|
+
readonly avoidPairs: typeof defaultMatchmakingAvoidPairs;
|
|
71
|
+
readonly context: MatchmakingAvoidContext;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/** System21 slot fixture for `avoidPairs`: the real default and a resolved override. */
|
|
75
|
+
export declare const matchmakingAvoidPairsSlotFixture: {
|
|
76
|
+
kind: "slot";
|
|
77
|
+
defaultImplementation: MatchmakingAvoidPairs;
|
|
78
|
+
defaultProbe: {
|
|
79
|
+
run: () => Effect.Effect<readonly AvoidPair[], MatchmakingError, never>;
|
|
80
|
+
expected: never[];
|
|
81
|
+
};
|
|
82
|
+
overrideProbe: {
|
|
83
|
+
run: () => Effect.Effect<readonly AvoidPair[], MatchmakingError, never>;
|
|
84
|
+
expected: string[][];
|
|
85
|
+
};
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export declare const matchmakingDirectorFakeLayer: Layer.Layer<MatchmakingDirectorPort, never, never>;
|
|
89
|
+
|
|
90
|
+
/** A scheduled Director run: `runKey` makes the run idempotent; `maxPairs` bounds tenant×queue work per run. */
|
|
91
|
+
declare const MatchmakingDirectorPayload: Schema.Struct<{
|
|
92
|
+
readonly runKey: Schema.NonEmptyString;
|
|
93
|
+
readonly maxPairs: Schema.Number;
|
|
94
|
+
}>;
|
|
95
|
+
|
|
96
|
+
declare type MatchmakingDirectorPayload = typeof MatchmakingDirectorPayload.Type;
|
|
97
|
+
|
|
98
|
+
declare class MatchmakingDirectorPort extends MatchmakingDirectorPort_base {
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
declare const MatchmakingDirectorPort_base: Context.ServiceClass<MatchmakingDirectorPort, "@voltro/MatchmakingDirectorPort", MatchmakingDirectorService>;
|
|
102
|
+
|
|
103
|
+
export declare const matchmakingDirectorPortFixture: {
|
|
104
|
+
kind: "port";
|
|
105
|
+
tag: typeof MatchmakingDirectorPort;
|
|
106
|
+
fakeLayer: Layer.Layer<MatchmakingDirectorPort, never, never>;
|
|
107
|
+
probe: {
|
|
108
|
+
run: () => Effect.Effect<{
|
|
109
|
+
readonly cycles: number;
|
|
110
|
+
readonly assigned: number;
|
|
111
|
+
readonly skipped: number;
|
|
112
|
+
readonly failed: number;
|
|
113
|
+
}, MatchmakingError, never>;
|
|
114
|
+
expected: {
|
|
115
|
+
cycles: number;
|
|
116
|
+
assigned: number;
|
|
117
|
+
skipped: number;
|
|
118
|
+
failed: number;
|
|
119
|
+
};
|
|
120
|
+
};
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
declare const MatchmakingDirectorResult: Schema.Struct<{
|
|
124
|
+
readonly cycles: Schema.Number;
|
|
125
|
+
readonly assigned: Schema.Number;
|
|
126
|
+
readonly skipped: Schema.Number;
|
|
127
|
+
readonly failed: Schema.Number;
|
|
128
|
+
}>;
|
|
129
|
+
|
|
130
|
+
declare type MatchmakingDirectorResult = typeof MatchmakingDirectorResult.Type;
|
|
131
|
+
|
|
132
|
+
/** One Director pass over every tenant and queue with open tickets: plan, then assign. */
|
|
133
|
+
declare interface MatchmakingDirectorService {
|
|
134
|
+
readonly run: (input: MatchmakingDirectorPayload) => Effect.Effect<MatchmakingDirectorResult, MatchmakingError>;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
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
|
+
/** Layer values the System21 suite compares by identity with the port fixtures. */
|
|
155
|
+
export declare const matchmakingFakeLayer: Layer.Layer<MatchmakingPort, never, never>;
|
|
156
|
+
|
|
157
|
+
export declare const matchmakingInputFixture: () => MatchInput;
|
|
158
|
+
|
|
159
|
+
declare class MatchmakingPort extends MatchmakingPort_base {
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
declare const MatchmakingPort_base: Context.ServiceClass<MatchmakingPort, "@voltro/MatchmakingPort", MatchmakingService>;
|
|
163
|
+
|
|
164
|
+
export declare const matchmakingPortFixture: {
|
|
165
|
+
kind: "port";
|
|
166
|
+
tag: typeof MatchmakingPort;
|
|
167
|
+
fakeLayer: Layer.Layer<MatchmakingPort, never, never>;
|
|
168
|
+
probe: {
|
|
169
|
+
run: () => Effect.Effect<"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" | "succeeded", never, never>;
|
|
170
|
+
expected: string;
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
/** Queries wrap status reactively; this port owns no transport or poll timer. */
|
|
175
|
+
declare interface MatchmakingService {
|
|
176
|
+
readonly enqueue: (subject: Subject, input: EnqueueIntent) => Effect.Effect<MatchmakingTicket, MatchmakingError>;
|
|
177
|
+
readonly cancel: (subject: Subject, ticketId: string) => Effect.Effect<void, MatchmakingError>;
|
|
178
|
+
readonly status: (subject: Subject, ticketId: string) => Effect.Effect<MatchmakingTicket, MatchmakingError>;
|
|
179
|
+
/** Median recent wait in a declared queue for the caller's tenant. Optional for replacement ports: without it
|
|
180
|
+
* `matchmaking.estimate` answers `unavailable`. */
|
|
181
|
+
readonly estimate?: (subject: Subject, queue: string) => Effect.Effect<MatchmakingEstimate, MatchmakingError>;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
declare type MatchmakingTicket = typeof MatchmakingTicketSchema.Type;
|
|
185
|
+
|
|
186
|
+
declare const MatchmakingTicketSchema: Schema.Struct<{
|
|
187
|
+
readonly id: Schema.String;
|
|
188
|
+
readonly queue: Schema.String;
|
|
189
|
+
readonly state: Schema.Literals<readonly ["open", "matched", "assigned", "cancelled", "expired"]>;
|
|
190
|
+
readonly members: Schema.$Array<Schema.String>;
|
|
191
|
+
readonly attributes: Schema.$Record<Schema.String, Schema.Unknown>;
|
|
192
|
+
readonly createdAt: Schema.Number;
|
|
193
|
+
readonly expiresAt: Schema.Number;
|
|
194
|
+
readonly closedAt: Schema.NullOr<Schema.Number>;
|
|
195
|
+
readonly metadata: Schema.$Record<Schema.String, Schema.Unknown>;
|
|
196
|
+
}>;
|
|
197
|
+
|
|
198
|
+
declare interface MatchOutput<A = Readonly<Record<string, unknown>>> {
|
|
199
|
+
readonly proposals: ReadonlyArray<MatchProposal<A>>;
|
|
200
|
+
readonly leftover: ReadonlyArray<MatchTicket<A>>;
|
|
201
|
+
/** Deterministic branch inspections, not a runtime or throughput measurement. */
|
|
202
|
+
readonly searchNodes: number;
|
|
203
|
+
readonly exhausted: boolean;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
declare interface MatchProposal<A = Readonly<Record<string, unknown>>> {
|
|
207
|
+
readonly teams: ReadonlyArray<ReadonlyArray<MatchTicket<A>>>;
|
|
208
|
+
readonly quality: number;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
declare interface MatchQueueRules {
|
|
212
|
+
readonly name: string;
|
|
213
|
+
readonly teams: {
|
|
214
|
+
readonly count: number;
|
|
215
|
+
readonly size: number;
|
|
216
|
+
};
|
|
217
|
+
readonly maxTickets: number;
|
|
218
|
+
readonly maxSearchNodes: number;
|
|
219
|
+
readonly maxAvoidPairs: number;
|
|
220
|
+
readonly maxAttributeBytes: number;
|
|
221
|
+
readonly minQuality: {
|
|
222
|
+
readonly initial: number;
|
|
223
|
+
readonly after: ReadonlyArray<readonly [number, number]>;
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
declare interface MatchTicket<A = Readonly<Record<string, unknown>>> {
|
|
228
|
+
readonly id: string;
|
|
229
|
+
readonly tenantId: string;
|
|
230
|
+
readonly queue: string;
|
|
231
|
+
readonly members: ReadonlyArray<string>;
|
|
232
|
+
readonly createdAt: number;
|
|
233
|
+
readonly attributes: A;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export declare const partyMembershipFakeLayer: Layer.Layer<PartyMembershipPort, never, never>;
|
|
237
|
+
|
|
238
|
+
declare class PartyMembershipPort extends PartyMembershipPort_base {
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
declare const PartyMembershipPort_base: Context.ServiceClass<PartyMembershipPort, "@voltro/PartyMembershipPort", PartyMembershipService>;
|
|
242
|
+
|
|
243
|
+
export declare const partyMembershipPortFixture: {
|
|
244
|
+
kind: "port";
|
|
245
|
+
tag: typeof PartyMembershipPort;
|
|
246
|
+
fakeLayer: Layer.Layer<PartyMembershipPort, never, never>;
|
|
247
|
+
probe: {
|
|
248
|
+
run: () => Effect.Effect<boolean, MatchmakingError, never>;
|
|
249
|
+
expected: boolean;
|
|
250
|
+
};
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
/** Chat and other plugins ask current party membership here, never party SQL. */
|
|
254
|
+
declare interface PartyMembershipService {
|
|
255
|
+
readonly isMember: (subject: Subject, partyId: string) => Effect.Effect<boolean, MatchmakingError>;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** A detached current SQL roster. It contains IDs, never inferred Subject kinds. */
|
|
259
|
+
declare interface PartyRoster {
|
|
260
|
+
readonly tenantId: string;
|
|
261
|
+
readonly partyId: string;
|
|
262
|
+
readonly leaderId: string;
|
|
263
|
+
readonly memberIds: ReadonlyArray<string>;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export declare const partyRosterFakeLayer: Layer.Layer<PartyRosterPort, never, never>;
|
|
267
|
+
|
|
268
|
+
declare class PartyRosterPort extends PartyRosterPort_base {
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
declare const PartyRosterPort_base: Context.ServiceClass<PartyRosterPort, "@voltro/PartyRosterPort", PartyRosterService>;
|
|
272
|
+
|
|
273
|
+
export declare const partyRosterPortFixture: {
|
|
274
|
+
kind: "port";
|
|
275
|
+
tag: typeof PartyRosterPort;
|
|
276
|
+
fakeLayer: Layer.Layer<PartyRosterPort, never, never>;
|
|
277
|
+
probe: {
|
|
278
|
+
run: () => Effect.Effect<readonly string[], MatchmakingError, never>;
|
|
279
|
+
expected: string[];
|
|
280
|
+
};
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
/** Trusted server composition only. The caller must authorize the operation;
|
|
284
|
+
* this read is not a player route or authority to move the returned members. */
|
|
285
|
+
declare interface PartyRosterService {
|
|
286
|
+
readonly read: (tenantId: string, partyId: string) => Effect.Effect<PartyRoster | null, MatchmakingError>;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
declare interface SkillWindowOptions {
|
|
290
|
+
readonly initialWindow: number;
|
|
291
|
+
readonly growPerSecond: number;
|
|
292
|
+
readonly maxWindow: number;
|
|
293
|
+
readonly latencyMaxMs?: number;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export { }
|
package/dist/testing.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { matchmakingError as e } from "./errors.js";
|
|
2
|
+
import { captureMatchInput as t } from "./contract.js";
|
|
3
|
+
import { defaultMatchmakingAvoidPairs as n } from "./avoidPairs.js";
|
|
4
|
+
import { fifo as r, regionLatency as i, skillWindow as a } from "./strategies.js";
|
|
5
|
+
import { defineQueue as o } from "./queue.js";
|
|
6
|
+
import { MatchmakingDirectorPort as s, MatchmakingPort as c, PartyMembershipPort as l, PartyRosterPort as u } from "./port.js";
|
|
7
|
+
import { resolveTicketAdmissionOptions as d } from "./service.js";
|
|
8
|
+
import { Effect as f, Layer as p, Result as m, Schema as h } from "effect";
|
|
9
|
+
//#region src/testing.ts
|
|
10
|
+
var g = () => ({
|
|
11
|
+
now: 1e3,
|
|
12
|
+
queue: {
|
|
13
|
+
name: "fixture",
|
|
14
|
+
teams: {
|
|
15
|
+
count: 2,
|
|
16
|
+
size: 1
|
|
17
|
+
},
|
|
18
|
+
maxTickets: 100,
|
|
19
|
+
maxSearchNodes: 1e4,
|
|
20
|
+
maxAvoidPairs: 100,
|
|
21
|
+
maxAttributeBytes: 1024,
|
|
22
|
+
minQuality: {
|
|
23
|
+
initial: 0,
|
|
24
|
+
after: []
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
avoid: [],
|
|
28
|
+
tickets: ["a", "b"].map((e) => ({
|
|
29
|
+
id: e,
|
|
30
|
+
tenantId: "tenant",
|
|
31
|
+
queue: "fixture",
|
|
32
|
+
members: [e],
|
|
33
|
+
createdAt: 0,
|
|
34
|
+
attributes: {
|
|
35
|
+
skill: 100,
|
|
36
|
+
region: "eu",
|
|
37
|
+
latencies: { eu: 20 }
|
|
38
|
+
}
|
|
39
|
+
}))
|
|
40
|
+
}), _ = {
|
|
41
|
+
fifo: r,
|
|
42
|
+
skillWindow: a,
|
|
43
|
+
regionLatency: i,
|
|
44
|
+
input: g
|
|
45
|
+
}, v = (t = {}) => ({
|
|
46
|
+
enqueue: t.enqueue ?? (() => f.fail(e("unavailable"))),
|
|
47
|
+
cancel: t.cancel ?? (() => f.fail(e("unavailable"))),
|
|
48
|
+
status: t.status ?? (() => f.fail(e("unavailable"))),
|
|
49
|
+
estimate: t.estimate ?? (() => f.fail(e("unavailable")))
|
|
50
|
+
}), y = (e = {}) => p.succeed(c, v(e)), b = (e) => {
|
|
51
|
+
let r = t(g());
|
|
52
|
+
return {
|
|
53
|
+
avoidPairs: n,
|
|
54
|
+
context: Object.freeze({
|
|
55
|
+
tenantId: "tenant",
|
|
56
|
+
queue: "fixture",
|
|
57
|
+
tickets: r.tickets,
|
|
58
|
+
maxPairs: r.queue.maxAvoidPairs,
|
|
59
|
+
query: e
|
|
60
|
+
})
|
|
61
|
+
};
|
|
62
|
+
}, x = {
|
|
63
|
+
kind: "slot",
|
|
64
|
+
defaultImplementation: n,
|
|
65
|
+
defaultProbe: {
|
|
66
|
+
run: () => n(b(async () => []).context),
|
|
67
|
+
expected: []
|
|
68
|
+
},
|
|
69
|
+
overrideProbe: {
|
|
70
|
+
run: () => d({
|
|
71
|
+
queues: [o({
|
|
72
|
+
name: "fixture",
|
|
73
|
+
room: "arena",
|
|
74
|
+
ticket: h.Struct({}),
|
|
75
|
+
teams: {
|
|
76
|
+
count: 2,
|
|
77
|
+
size: 1
|
|
78
|
+
},
|
|
79
|
+
matchFunction: r()
|
|
80
|
+
})],
|
|
81
|
+
prepareTicket: () => f.succeed({}),
|
|
82
|
+
admit: () => f.void,
|
|
83
|
+
avoidPairs: () => f.succeed([["a", "b"]])
|
|
84
|
+
}).avoidPairs(b(async () => []).context),
|
|
85
|
+
expected: [["a", "b"]]
|
|
86
|
+
}
|
|
87
|
+
}, S = y(), C = {
|
|
88
|
+
kind: "port",
|
|
89
|
+
tag: c,
|
|
90
|
+
fakeLayer: S,
|
|
91
|
+
probe: {
|
|
92
|
+
run: () => c.pipe(f.flatMap((e) => f.result(e.status({
|
|
93
|
+
type: "user",
|
|
94
|
+
id: "a",
|
|
95
|
+
tenantId: "tenant"
|
|
96
|
+
}, "ticket"))), f.map((e) => m.isFailure(e) ? e.failure.code : "succeeded"), f.provide(S)),
|
|
97
|
+
expected: "unavailable"
|
|
98
|
+
}
|
|
99
|
+
}, w = (e = {}) => p.succeed(l, { isMember: (t, n) => f.succeed(t.type === "user" && (e[n] ?? []).includes(String(t.id))) }), T = w({ party: ["a"] }), E = {
|
|
100
|
+
kind: "port",
|
|
101
|
+
tag: l,
|
|
102
|
+
fakeLayer: T,
|
|
103
|
+
probe: {
|
|
104
|
+
run: () => l.pipe(f.flatMap((e) => e.isMember({
|
|
105
|
+
type: "user",
|
|
106
|
+
id: "a",
|
|
107
|
+
tenantId: "tenant"
|
|
108
|
+
}, "party")), f.provide(T)),
|
|
109
|
+
expected: !0
|
|
110
|
+
}
|
|
111
|
+
}, D = (t = () => f.fail(e("unavailable"))) => p.succeed(u, { read: t }), O = D((e, t) => f.succeed(e === "tenant" && t === "party" ? Object.freeze({
|
|
112
|
+
tenantId: e,
|
|
113
|
+
partyId: t,
|
|
114
|
+
leaderId: "a",
|
|
115
|
+
memberIds: Object.freeze(["a", "b"])
|
|
116
|
+
}) : null)), k = {
|
|
117
|
+
kind: "port",
|
|
118
|
+
tag: u,
|
|
119
|
+
fakeLayer: O,
|
|
120
|
+
probe: {
|
|
121
|
+
run: () => u.pipe(f.flatMap((e) => e.read("tenant", "party")), f.map((e) => e?.memberIds ?? []), f.provide(O)),
|
|
122
|
+
expected: ["a", "b"]
|
|
123
|
+
}
|
|
124
|
+
}, A = (e = {
|
|
125
|
+
cycles: 0,
|
|
126
|
+
assigned: 0,
|
|
127
|
+
skipped: 0,
|
|
128
|
+
failed: 0
|
|
129
|
+
}) => p.succeed(s, { run: () => f.succeed(e) }), j = A({
|
|
130
|
+
cycles: 1,
|
|
131
|
+
assigned: 1,
|
|
132
|
+
skipped: 0,
|
|
133
|
+
failed: 0
|
|
134
|
+
}), M = {
|
|
135
|
+
kind: "port",
|
|
136
|
+
tag: s,
|
|
137
|
+
fakeLayer: j,
|
|
138
|
+
probe: {
|
|
139
|
+
run: () => s.pipe(f.flatMap((e) => e.run({
|
|
140
|
+
runKey: "fixture",
|
|
141
|
+
maxPairs: 1
|
|
142
|
+
})), f.provide(j)),
|
|
143
|
+
expected: {
|
|
144
|
+
cycles: 1,
|
|
145
|
+
assigned: 1,
|
|
146
|
+
skipped: 0,
|
|
147
|
+
failed: 0
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
//#endregion
|
|
152
|
+
export { A as fakeMatchmakingDirectorLayer, y as fakeMatchmakingLayer, v as fakeMatchmakingService, w as fakePartyMembershipLayer, D as fakePartyRosterLayer, _ as matchFunctionFixture, b as matchmakingAvoidPairsFixture, x as matchmakingAvoidPairsSlotFixture, j as matchmakingDirectorFakeLayer, M as matchmakingDirectorPortFixture, S as matchmakingFakeLayer, g as matchmakingInputFixture, C as matchmakingPortFixture, T as partyMembershipFakeLayer, E as partyMembershipPortFixture, O as partyRosterFakeLayer, k as partyRosterPortFixture };
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Schema } from 'effect';
|
|
2
|
+
|
|
3
|
+
export declare const ASSIGNMENTS_TABLE = "_voltro_mm_assignments";
|
|
4
|
+
|
|
5
|
+
export declare const CLAIMS_TABLE = "_voltro_mm_subject_admission";
|
|
6
|
+
|
|
7
|
+
/** Players send a queue preference intent; never skill, members, state or assignments. */
|
|
8
|
+
export declare interface EnqueueIntent {
|
|
9
|
+
readonly queue: string;
|
|
10
|
+
readonly requestId: string;
|
|
11
|
+
readonly preferences: Readonly<Record<string, unknown>>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export declare type MatchmakingAssignment = typeof MatchmakingAssignmentSchema.Type;
|
|
15
|
+
|
|
16
|
+
export declare const MatchmakingAssignmentSchema: Schema.Struct<{
|
|
17
|
+
readonly ticketId: Schema.String;
|
|
18
|
+
readonly queue: Schema.String;
|
|
19
|
+
readonly roomId: Schema.String;
|
|
20
|
+
readonly sessionId: Schema.String;
|
|
21
|
+
readonly createdAt: Schema.Number;
|
|
22
|
+
}>;
|
|
23
|
+
|
|
24
|
+
export declare type MatchmakingEstimate = typeof MatchmakingEstimateSchema.Type;
|
|
25
|
+
|
|
26
|
+
/** Expected wait in a queue: the median time recent searches of this tenant waited until matched, from up to
|
|
27
|
+
* `samples` searches matched in the last 30 minutes. `waitMs` is null with fewer than three such searches. */
|
|
28
|
+
export declare const MatchmakingEstimateSchema: Schema.Struct<{
|
|
29
|
+
readonly queue: Schema.String;
|
|
30
|
+
readonly waitMs: Schema.NullOr<Schema.Number>;
|
|
31
|
+
readonly samples: Schema.Number;
|
|
32
|
+
}>;
|
|
33
|
+
|
|
34
|
+
export declare type MatchmakingTicket = typeof MatchmakingTicketSchema.Type;
|
|
35
|
+
|
|
36
|
+
export declare const MatchmakingTicketSchema: Schema.Struct<{
|
|
37
|
+
readonly id: Schema.String;
|
|
38
|
+
readonly queue: Schema.String;
|
|
39
|
+
readonly state: Schema.Literals<readonly ["open", "matched", "assigned", "cancelled", "expired"]>;
|
|
40
|
+
readonly members: Schema.$Array<Schema.String>;
|
|
41
|
+
readonly attributes: Schema.$Record<Schema.String, Schema.Unknown>;
|
|
42
|
+
readonly createdAt: Schema.Number;
|
|
43
|
+
readonly expiresAt: Schema.Number;
|
|
44
|
+
readonly closedAt: Schema.NullOr<Schema.Number>;
|
|
45
|
+
readonly metadata: Schema.$Record<Schema.String, Schema.Unknown>;
|
|
46
|
+
}>;
|
|
47
|
+
|
|
48
|
+
export declare const QUEUES_TABLE = "_voltro_mm_queue_admission";
|
|
49
|
+
|
|
50
|
+
export declare const TICKETS_TABLE = "_voltro_mm_tickets";
|
|
51
|
+
|
|
52
|
+
export { }
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Schema as e } from "effect";
|
|
2
|
+
//#region src/ticketContract.ts
|
|
3
|
+
var t = "_voltro_mm_tickets", n = "_voltro_mm_subject_admission", r = "_voltro_mm_queue_admission", i = "_voltro_mm_assignments", a = e.Number.pipe(e.check(e.isInt()), e.check(e.isGreaterThanOrEqualTo(0))), o = e.Record(e.String, e.Unknown), s = e.Struct({
|
|
4
|
+
id: e.String,
|
|
5
|
+
queue: e.String,
|
|
6
|
+
state: e.Literals([
|
|
7
|
+
"open",
|
|
8
|
+
"matched",
|
|
9
|
+
"assigned",
|
|
10
|
+
"cancelled",
|
|
11
|
+
"expired"
|
|
12
|
+
]),
|
|
13
|
+
members: e.Array(e.String),
|
|
14
|
+
attributes: o,
|
|
15
|
+
createdAt: a,
|
|
16
|
+
expiresAt: a,
|
|
17
|
+
closedAt: e.NullOr(a),
|
|
18
|
+
metadata: o
|
|
19
|
+
}), c = e.Struct({
|
|
20
|
+
queue: e.String,
|
|
21
|
+
waitMs: e.NullOr(e.Number),
|
|
22
|
+
samples: e.Number
|
|
23
|
+
}), l = e.Struct({
|
|
24
|
+
ticketId: e.String,
|
|
25
|
+
queue: e.String,
|
|
26
|
+
roomId: e.String,
|
|
27
|
+
sessionId: e.String,
|
|
28
|
+
createdAt: a
|
|
29
|
+
});
|
|
30
|
+
//#endregion
|
|
31
|
+
export { i as ASSIGNMENTS_TABLE, n as CLAIMS_TABLE, l as MatchmakingAssignmentSchema, c as MatchmakingEstimateSchema, s as MatchmakingTicketSchema, r as QUEUES_TABLE, t as TICKETS_TABLE };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ASSIGNMENTS_TABLE as e, TICKETS_TABLE as t } from "./ticketContract.js";
|
|
2
|
+
import { id as n, integer as r, json as i, table as a, text as o, timestamp as s } from "@voltro/database";
|
|
3
|
+
//#region src/tickets.entity.ts
|
|
4
|
+
var c = a(t, {
|
|
5
|
+
id: n({ prefix: "mmt" }),
|
|
6
|
+
tenantId: o().maxLength(256).externalId().sensitive("pii"),
|
|
7
|
+
subjectId: o().maxLength(256).externalId().sensitive("pii"),
|
|
8
|
+
queue: o().maxLength(256).safe(),
|
|
9
|
+
requestKey: o().maxLength(64).serverOnly(),
|
|
10
|
+
partyId: o().maxLength(64).nullable().safe(),
|
|
11
|
+
members: i().sensitive("pii"),
|
|
12
|
+
attributes: i().sensitive("pii"),
|
|
13
|
+
state: o().maxLength(16).oneOf([
|
|
14
|
+
"open",
|
|
15
|
+
"matched",
|
|
16
|
+
"assigned",
|
|
17
|
+
"cancelled",
|
|
18
|
+
"expired"
|
|
19
|
+
]).safe(),
|
|
20
|
+
attempts: r().default(0).safe(),
|
|
21
|
+
matchedAt: s().nullable().safe(),
|
|
22
|
+
createdAt: s().safe(),
|
|
23
|
+
expiresAt: s().safe(),
|
|
24
|
+
closedAt: s().nullable().safe(),
|
|
25
|
+
metadata: i().sensitive("pii")
|
|
26
|
+
}).index([
|
|
27
|
+
"tenantId",
|
|
28
|
+
"queue",
|
|
29
|
+
"state",
|
|
30
|
+
"createdAt"
|
|
31
|
+
]).index(["tenantId", "subjectId"]).index([
|
|
32
|
+
"tenantId",
|
|
33
|
+
"partyId",
|
|
34
|
+
"state"
|
|
35
|
+
]), l = a(e, {
|
|
36
|
+
id: n({ prefix: "mma" }),
|
|
37
|
+
tenantId: o().maxLength(256).externalId().sensitive("pii"),
|
|
38
|
+
subjectId: o().maxLength(256).externalId().sensitive("pii"),
|
|
39
|
+
ticketId: o().maxLength(64).safe(),
|
|
40
|
+
queue: o().maxLength(256).safe(),
|
|
41
|
+
roomId: o().maxLength(191).safe(),
|
|
42
|
+
sessionId: o().maxLength(191).serverOnly(),
|
|
43
|
+
createdAt: s().safe()
|
|
44
|
+
}).unique("byMatchmakingAssignmentSeat", ["ticketId", "subjectId"]).index([
|
|
45
|
+
"tenantId",
|
|
46
|
+
"subjectId",
|
|
47
|
+
"createdAt"
|
|
48
|
+
]);
|
|
49
|
+
//#endregion
|
|
50
|
+
export { l as matchmakingAssignmentsTable, c as matchmakingTicketsTable };
|