@voltro/plugin-leaderboards 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.
- package/CHANGELOG.md +24492 -0
- package/LICENSE +61 -0
- package/README.md +1087 -2
- package/SECURITY.md +56 -0
- package/THIRD-PARTY-NOTICES.md +5603 -0
- package/dist/administration.js +4 -0
- package/dist/bracketAdmission.js +293 -0
- package/dist/bracketAdmissionPolicy.js +21 -0
- package/dist/bracketInstallation.js +132 -0
- package/dist/bracketMatchPorts.js +4 -0
- package/dist/bracketMatches.js +545 -0
- package/dist/brackets.d.ts +149 -0
- package/dist/brackets.js +378 -0
- package/dist/bucketStore.js +92 -0
- package/dist/buckets.js +8 -0
- package/dist/errors.d.ts +13 -0
- package/dist/errors.js +16 -0
- package/dist/glicko2.js +71 -0
- package/dist/index.d.ts +1208 -0
- package/dist/index.js +15 -0
- package/dist/inspect.js +83 -0
- package/dist/json.js +38 -0
- package/dist/leaderboard.d.ts +49 -0
- package/dist/leaderboard.js +56 -0
- package/dist/leagueSchedule.js +39 -0
- package/dist/leagues.js +30 -0
- package/dist/openskill.js +73 -0
- package/dist/plugin.js +337 -0
- package/dist/portLifetime.js +66 -0
- package/dist/ports.d.ts +294 -0
- package/dist/ports.js +6 -0
- package/dist/rankQuery.js +23 -0
- package/dist/ranking.d.ts +42 -0
- package/dist/ranking.js +70 -0
- package/dist/rating.d.ts +103 -0
- package/dist/rating.js +49 -0
- package/dist/ratingPools.js +133 -0
- package/dist/ratingStore.js +333 -0
- package/dist/ratingStrategies.js +107 -0
- package/dist/records.d.ts +116 -0
- package/dist/records.js +87 -0
- package/dist/rpc.d.ts +321 -0
- package/dist/rpc.js +305 -0
- package/dist/score.d.ts +13 -0
- package/dist/score.js +26 -0
- package/dist/server.d.ts +1477 -0
- package/dist/server.js +17 -0
- package/dist/service.js +948 -0
- package/dist/subjectFences.js +43 -0
- package/dist/tables.entity.js +187 -0
- package/dist/testing.d.ts +872 -0
- package/dist/testing.js +367 -0
- package/dist/tournament-app/tournament.advance.workflow.server.tsx +3 -0
- package/dist/tournament-app/tournament.advance.workflow.tsx +12 -0
- package/dist/tournament-app/tournament.stage.workflow.server.tsx +3 -0
- package/dist/tournament-app/tournament.stage.workflow.tsx +10 -0
- package/dist/tournamentInstallation.js +167 -0
- package/dist/tournamentPrizes.js +33 -0
- package/dist/tournamentStage.js +150 -0
- package/dist/tournamentStagePorts.js +4 -0
- package/dist/tournamentWorkflow.js +100 -0
- package/dist/tournamentWorkflowSchemas.js +53 -0
- package/dist/tournaments.d.ts +32 -0
- package/dist/tournaments.js +20 -0
- package/dist/web.d.ts +175 -0
- package/dist/web.js +16 -0
- package/package.json +113 -4
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
import { LeaderboardError as e, leaderboardError as t } from "./errors.js";
|
|
2
|
+
import { canonicalLeaderboardJson as n, captureLeaderboardJson as r } from "./json.js";
|
|
3
|
+
import { makeLeaderboardSubjectFences as i } from "./subjectFences.js";
|
|
4
|
+
import { TournamentBracketPort as a } from "./bracketMatchPorts.js";
|
|
5
|
+
import { advanceDoubleElimination as o, advanceRoundRobin as s, advanceSingleElimination as c, advanceSwiss as l, seedDoubleElimination as u, seedSingleElimination as d } from "./brackets.js";
|
|
6
|
+
import { Effect as f, Layer as p, Schema as m } from "effect";
|
|
7
|
+
import { Subject as h } from "@voltro/protocol";
|
|
8
|
+
import { createHash as g } from "node:crypto";
|
|
9
|
+
import { and as _, eq as v, id as y, json as b, table as x, text as S, timestamp as C } from "@voltro/database";
|
|
10
|
+
// src/bracketMatches.ts
|
|
11
|
+
var w = "_voltro_brackets", T = x(w, {
|
|
12
|
+
id: y({ prefix: "tbrk" }),
|
|
13
|
+
tenantId: S().maxLength(120).externalId().sensitive("pii"),
|
|
14
|
+
tournamentId: S().maxLength(120).serverOnly(),
|
|
15
|
+
state: b().sensitive("pii"),
|
|
16
|
+
createdAt: C(),
|
|
17
|
+
updatedAt: C()
|
|
18
|
+
}).unique("byTournamentBracket", ["tenantId", "tournamentId"]).index(["tenantId"]);
|
|
19
|
+
function E(e) {
|
|
20
|
+
throw t(e);
|
|
21
|
+
}
|
|
22
|
+
var D = (e) => typeof e == "string" && e.length > 0 && e.length <= 120 && e.trim() === e && e.isWellFormed() && !/[\u0000-\u001f\u007f]/.test(e) ? e : E("invalid_input"), O = (e) => Number.isSafeInteger(e) && e >= 0 && e <= 864e13 ? e : E("invalid_input"), k = (e) => {
|
|
23
|
+
let t = r(e, 1024);
|
|
24
|
+
return t && Object.keys(t).length === 2 && Object.hasOwn(t, "tenantId") && Object.hasOwn(t, "tournamentId") || E("invalid_input"), Object.freeze({
|
|
25
|
+
tenantId: D(t.tenantId),
|
|
26
|
+
tournamentId: D(t.tournamentId)
|
|
27
|
+
});
|
|
28
|
+
}, A = (e, t) => m.decodeUnknownSync(e)(r(t, 65536), { onExcessProperty: "error" }), j = m.Struct({
|
|
29
|
+
format: m.Literals([
|
|
30
|
+
"single-elimination",
|
|
31
|
+
"double-elimination",
|
|
32
|
+
"round-robin",
|
|
33
|
+
"swiss"
|
|
34
|
+
]),
|
|
35
|
+
entrants: m.Array(m.Struct({
|
|
36
|
+
entrantId: m.String,
|
|
37
|
+
seed: m.Int
|
|
38
|
+
})).pipe(m.check(m.isMinLength(2)), m.check(m.isMaxLength(64))),
|
|
39
|
+
rounds: m.optional(m.Int.pipe(m.check(m.isBetween({
|
|
40
|
+
minimum: 1,
|
|
41
|
+
maximum: 63
|
|
42
|
+
})))),
|
|
43
|
+
ruleVersion: m.String
|
|
44
|
+
}), M = m.Struct({
|
|
45
|
+
roomId: m.String,
|
|
46
|
+
epoch: m.Int.pipe(m.check(m.isBetween({
|
|
47
|
+
minimum: 1,
|
|
48
|
+
maximum: 4294967295
|
|
49
|
+
})))
|
|
50
|
+
}), N = m.Struct({
|
|
51
|
+
...M.fields,
|
|
52
|
+
matchId: m.String,
|
|
53
|
+
reportId: m.String,
|
|
54
|
+
winnerId: m.String
|
|
55
|
+
}), P = m.Struct({
|
|
56
|
+
disputeId: m.String,
|
|
57
|
+
matchId: m.String,
|
|
58
|
+
reason: m.Literals([
|
|
59
|
+
"result_mismatch",
|
|
60
|
+
"no_show",
|
|
61
|
+
"connection"
|
|
62
|
+
])
|
|
63
|
+
}), F = m.Struct({
|
|
64
|
+
disputeId: m.String,
|
|
65
|
+
resolutionId: m.String,
|
|
66
|
+
winnerId: m.String
|
|
67
|
+
}), I = m.Struct({
|
|
68
|
+
tenantId: m.String,
|
|
69
|
+
tournamentId: m.String,
|
|
70
|
+
matchId: m.String,
|
|
71
|
+
requestId: m.String,
|
|
72
|
+
left: m.String,
|
|
73
|
+
right: m.String,
|
|
74
|
+
ruleVersion: m.String,
|
|
75
|
+
roomId: m.NullOr(m.String),
|
|
76
|
+
epoch: m.NullOr(m.Int)
|
|
77
|
+
}), L = m.Struct({
|
|
78
|
+
version: m.Literal(1),
|
|
79
|
+
format: j.fields.format,
|
|
80
|
+
rounds: j.fields.rounds,
|
|
81
|
+
entrants: m.Array(m.Struct({
|
|
82
|
+
entrantId: m.String,
|
|
83
|
+
seed: m.Int
|
|
84
|
+
})).pipe(m.check(m.isMaxLength(64))),
|
|
85
|
+
ruleVersion: m.String,
|
|
86
|
+
phase: m.Literals([
|
|
87
|
+
"active",
|
|
88
|
+
"paused",
|
|
89
|
+
"complete",
|
|
90
|
+
"erasing",
|
|
91
|
+
"cancelled"
|
|
92
|
+
]),
|
|
93
|
+
withdrawn: m.Boolean,
|
|
94
|
+
provisions: m.Array(I),
|
|
95
|
+
results: m.Array(m.Struct({
|
|
96
|
+
...N.fields,
|
|
97
|
+
reportedWinnerId: m.String
|
|
98
|
+
})),
|
|
99
|
+
disputes: m.Array(m.Struct({
|
|
100
|
+
...P.fields,
|
|
101
|
+
ownerId: m.String,
|
|
102
|
+
resolutionId: m.NullOr(m.String),
|
|
103
|
+
winnerId: m.NullOr(m.String)
|
|
104
|
+
})),
|
|
105
|
+
settlement: m.NullOr(m.Struct({
|
|
106
|
+
requestId: m.String,
|
|
107
|
+
winnerId: m.String,
|
|
108
|
+
complete: m.Boolean
|
|
109
|
+
})),
|
|
110
|
+
erase: m.NullOr(m.Struct({
|
|
111
|
+
requestId: m.String,
|
|
112
|
+
targetDigest: m.String
|
|
113
|
+
}))
|
|
114
|
+
}), R = (e) => g("sha256").update(n(e)).digest("hex"), z = (e, t, n) => "tbrk:" + R([
|
|
115
|
+
e,
|
|
116
|
+
t,
|
|
117
|
+
n
|
|
118
|
+
]), B = (e, t) => {
|
|
119
|
+
let n = t.map((e) => ({
|
|
120
|
+
matchId: e.matchId,
|
|
121
|
+
winnerId: e.winnerId
|
|
122
|
+
}));
|
|
123
|
+
switch (e.format) {
|
|
124
|
+
case "single-elimination": return c(d(e.entrants), n);
|
|
125
|
+
case "double-elimination": return o(u(e.entrants), n);
|
|
126
|
+
case "round-robin": return s(e.entrants, n);
|
|
127
|
+
case "swiss": return l(e.entrants, n, V(e));
|
|
128
|
+
}
|
|
129
|
+
}, V = (e) => e.rounds ?? Math.max(1, Math.ceil(Math.log2(e.entrants.length))), H = (e) => {
|
|
130
|
+
let t = e.entrants.length;
|
|
131
|
+
return e.format === "round-robin" ? t * (t - 1) / 2 : e.format === "swiss" ? V(e) * Math.floor(t / 2) : 2 * t - 1;
|
|
132
|
+
}, U = (e) => {
|
|
133
|
+
let t = A(j, e);
|
|
134
|
+
return D(t.ruleVersion), (t.rounds !== void 0 && t.format !== "swiss" || H(t) > 127) && E("invalid_input"), B(t, []), r(t, 16384);
|
|
135
|
+
}, W = (e, t) => {
|
|
136
|
+
new Set(e.map(t)).size !== e.length && E("unavailable");
|
|
137
|
+
}, G = (e, t) => {
|
|
138
|
+
(e.tenantId !== t.tenantId || e.tournamentId !== t.tournamentId) && E("unavailable");
|
|
139
|
+
let n = A(L, e.state);
|
|
140
|
+
D(n.ruleVersion), W(n.entrants, (e) => e.entrantId), W(n.provisions, (e) => e.matchId), W(n.provisions, (e) => e.requestId), W(n.results, (e) => e.matchId), W(n.results, (e) => e.reportId), W(n.disputes, (e) => e.disputeId);
|
|
141
|
+
for (let t of ["createdAt", "updatedAt"]) e[t] instanceof Date && Number.isFinite(e[t].getTime()) || E("unavailable");
|
|
142
|
+
(n.provisions.length > 127 || n.results.length > 127 || n.disputes.length > 254) && E("unavailable");
|
|
143
|
+
for (let e of n.entrants) D(e.entrantId), (!Number.isSafeInteger(e.seed) || e.seed < 1 || e.seed > 64) && E("unavailable");
|
|
144
|
+
for (let e of n.provisions) {
|
|
145
|
+
for (let t of [
|
|
146
|
+
"matchId",
|
|
147
|
+
"left",
|
|
148
|
+
"right",
|
|
149
|
+
"requestId"
|
|
150
|
+
]) D(e[t]);
|
|
151
|
+
(e.tenantId !== t.tenantId || e.tournamentId !== t.tournamentId || e.ruleVersion !== n.ruleVersion || e.left === e.right || e.requestId !== z(t, n.ruleVersion, "room:" + e.matchId) || e.roomId === null != (e.epoch === null)) && E("unavailable"), e.roomId !== null && A(M, {
|
|
152
|
+
roomId: D(e.roomId),
|
|
153
|
+
epoch: e.epoch
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
for (let e of n.results) {
|
|
157
|
+
for (let t of [
|
|
158
|
+
"reportId",
|
|
159
|
+
"winnerId",
|
|
160
|
+
"reportedWinnerId"
|
|
161
|
+
]) D(e[t]);
|
|
162
|
+
let t = n.provisions.find((t) => t.matchId === e.matchId);
|
|
163
|
+
t && t.roomId === e.roomId && t.epoch === e.epoch && [t.left, t.right].includes(e.winnerId) && [t.left, t.right].includes(e.reportedWinnerId) || E("unavailable");
|
|
164
|
+
}
|
|
165
|
+
for (let e of n.disputes) {
|
|
166
|
+
D(e.disputeId), D(e.ownerId);
|
|
167
|
+
let t = n.provisions.find((t) => t.matchId === e.matchId);
|
|
168
|
+
t && n.results.some((t) => t.matchId === e.matchId) && [t.left, t.right].includes(e.ownerId) && e.resolutionId === null == (e.winnerId === null) || E("unavailable"), e.resolutionId !== null && (D(e.resolutionId), t && [t.left, t.right].includes(e.winnerId) || E("unavailable"));
|
|
169
|
+
}
|
|
170
|
+
if (n.erase === null || /^[a-f0-9]{64}$/.test(n.erase.targetDigest) && D(n.erase.requestId) || E("unavailable"), ["erasing", "cancelled"].includes(n.phase) !== (n.erase !== null) && E("unavailable"), n.phase === "cancelled" && !n.withdrawn && E("unavailable"), n.withdrawn) return (!["erasing", "cancelled"].includes(n.phase) || n.provisions.length || n.results.length || n.disputes.length || n.settlement !== null) && E("unavailable"), n;
|
|
171
|
+
let r = B(n, n.results);
|
|
172
|
+
for (let e of n.provisions) {
|
|
173
|
+
let t = r.matches.find((t) => t.id === e.matchId);
|
|
174
|
+
t && t.left === e.left && t.right === e.right && ["ready", "complete"].includes(t.status) || E("unavailable");
|
|
175
|
+
}
|
|
176
|
+
let i = n.disputes.filter((e) => e.resolutionId === null).length;
|
|
177
|
+
return n.phase !== "erasing" && (n.phase === "paused" ? i === 0 : i !== 0) && E("unavailable"), n.settlement !== null && (r.champion !== n.settlement.winnerId || n.settlement.requestId !== z(t, n.ruleVersion, "champion") || i !== 0) && E("unavailable"), (n.phase === "complete" && n.settlement?.complete !== !0 || n.settlement?.complete && !["complete", "erasing"].includes(n.phase)) && E("unavailable"), n;
|
|
178
|
+
}, K = (e) => {
|
|
179
|
+
let t = [
|
|
180
|
+
"epochNow",
|
|
181
|
+
"authorize",
|
|
182
|
+
"provisionRoom",
|
|
183
|
+
"verifyResult",
|
|
184
|
+
"settleChampion",
|
|
185
|
+
"withdrawSubject",
|
|
186
|
+
"withdrawTournament",
|
|
187
|
+
"authorizeExport"
|
|
188
|
+
];
|
|
189
|
+
(!e || Object.getPrototypeOf(e) !== Object.prototype || Object.getOwnPropertySymbols(e).length || Object.entries(Object.getOwnPropertyDescriptors(e)).some(([e, n]) => ![...t, "maxTenantTournaments"].includes(e) || !n.enumerable || !("value" in n)) || t.some((t) => typeof e[t] != "function")) && E("invalid_input");
|
|
190
|
+
let n = e.maxTenantTournaments ?? 1024;
|
|
191
|
+
return (!Number.isSafeInteger(n) || n < 1 || n > 4096) && E("invalid_input"), Object.freeze({
|
|
192
|
+
epochNow: e.epochNow.bind(e),
|
|
193
|
+
authorize: e.authorize.bind(e),
|
|
194
|
+
provisionRoom: e.provisionRoom.bind(e),
|
|
195
|
+
verifyResult: e.verifyResult.bind(e),
|
|
196
|
+
settleChampion: e.settleChampion.bind(e),
|
|
197
|
+
withdrawSubject: e.withdrawSubject.bind(e),
|
|
198
|
+
withdrawTournament: e.withdrawTournament.bind(e),
|
|
199
|
+
authorizeExport: e.authorizeExport.bind(e),
|
|
200
|
+
maxTenantTournaments: n
|
|
201
|
+
});
|
|
202
|
+
}, q = (a, o) => {
|
|
203
|
+
let s = K(o), c = s.maxTenantTournaments, l = s.epochNow.bind(s), u = s.authorize.bind(s), d = s.provisionRoom.bind(s), p = s.verifyResult.bind(s), g = s.settleChampion.bind(s), y = s.withdrawSubject.bind(s), b = s.withdrawTournament.bind(s), x = s.authorizeExport.bind(s), { seedSubject: S, subjectFence: C, eraseSubjectFence: T } = i(a), j = (n) => f.tryPromise({
|
|
204
|
+
try: n,
|
|
205
|
+
catch: (n) => n instanceof e ? n : t("unavailable")
|
|
206
|
+
}).pipe(f.uninterruptible), I = (n) => {
|
|
207
|
+
try {
|
|
208
|
+
return n().pipe(f.uninterruptible);
|
|
209
|
+
} catch (n) {
|
|
210
|
+
return f.fail(n instanceof e ? n : t("invalid_input"));
|
|
211
|
+
}
|
|
212
|
+
}, L = (e, t, n) => {
|
|
213
|
+
let r = m.decodeUnknownSync(m.toType(h))(structuredClone(e));
|
|
214
|
+
return (n ? !["system", "service"].includes(r.type) : r.type !== "user" || r.id === null || r.tenantId !== t.tenantId) && E("forbidden"), r;
|
|
215
|
+
}, V = (e) => {
|
|
216
|
+
let t = A(m.Struct({
|
|
217
|
+
requestId: m.String,
|
|
218
|
+
target: m.Struct({
|
|
219
|
+
tenantId: m.String,
|
|
220
|
+
subjectId: m.String,
|
|
221
|
+
kind: m.Literals(["user", "guest"])
|
|
222
|
+
})
|
|
223
|
+
}), e);
|
|
224
|
+
return D(t.requestId), D(t.target.tenantId), D(t.target.subjectId), r(t, 2048);
|
|
225
|
+
}, H = async (e, t, n = !1) => {
|
|
226
|
+
let r = await e.query({
|
|
227
|
+
table: w,
|
|
228
|
+
predicate: _(v("tenantId", t.tenantId), v("tournamentId", t.tournamentId)),
|
|
229
|
+
order: [],
|
|
230
|
+
take: 2,
|
|
231
|
+
skip: void 0,
|
|
232
|
+
projection: void 0,
|
|
233
|
+
...n ? { lock: "update" } : {}
|
|
234
|
+
});
|
|
235
|
+
return r.length !== 1 && E(r.length ? "unavailable" : "not_found"), {
|
|
236
|
+
row: r[0],
|
|
237
|
+
state: G(r[0], t)
|
|
238
|
+
};
|
|
239
|
+
}, q = async (e, t, n) => {
|
|
240
|
+
r(n, 65536), G({
|
|
241
|
+
...t,
|
|
242
|
+
state: n
|
|
243
|
+
}, k({
|
|
244
|
+
tenantId: D(t.tenantId),
|
|
245
|
+
tournamentId: D(t.tournamentId)
|
|
246
|
+
})), await e.update("_voltro_brackets", String(t.id), {
|
|
247
|
+
state: n,
|
|
248
|
+
updatedAt: new Date(O(l()))
|
|
249
|
+
}) || E("unavailable");
|
|
250
|
+
}, J = (e) => {
|
|
251
|
+
(e.phase === "erasing" || e.phase === "cancelled") && E("erased");
|
|
252
|
+
}, Y = (e) => {
|
|
253
|
+
let t = e.withdrawn ? {
|
|
254
|
+
champion: null,
|
|
255
|
+
matches: []
|
|
256
|
+
} : B(e, e.results), n = new Map(e.provisions.filter((e) => e.roomId !== null).map((e) => [e.matchId, e.roomId]));
|
|
257
|
+
return r({
|
|
258
|
+
phase: e.phase,
|
|
259
|
+
format: e.format,
|
|
260
|
+
champion: t.champion,
|
|
261
|
+
matches: t.matches.map((e) => ({
|
|
262
|
+
...e,
|
|
263
|
+
roomId: n.get(e.id) ?? null
|
|
264
|
+
})),
|
|
265
|
+
unresolvedDisputes: e.disputes.filter((e) => e.resolutionId === null).length
|
|
266
|
+
}, 65536);
|
|
267
|
+
}, X = (e, t, r = !1) => j(async () => {
|
|
268
|
+
let i = r ? [] : (await H(a, e)).state.entrants.map((e) => e.entrantId).sort();
|
|
269
|
+
return a.transactional(async (a) => {
|
|
270
|
+
for (let t of i) await C(a, e.tenantId, t);
|
|
271
|
+
let { row: o, state: s } = await H(a, e, !0);
|
|
272
|
+
return !r && n(s.entrants.map((e) => e.entrantId).sort()) !== n(i) && E("conflict"), t(a, o, s);
|
|
273
|
+
});
|
|
274
|
+
}), Z = async (e) => {
|
|
275
|
+
let t = await a.query({
|
|
276
|
+
table: w,
|
|
277
|
+
predicate: v("tenantId", e),
|
|
278
|
+
order: [],
|
|
279
|
+
take: c + 1,
|
|
280
|
+
skip: void 0,
|
|
281
|
+
projection: void 0
|
|
282
|
+
});
|
|
283
|
+
t.length > c && E("limit_exceeded");
|
|
284
|
+
let n = t.map((t) => {
|
|
285
|
+
let n = k({
|
|
286
|
+
tenantId: e,
|
|
287
|
+
tournamentId: D(t.tournamentId)
|
|
288
|
+
});
|
|
289
|
+
return {
|
|
290
|
+
scope: n,
|
|
291
|
+
state: G(t, n)
|
|
292
|
+
};
|
|
293
|
+
});
|
|
294
|
+
return W(n, (e) => e.scope.tournamentId), n.sort((e, t) => e.scope.tournamentId < t.scope.tournamentId ? -1 : +(e.scope.tournamentId > t.scope.tournamentId));
|
|
295
|
+
}, ee = (e) => {
|
|
296
|
+
let t = A(N, e);
|
|
297
|
+
for (let e of [
|
|
298
|
+
"roomId",
|
|
299
|
+
"matchId",
|
|
300
|
+
"reportId",
|
|
301
|
+
"winnerId"
|
|
302
|
+
]) D(t[e]);
|
|
303
|
+
return r(t, 2048);
|
|
304
|
+
}, Q = (e, t) => e.roomId === t.roomId && e.epoch === t.epoch && e.reportId === t.reportId && e.reportedWinnerId === t.winnerId && e.matchId === t.matchId, $ = (e, t) => {
|
|
305
|
+
J(e), e.phase !== "active" && E("conflict");
|
|
306
|
+
let n = B(e, e.results).matches.find((e) => e.id === t.matchId), r = e.provisions.find((e) => e.matchId === t.matchId);
|
|
307
|
+
n && n.status === "ready" && [n.left, n.right].includes(t.winnerId) && r && r.roomId === t.roomId && r.epoch === t.epoch || E("conflict");
|
|
308
|
+
};
|
|
309
|
+
return Object.freeze({
|
|
310
|
+
create: (e, t, r) => I(() => {
|
|
311
|
+
let i = k(t), o = L(e, i, !0), s = U(r);
|
|
312
|
+
return u(o, i, "create").pipe(f.flatMap(() => j(async () => {
|
|
313
|
+
for (let e of s.entrants.map((e) => e.entrantId).sort()) await S(i.tenantId, e);
|
|
314
|
+
return a.transactional(async (e) => {
|
|
315
|
+
for (let t of s.entrants.map((e) => e.entrantId).sort()) await C(e, i.tenantId, t);
|
|
316
|
+
let t = O(l()), r = {
|
|
317
|
+
...s,
|
|
318
|
+
version: 1,
|
|
319
|
+
phase: "active",
|
|
320
|
+
withdrawn: !1,
|
|
321
|
+
provisions: [],
|
|
322
|
+
results: [],
|
|
323
|
+
disputes: [],
|
|
324
|
+
settlement: null,
|
|
325
|
+
erase: null
|
|
326
|
+
};
|
|
327
|
+
await e.insertIgnore(w, {
|
|
328
|
+
...i,
|
|
329
|
+
state: r,
|
|
330
|
+
createdAt: new Date(t),
|
|
331
|
+
updatedAt: new Date(t)
|
|
332
|
+
}, { conflictColumns: ["tenantId", "tournamentId"] });
|
|
333
|
+
let a = (await H(e, i)).state;
|
|
334
|
+
return n({
|
|
335
|
+
format: a.format,
|
|
336
|
+
entrants: a.entrants,
|
|
337
|
+
...a.rounds === void 0 ? {} : { rounds: a.rounds },
|
|
338
|
+
ruleVersion: a.ruleVersion
|
|
339
|
+
}) !== n(s) && E("conflict"), J(a), Y(a);
|
|
340
|
+
});
|
|
341
|
+
})));
|
|
342
|
+
}),
|
|
343
|
+
report: (e, t, n) => I(() => {
|
|
344
|
+
let r = k(t), i = L(e, r, !0), o = ee(n);
|
|
345
|
+
return u(i, r, "report").pipe(f.flatMap(() => j(async () => {
|
|
346
|
+
let { state: e } = await H(a, r);
|
|
347
|
+
J(e);
|
|
348
|
+
let t = e.results.find((e) => e.matchId === o.matchId);
|
|
349
|
+
return t ? (Q(t, o) || E("conflict"), Y(e)) : ($(e, o), null);
|
|
350
|
+
})), f.flatMap((e) => e === null ? p(i, r, o).pipe(f.flatMap(() => X(r, async (e, t, n) => {
|
|
351
|
+
J(n);
|
|
352
|
+
let r = n.results.find((e) => e.matchId === o.matchId);
|
|
353
|
+
if (r) return Q(r, o) || E("conflict"), Y(n);
|
|
354
|
+
$(n, o);
|
|
355
|
+
let i = {
|
|
356
|
+
...n,
|
|
357
|
+
results: [...n.results, {
|
|
358
|
+
...o,
|
|
359
|
+
reportedWinnerId: o.winnerId
|
|
360
|
+
}]
|
|
361
|
+
};
|
|
362
|
+
return await q(e, t, i), Y(i);
|
|
363
|
+
}))) : f.succeed(e)));
|
|
364
|
+
}),
|
|
365
|
+
dispute: (e, t, n) => I(() => {
|
|
366
|
+
let r = k(t), i = L(e, r, !1), a = A(P, n);
|
|
367
|
+
return D(a.disputeId), D(a.matchId), u(i, r, "dispute").pipe(f.flatMap(() => X(r, async (e, t, n) => {
|
|
368
|
+
J(n);
|
|
369
|
+
let r = n.disputes.find((e) => e.disputeId === a.disputeId);
|
|
370
|
+
if (r) return (r.ownerId !== i.id || r.matchId !== a.matchId || r.reason !== a.reason) && E("conflict"), Y(n);
|
|
371
|
+
let o = n.provisions.find((e) => e.matchId === a.matchId);
|
|
372
|
+
n.settlement === null && o && [o.left, o.right].includes(i.id) && n.results.some((e) => e.matchId === a.matchId) && !n.disputes.some((e) => e.matchId === a.matchId && e.ownerId === i.id) || E("conflict");
|
|
373
|
+
let s = {
|
|
374
|
+
...n,
|
|
375
|
+
phase: "paused",
|
|
376
|
+
disputes: [...n.disputes, {
|
|
377
|
+
...a,
|
|
378
|
+
ownerId: i.id,
|
|
379
|
+
resolutionId: null,
|
|
380
|
+
winnerId: null
|
|
381
|
+
}]
|
|
382
|
+
};
|
|
383
|
+
return await q(e, t, s), Y(s);
|
|
384
|
+
})));
|
|
385
|
+
}),
|
|
386
|
+
resolve: (e, t, n) => I(() => {
|
|
387
|
+
let r = k(t), i = L(e, r, !0), a = A(F, n);
|
|
388
|
+
return D(a.disputeId), D(a.resolutionId), D(a.winnerId), u(i, r, "resolve").pipe(f.flatMap(() => X(r, async (e, t, n) => {
|
|
389
|
+
J(n);
|
|
390
|
+
let r = n.disputes.find((e) => e.disputeId === a.disputeId);
|
|
391
|
+
if (r || E("not_found"), r.resolutionId !== null) return (r.resolutionId !== a.resolutionId || r.winnerId !== a.winnerId) && E("conflict"), Y(n);
|
|
392
|
+
(n.phase !== "paused" || n.settlement !== null || n.disputes.some((e) => e.resolutionId === a.resolutionId)) && E("conflict");
|
|
393
|
+
let i = n.disputes.map((e) => e.disputeId === a.disputeId ? {
|
|
394
|
+
...e,
|
|
395
|
+
resolutionId: a.resolutionId,
|
|
396
|
+
winnerId: a.winnerId
|
|
397
|
+
} : e), o = {
|
|
398
|
+
...n,
|
|
399
|
+
disputes: i,
|
|
400
|
+
phase: i.some((e) => e.resolutionId === null) ? "paused" : "active",
|
|
401
|
+
results: n.results.map((e) => e.matchId === r.matchId ? {
|
|
402
|
+
...e,
|
|
403
|
+
winnerId: a.winnerId
|
|
404
|
+
} : e)
|
|
405
|
+
};
|
|
406
|
+
return await q(e, t, o), Y(o);
|
|
407
|
+
})));
|
|
408
|
+
}),
|
|
409
|
+
advance: (n, r) => I(() => {
|
|
410
|
+
let i = k(r), o = L(n, i, !0);
|
|
411
|
+
return u(o, i, "advance").pipe(f.flatMap(() => X(i, async (e, t, n) => {
|
|
412
|
+
if (J(n), n.phase === "complete") return { kind: "idle" };
|
|
413
|
+
n.phase !== "active" && E("conflict");
|
|
414
|
+
let r = B(n, n.results);
|
|
415
|
+
if (r.champion !== null) {
|
|
416
|
+
let a = n.settlement ?? {
|
|
417
|
+
requestId: z(i, n.ruleVersion, "champion"),
|
|
418
|
+
winnerId: r.champion,
|
|
419
|
+
complete: !1
|
|
420
|
+
};
|
|
421
|
+
return n.settlement === null && await q(e, t, {
|
|
422
|
+
...n,
|
|
423
|
+
settlement: a
|
|
424
|
+
}), {
|
|
425
|
+
kind: "settle",
|
|
426
|
+
work: {
|
|
427
|
+
...i,
|
|
428
|
+
requestId: a.requestId,
|
|
429
|
+
winnerId: a.winnerId,
|
|
430
|
+
ruleVersion: n.ruleVersion
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
let a = r.matches.find((e) => e.status === "ready" && !n.provisions.some((t) => t.matchId === e.id && t.roomId !== null));
|
|
435
|
+
if (!a || a.left === null || a.right === null) return { kind: "idle" };
|
|
436
|
+
let o = n.provisions.find((e) => e.matchId === a.id), s = o ?? {
|
|
437
|
+
...i,
|
|
438
|
+
matchId: a.id,
|
|
439
|
+
requestId: z(i, n.ruleVersion, "room:" + a.id),
|
|
440
|
+
left: a.left,
|
|
441
|
+
right: a.right,
|
|
442
|
+
ruleVersion: n.ruleVersion,
|
|
443
|
+
roomId: null,
|
|
444
|
+
epoch: null
|
|
445
|
+
};
|
|
446
|
+
return o || await q(e, t, {
|
|
447
|
+
...n,
|
|
448
|
+
provisions: [...n.provisions, s]
|
|
449
|
+
}), {
|
|
450
|
+
kind: "room",
|
|
451
|
+
work: {
|
|
452
|
+
...i,
|
|
453
|
+
matchId: s.matchId,
|
|
454
|
+
requestId: s.requestId,
|
|
455
|
+
left: s.left,
|
|
456
|
+
right: s.right,
|
|
457
|
+
ruleVersion: s.ruleVersion
|
|
458
|
+
}
|
|
459
|
+
};
|
|
460
|
+
})), f.flatMap((n) => n.kind === "idle" ? j(async () => Y((await H(a, i)).state)) : n.kind === "settle" ? g(n.work).pipe(f.flatMap(() => X(i, async (e, t, r) => {
|
|
461
|
+
if (J(r), r.phase === "complete" && r.settlement?.requestId === n.work.requestId) return Y(r);
|
|
462
|
+
(r.phase !== "active" || r.settlement?.requestId !== n.work.requestId || r.settlement.winnerId !== n.work.winnerId) && E("conflict");
|
|
463
|
+
let i = {
|
|
464
|
+
...r,
|
|
465
|
+
phase: "complete",
|
|
466
|
+
settlement: {
|
|
467
|
+
...r.settlement,
|
|
468
|
+
complete: !0
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
return await q(e, t, i), Y(i);
|
|
472
|
+
}))) : d(n.work).pipe(f.flatMap((n) => f.try({
|
|
473
|
+
try: () => {
|
|
474
|
+
let e = A(M, n);
|
|
475
|
+
return D(e.roomId), e;
|
|
476
|
+
},
|
|
477
|
+
catch: (n) => n instanceof e ? n : t("invalid_input")
|
|
478
|
+
})), f.flatMap((e) => X(i, async (t, r, i) => {
|
|
479
|
+
J(i);
|
|
480
|
+
let a = i.provisions.find((e) => e.requestId === n.work.requestId);
|
|
481
|
+
if (i.phase === "active" && a || E("conflict"), a.roomId !== null) return (a.roomId !== e.roomId || a.epoch !== e.epoch) && E("conflict"), Y(i);
|
|
482
|
+
let o = {
|
|
483
|
+
...i,
|
|
484
|
+
provisions: i.provisions.map((t) => t.requestId === n.work.requestId ? {
|
|
485
|
+
...t,
|
|
486
|
+
...e
|
|
487
|
+
} : t)
|
|
488
|
+
};
|
|
489
|
+
return await q(t, r, o), Y(o);
|
|
490
|
+
})))));
|
|
491
|
+
}),
|
|
492
|
+
status: (e, t) => I(() => {
|
|
493
|
+
let n = k(t), r = L(e, n, !0);
|
|
494
|
+
return u(r, n, "status").pipe(f.flatMap(() => j(async () => Y((await H(a, n)).state))));
|
|
495
|
+
}),
|
|
496
|
+
exportSubject: (e) => I(() => {
|
|
497
|
+
let t = V(e);
|
|
498
|
+
return x(t).pipe(f.flatMap(() => j(async () => (await Z(t.target.tenantId)).flatMap(({ scope: e, state: n }) => {
|
|
499
|
+
let r = n.entrants.find((e) => e.entrantId === t.target.subjectId);
|
|
500
|
+
return r ? [{
|
|
501
|
+
tournamentId: e.tournamentId,
|
|
502
|
+
phase: n.phase,
|
|
503
|
+
seed: r.seed,
|
|
504
|
+
matches: n.results.filter((e) => n.provisions.some((t) => t.matchId === e.matchId && [t.left, t.right].includes(r.entrantId))).map((e) => ({
|
|
505
|
+
matchId: e.matchId,
|
|
506
|
+
roomId: e.roomId,
|
|
507
|
+
won: e.winnerId === r.entrantId
|
|
508
|
+
})),
|
|
509
|
+
disputes: n.disputes.filter((e) => e.ownerId === r.entrantId).map((e) => ({
|
|
510
|
+
disputeId: e.disputeId,
|
|
511
|
+
matchId: e.matchId,
|
|
512
|
+
reason: e.reason,
|
|
513
|
+
resolutionId: e.resolutionId
|
|
514
|
+
}))
|
|
515
|
+
}] : [];
|
|
516
|
+
}))));
|
|
517
|
+
}),
|
|
518
|
+
eraseSubject: (e) => I(() => {
|
|
519
|
+
let t = V(e), n = R(t.target);
|
|
520
|
+
return y(t).pipe(f.flatMap(() => j(() => T(t.target.tenantId, t.target.subjectId))), f.flatMap(() => j(() => Z(t.target.tenantId))), f.flatMap((e) => f.forEach(e.filter(({ state: e }) => e.entrants.some((e) => e.entrantId === t.target.subjectId) || e.erase?.requestId === t.requestId && e.erase.targetDigest === n), ({ scope: e }) => X(e, async (e, r, i) => i.phase === "cancelled" && i.erase?.requestId === t.requestId && i.erase.targetDigest === n && !i.entrants.some((e) => e.entrantId === t.target.subjectId) || (i.phase === "erasing" && (i.erase?.requestId !== t.requestId || i.erase.targetDigest !== n) && E("conflict"), i.entrants.some((e) => e.entrantId === t.target.subjectId) || E("unavailable"), await q(e, r, {
|
|
521
|
+
...i,
|
|
522
|
+
phase: "erasing",
|
|
523
|
+
erase: {
|
|
524
|
+
requestId: t.requestId,
|
|
525
|
+
targetDigest: n
|
|
526
|
+
}
|
|
527
|
+
}), !1), !0).pipe(f.flatMap((r) => r ? f.void : b(e, t).pipe(f.flatMap(() => X(e, async (e, r, i) => {
|
|
528
|
+
(i.phase !== "erasing" || i.erase?.requestId !== t.requestId || i.erase.targetDigest !== n) && E("conflict"), await q(e, r, {
|
|
529
|
+
...i,
|
|
530
|
+
phase: "cancelled",
|
|
531
|
+
withdrawn: !0,
|
|
532
|
+
entrants: i.entrants.filter((e) => e.entrantId !== t.target.subjectId),
|
|
533
|
+
provisions: [],
|
|
534
|
+
results: [],
|
|
535
|
+
disputes: [],
|
|
536
|
+
settlement: null
|
|
537
|
+
});
|
|
538
|
+
}, !0))))), {
|
|
539
|
+
concurrency: 1,
|
|
540
|
+
discard: !0
|
|
541
|
+
})));
|
|
542
|
+
})
|
|
543
|
+
});
|
|
544
|
+
}, J = (e, t) => p.succeed(a, q(e, t));
|
|
545
|
+
export { w as TOURNAMENT_BRACKETS_TABLE, q as dataStoreTournamentBrackets, K as resolveTournamentBracketOptions, J as tournamentBracketLayer, T as tournamentBracketsTable };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export declare const advanceDoubleElimination: (input: DoubleEliminationBracket, inputResults: ReadonlyArray<BracketResult>, config?: Partial<TournamentPairingLimits>) => DoubleEliminationProgress;
|
|
2
|
+
|
|
3
|
+
/** Round robin as a bracket: every pair once (circle schedule), all matches ready from the
|
|
4
|
+
* start. The champion is known once every match is complete (a win is one point). */
|
|
5
|
+
export declare const advanceRoundRobin: (input: ReadonlyArray<TournamentEntrant>, inputResults: ReadonlyArray<BracketResult>, config?: Partial<TournamentPairingLimits>) => BracketProgress;
|
|
6
|
+
|
|
7
|
+
/** Rebuilds and verifies the canonical graph from its roster before advancing.
|
|
8
|
+
* Results are trusted server facts supplied by a later SQL/Room coordinator. */
|
|
9
|
+
export declare const advanceSingleElimination: (input: SingleEliminationBracket, inputResults: ReadonlyArray<BracketResult>, config?: Partial<TournamentPairingLimits>) => BracketProgress;
|
|
10
|
+
|
|
11
|
+
/** Swiss as a bracket: round r is paired (no rematches, close scores first) once round r-1 is
|
|
12
|
+
* complete; an odd roster gives one bye per round (a walkover, worth a win). After `rounds`
|
|
13
|
+
* complete rounds the champion is the points leader. */
|
|
14
|
+
export declare const advanceSwiss: (input: ReadonlyArray<TournamentEntrant>, inputResults: ReadonlyArray<BracketResult>, rounds: number, config?: Partial<TournamentPairingLimits>) => BracketProgress;
|
|
15
|
+
|
|
16
|
+
export declare interface BracketMatchObservation {
|
|
17
|
+
readonly id: string;
|
|
18
|
+
readonly round: number;
|
|
19
|
+
readonly left: string | null;
|
|
20
|
+
readonly right: string | null;
|
|
21
|
+
readonly status: 'pending' | 'ready' | 'walkover' | 'complete';
|
|
22
|
+
readonly winnerId: string | null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export declare interface BracketProgress {
|
|
26
|
+
readonly matches: ReadonlyArray<BracketMatchObservation>;
|
|
27
|
+
readonly champion: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export declare interface BracketResult {
|
|
31
|
+
readonly matchId: string;
|
|
32
|
+
readonly winnerId: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export declare type BracketSlot = {
|
|
36
|
+
readonly kind: 'entrant';
|
|
37
|
+
readonly entrantId: string;
|
|
38
|
+
} | {
|
|
39
|
+
readonly kind: 'winner';
|
|
40
|
+
readonly matchId: string;
|
|
41
|
+
} | {
|
|
42
|
+
readonly kind: 'bye';
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export declare interface DoubleEliminationBracket {
|
|
46
|
+
readonly format: 'double-elimination';
|
|
47
|
+
readonly version: 1;
|
|
48
|
+
readonly entrants: ReadonlyArray<TournamentEntrant>;
|
|
49
|
+
readonly matches: ReadonlyArray<DoubleEliminationMatch>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export declare interface DoubleEliminationMatch {
|
|
53
|
+
readonly id: string;
|
|
54
|
+
readonly stage: 'winners' | 'losers' | 'final' | 'reset';
|
|
55
|
+
readonly round: number;
|
|
56
|
+
readonly left: DoubleEliminationSlot;
|
|
57
|
+
readonly right: DoubleEliminationSlot;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export declare interface DoubleEliminationObservation extends Omit<BracketMatchObservation, 'status'> {
|
|
61
|
+
readonly stage: DoubleEliminationMatch['stage'];
|
|
62
|
+
readonly status: BracketMatchObservation['status'] | 'empty' | 'inactive';
|
|
63
|
+
readonly loserId: string | null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export declare interface DoubleEliminationProgress {
|
|
67
|
+
readonly matches: ReadonlyArray<DoubleEliminationObservation>;
|
|
68
|
+
readonly champion: string | null;
|
|
69
|
+
readonly losses: ReadonlyArray<{
|
|
70
|
+
readonly entrantId: string;
|
|
71
|
+
readonly losses: number;
|
|
72
|
+
}>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export declare type DoubleEliminationSlot = BracketSlot | {
|
|
76
|
+
readonly kind: 'loser';
|
|
77
|
+
readonly matchId: string;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** Circle schedule, fixed first seed, whole roster and one bye per odd round.
|
|
81
|
+
* The return value is a plan; it creates no Room or result authority. */
|
|
82
|
+
export declare const roundRobin: (input: ReadonlyArray<TournamentEntrant>, options?: {
|
|
83
|
+
readonly doubleRound?: boolean;
|
|
84
|
+
readonly limits?: Partial<TournamentPairingLimits>;
|
|
85
|
+
}) => ReadonlyArray<RoundRobinRound>;
|
|
86
|
+
|
|
87
|
+
export declare interface RoundRobinRound {
|
|
88
|
+
readonly round: number;
|
|
89
|
+
readonly pairs: ReadonlyArray<TournamentPair>;
|
|
90
|
+
readonly bye: string | null;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Winners use the same mirrored seeds. Losers alternate cross-dropped winners
|
|
94
|
+
* losses and consolidation rounds. Grand Final resets only when the lower
|
|
95
|
+
* bracket finalist wins its first game against the undefeated finalist. */
|
|
96
|
+
export declare const seedDoubleElimination: (input: ReadonlyArray<TournamentEntrant>, config?: Partial<TournamentPairingLimits>) => DoubleEliminationBracket;
|
|
97
|
+
|
|
98
|
+
export declare const seedSingleElimination: (input: ReadonlyArray<TournamentEntrant>, config?: Partial<TournamentPairingLimits>) => SingleEliminationBracket;
|
|
99
|
+
|
|
100
|
+
export declare interface SingleEliminationBracket {
|
|
101
|
+
readonly format: 'single-elimination';
|
|
102
|
+
readonly version: 1;
|
|
103
|
+
readonly entrants: ReadonlyArray<TournamentEntrant>;
|
|
104
|
+
readonly matches: ReadonlyArray<SingleEliminationMatch>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export declare interface SingleEliminationMatch {
|
|
108
|
+
readonly id: string;
|
|
109
|
+
readonly round: number;
|
|
110
|
+
readonly left: BracketSlot;
|
|
111
|
+
readonly right: BracketSlot;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export declare interface SwissPairing {
|
|
115
|
+
readonly pairs: ReadonlyArray<TournamentPair>;
|
|
116
|
+
readonly bye: string | null;
|
|
117
|
+
readonly searchNodes: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Ordered bounded backtracking. Prioritizes close scores then seed distance;
|
|
121
|
+
* finds the first complete no-rematch assignment, not a federation algorithm or
|
|
122
|
+
* a globally optimal pairing. No partial result escapes budget exhaustion. */
|
|
123
|
+
export declare const swissPairings: (input: ReadonlyArray<SwissStanding>, config?: Partial<TournamentPairingLimits>) => SwissPairing;
|
|
124
|
+
|
|
125
|
+
export declare interface SwissStanding extends TournamentEntrant {
|
|
126
|
+
/** Whole scoring units selected by the studio (for example win2/draw1). */
|
|
127
|
+
readonly points: number;
|
|
128
|
+
readonly byes: number;
|
|
129
|
+
readonly opponents: ReadonlyArray<string>;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export declare interface TournamentEntrant {
|
|
133
|
+
readonly entrantId: string;
|
|
134
|
+
readonly seed: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export declare interface TournamentPair {
|
|
138
|
+
readonly left: string;
|
|
139
|
+
readonly right: string;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export declare interface TournamentPairingLimits {
|
|
143
|
+
readonly maxEntrants: number;
|
|
144
|
+
readonly maxMatches: number;
|
|
145
|
+
readonly maxSearchNodes: number;
|
|
146
|
+
readonly maxInputBytes: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export { }
|