@chaos-overlords/kernel 0.8.1 → 0.9.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/dist/domain/entities.d.ts +31 -1
- package/dist/domain/entities.d.ts.map +1 -1
- package/dist/domain/entities.js +9 -1
- package/dist/domain/entities.js.map +1 -1
- package/dist/domain/errors.d.ts.map +1 -1
- package/dist/domain/errors.js.map +1 -1
- package/dist/logic/canonical-json.d.ts.map +1 -1
- package/dist/logic/canonical-json.js.map +1 -1
- package/dist/logic/turn-logic.d.ts.map +1 -1
- package/dist/logic/turn-logic.js +2 -4
- package/dist/logic/turn-logic.js.map +1 -1
- package/dist/ports/storage.d.ts +130 -12
- package/dist/ports/storage.d.ts.map +1 -1
- package/dist/services/AuthService.d.ts.map +1 -1
- package/dist/services/AuthService.js.map +1 -1
- package/dist/services/EventPublisher.d.ts.map +1 -1
- package/dist/services/EventPublisher.js.map +1 -1
- package/dist/services/LobbyService.d.ts +40 -2
- package/dist/services/LobbyService.d.ts.map +1 -1
- package/dist/services/LobbyService.js +245 -91
- package/dist/services/LobbyService.js.map +1 -1
- package/dist/services/MatchQueryService.d.ts +12 -2
- package/dist/services/MatchQueryService.d.ts.map +1 -1
- package/dist/services/MatchQueryService.js +44 -24
- package/dist/services/MatchQueryService.js.map +1 -1
- package/dist/services/RateLimiter.d.ts +10 -0
- package/dist/services/RateLimiter.d.ts.map +1 -1
- package/dist/services/RateLimiter.js +48 -5
- package/dist/services/RateLimiter.js.map +1 -1
- package/dist/services/RetentionService.d.ts +15 -0
- package/dist/services/RetentionService.d.ts.map +1 -1
- package/dist/services/RetentionService.js +20 -23
- package/dist/services/RetentionService.js.map +1 -1
- package/dist/services/SnapshotService.d.ts +55 -9
- package/dist/services/SnapshotService.d.ts.map +1 -1
- package/dist/services/SnapshotService.js +120 -41
- package/dist/services/SnapshotService.js.map +1 -1
- package/dist/services/TurnService.d.ts +23 -7
- package/dist/services/TurnService.d.ts.map +1 -1
- package/dist/services/TurnService.js +217 -76
- package/dist/services/TurnService.js.map +1 -1
- package/dist/services/createKernel.js.map +1 -1
- package/dist/services/guards.d.ts +9 -0
- package/dist/services/guards.d.ts.map +1 -0
- package/dist/services/guards.js +22 -0
- package/dist/services/guards.js.map +1 -0
- package/dist/testing/CountingStorage.d.ts +25 -0
- package/dist/testing/CountingStorage.d.ts.map +1 -0
- package/dist/testing/CountingStorage.js +51 -0
- package/dist/testing/CountingStorage.js.map +1 -0
- package/dist/testing/InMemoryStorage.d.ts.map +1 -1
- package/dist/testing/InMemoryStorage.js +83 -17
- package/dist/testing/InMemoryStorage.js.map +1 -1
- package/dist/testing/fakes.d.ts.map +1 -1
- package/dist/testing/fakes.js.map +1 -1
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +1 -0
- package/dist/testing/index.js.map +1 -1
- package/package.json +4 -4
|
@@ -2,6 +2,7 @@ import { gameSettingsSchema, } from '@chaos-overlords/contracts';
|
|
|
2
2
|
import { safeParse } from 'valibot';
|
|
3
3
|
import { ConflictError, ForbiddenError, NotFoundError } from '../domain/errors';
|
|
4
4
|
import { authoritativeCandidates } from '../logic/turn-logic';
|
|
5
|
+
import { requireInProgress, requireParticipant, requireTurn } from './guards';
|
|
5
6
|
/**
|
|
6
7
|
* Snapshots kept per live match. Enough to cover a desync being repaired while an earlier one is
|
|
7
8
|
* still being fetched by a straggler, and far fewer than a long match would otherwise accumulate.
|
|
@@ -23,15 +24,16 @@ export class SnapshotService {
|
|
|
23
24
|
}
|
|
24
25
|
async upload(principal, request) {
|
|
25
26
|
const { match, player } = principal;
|
|
26
|
-
if (player.id !== match.hostPlayerId) {
|
|
27
|
-
throw new ForbiddenError('Only the host uploads snapshots', { reason: 'host_only' });
|
|
28
|
-
}
|
|
29
27
|
const isBootstrap = request.turn === 0 && match.status === 'running' && match.currentTurn === 1;
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
// The bootstrap snapshot is the host's alone: it is unconstrained (nothing has been reported
|
|
29
|
+
// yet) and it is the match's starting state, which only the host has.
|
|
30
|
+
if (isBootstrap && player.id !== match.hostPlayerId) {
|
|
31
|
+
throw new ForbiddenError('Only the host uploads the starting snapshot', {
|
|
32
|
+
reason: 'host_only',
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
|
+
requireParticipant(player);
|
|
36
|
+
requireInProgress(match);
|
|
35
37
|
if (request.turn > match.currentTurn) {
|
|
36
38
|
throw new ConflictError('Cannot snapshot a turn that has not opened', {
|
|
37
39
|
reason: 'future_turn',
|
|
@@ -42,38 +44,85 @@ export class SnapshotService {
|
|
|
42
44
|
reason: 'turn_open',
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
if (isBootstrap) {
|
|
48
|
+
// Nothing has been reported yet, so there is no consensus a bootstrap could contradict.
|
|
49
|
+
await this.store(match, player.id, request);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const turn = await requireTurn(this.deps.storage.turns, match.id, request.turn);
|
|
53
|
+
if (turn.status === 'confirmed') {
|
|
54
|
+
await this.storeCheckpoint(match, player, turn, request);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
this.requireDesyncedTurn(turn);
|
|
58
|
+
await this.requireRepairAuthority(match, player, request.turn, request.stateHash);
|
|
59
|
+
await this.store(match, player.id, request);
|
|
60
|
+
await this.publisher.publish(match.id, {
|
|
61
|
+
type: 'snapshot.available',
|
|
62
|
+
payload: {
|
|
63
|
+
turn: request.turn,
|
|
64
|
+
formatVersion: request.formatVersion,
|
|
65
|
+
stateHash: request.stateHash,
|
|
66
|
+
uploadedByPlayerId: player.id,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
await this.turns.settle(match.id, request.turn);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* A periodic checkpoint of a turn the match has already agreed on.
|
|
73
|
+
*
|
|
74
|
+
* Only two kinds of snapshot used to exist, the host's turn-0 bootstrap and desync repairs, so a
|
|
75
|
+
* client reconnecting at turn 80 replayed eighty turns from the bootstrap — eighty sequential
|
|
76
|
+
* fetches, each under its own deadline, and eighty full turn resolutions, before the player saw
|
|
77
|
+
* anything. The design doc talks about matches of hundreds of turns.
|
|
78
|
+
*
|
|
79
|
+
* It is not a second way to say what a turn's state was. The turn is CONFIRMED, so the verdict
|
|
80
|
+
* has already been reached and written on the turn row, and the checkpoint is refused unless it
|
|
81
|
+
* claims exactly that hash: it can only agree with what every client already computed. The host
|
|
82
|
+
* writes it because the host is the one client the protocol already asks for bytes, storage stays
|
|
83
|
+
* bounded by the per-match snapshot pruning, and the rate stays bounded by the upload budget.
|
|
84
|
+
*
|
|
85
|
+
* Nothing is announced. A checkpoint changes nothing about the match; it is there for whoever
|
|
86
|
+
* reconnects next, and `getLatest` is what finds it.
|
|
87
|
+
*/
|
|
88
|
+
async storeCheckpoint(match, player, turn, request) {
|
|
89
|
+
if (player.id !== match.hostPlayerId) {
|
|
90
|
+
throw new ForbiddenError('Only the host uploads checkpoints', { reason: 'host_only' });
|
|
91
|
+
}
|
|
92
|
+
if (turn.stateHash !== request.stateHash) {
|
|
93
|
+
throw new ConflictError('A checkpoint must be the state the turn was confirmed on', {
|
|
94
|
+
reason: 'uncorroborated_state_hash',
|
|
95
|
+
candidateStateHashes: turn.stateHash === null ? [] : [turn.stateHash],
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
await this.store(match, player.id, request);
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Write an accepted snapshot: the row, the seat summaries it carries, then the prune.
|
|
102
|
+
*
|
|
103
|
+
* The merge is judged before anything is written: a refusal after the row landed would leave a
|
|
104
|
+
* stored snapshot the caller was told was rejected, with no `snapshot.available` and no verdict.
|
|
105
|
+
* Re-running the settings schema over the merge is what holds the blob to its cap, which is why
|
|
106
|
+
* every upload path comes through here before its write.
|
|
107
|
+
*/
|
|
108
|
+
async store(match, uploadedByPlayerId, request) {
|
|
50
109
|
const gameSettings = mergeSeatSummaries(match.settings.gameSettings, request.seatSummaries);
|
|
51
|
-
|
|
110
|
+
await this.deps.storage.snapshots.put(this.snapshotOf(match, uploadedByPlayerId, request));
|
|
111
|
+
await this.publishSeatSummaries(match.id, gameSettings);
|
|
112
|
+
await this.pruneOldSnapshots(match.id);
|
|
113
|
+
}
|
|
114
|
+
snapshotOf(match, uploadedByPlayerId, request) {
|
|
115
|
+
return {
|
|
52
116
|
matchId: match.id,
|
|
53
117
|
turn: request.turn,
|
|
54
118
|
formatVersion: request.formatVersion,
|
|
55
119
|
protocolVersion: request.protocolVersion ?? 1,
|
|
56
120
|
sessionVersion: request.sessionVersion ?? 1,
|
|
57
121
|
stateHash: request.stateHash,
|
|
58
|
-
uploadedByPlayerId
|
|
122
|
+
uploadedByPlayerId,
|
|
59
123
|
uploadedAt: this.deps.clock.now(),
|
|
60
124
|
body: request.body,
|
|
61
125
|
};
|
|
62
|
-
await this.deps.storage.snapshots.put(snapshot);
|
|
63
|
-
await this.publishSeatSummaries(match.id, gameSettings);
|
|
64
|
-
await this.pruneOldSnapshots(match.id);
|
|
65
|
-
if (match.status === 'desynced') {
|
|
66
|
-
await this.publisher.publish(match.id, {
|
|
67
|
-
type: 'snapshot.available',
|
|
68
|
-
payload: {
|
|
69
|
-
turn: request.turn,
|
|
70
|
-
formatVersion: request.formatVersion,
|
|
71
|
-
stateHash: request.stateHash,
|
|
72
|
-
uploadedByPlayerId: player.id,
|
|
73
|
-
},
|
|
74
|
-
});
|
|
75
|
-
await this.turns.settle(match.id, request.turn);
|
|
76
|
-
}
|
|
77
126
|
}
|
|
78
127
|
/**
|
|
79
128
|
* Late-join hints are optional metadata; the snapshot the players are waiting on is not. A
|
|
@@ -88,25 +137,55 @@ export class SnapshotService {
|
|
|
88
137
|
}
|
|
89
138
|
}
|
|
90
139
|
/**
|
|
91
|
-
* A
|
|
92
|
-
* in the greatest number.
|
|
140
|
+
* A repair must name the turn that actually diverged.
|
|
93
141
|
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
142
|
+
* "Below `currentTurn` while the match is desynced" was too loose, and the corroboration check
|
|
143
|
+
* below cannot make up the difference: it counts reports, and a turn with none — the successor a
|
|
144
|
+
* seal opened before anybody had reported on it — has no candidates to contradict, so it returned
|
|
145
|
+
* early and let the host store any hash it liked for that turn. `evaluateConsensus` then had an
|
|
146
|
+
* authoritative hash to judge against and could only confirm or wait, never desync. That is
|
|
147
|
+
* exactly the "host arbitrates a disagreement it is a party to" case the corroboration rule
|
|
148
|
+
* exists to prevent; it needs the turn's own status to close it.
|
|
100
149
|
*/
|
|
101
|
-
|
|
150
|
+
requireDesyncedTurn(turn) {
|
|
151
|
+
if (turn.status === 'desynced')
|
|
152
|
+
return;
|
|
153
|
+
throw new ConflictError('Only a turn that diverged may be repaired with a snapshot', {
|
|
154
|
+
reason: 'turn_not_desynced',
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Who may post a repair, and which hash they may claim.
|
|
159
|
+
*
|
|
160
|
+
* The hash is the hard rule and it binds everyone: the snapshot for a desynced turn becomes the
|
|
161
|
+
* state every other client is told to converge on, so a client free to name any hash could
|
|
162
|
+
* resolve a deliberate desync in favour of a doctored one. A hash that more active players
|
|
163
|
+
* reported than any other cannot be minted by one of them.
|
|
164
|
+
*
|
|
165
|
+
* Who may post it follows from that. A hash that is the SOLE most-reported one is already the
|
|
166
|
+
* majority's, so whoever holds it may post the repair whether or not they are the host. That is
|
|
167
|
+
* the one case a host-only rule could not serve at all: a host that is itself the odd one out can
|
|
168
|
+
* never name a majority hash, so no repair existed and the match stayed paused until retention
|
|
169
|
+
* collected it. A TIE — most of all the 1-1 split of a two-player match — is different: there is
|
|
170
|
+
* no majority, nothing to count, and no third party to ask, so the host breaks it as before and a
|
|
171
|
+
* peer may not. Letting either side of a tie impose its state would hand a two-player match to
|
|
172
|
+
* whoever uploaded first.
|
|
173
|
+
*/
|
|
174
|
+
async requireRepairAuthority(match, player, turn, stateHash) {
|
|
102
175
|
const [players, reports] = await Promise.all([
|
|
103
|
-
this.deps.storage.players.listByMatch(
|
|
104
|
-
this.deps.storage.turns.listReports(
|
|
176
|
+
this.deps.storage.players.listByMatch(match.id),
|
|
177
|
+
this.deps.storage.turns.listReports(match.id, turn),
|
|
105
178
|
]);
|
|
106
179
|
const candidates = authoritativeCandidates(players, reports);
|
|
107
|
-
if (candidates.
|
|
180
|
+
if (!candidates.includes(stateHash)) {
|
|
181
|
+
throw new ConflictError('A recovery snapshot must match a state hash the players reported most often', { reason: 'uncorroborated_state_hash', candidateStateHashes: candidates });
|
|
182
|
+
}
|
|
183
|
+
if (candidates.length === 1 || player.id === match.hostPlayerId)
|
|
108
184
|
return;
|
|
109
|
-
throw new
|
|
185
|
+
throw new ForbiddenError('Only the host may break a tie between reported states', {
|
|
186
|
+
reason: 'host_only',
|
|
187
|
+
candidateStateHashes: candidates,
|
|
188
|
+
});
|
|
110
189
|
}
|
|
111
190
|
/**
|
|
112
191
|
* Bound what one live match holds. A snapshot is a megabyte of base64 and a long match can desync
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SnapshotService.js","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,kBAAkB,GAGnB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAA;AAEnC,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAA;AAC/E,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAA;
|
|
1
|
+
{"version":3,"file":"SnapshotService.js","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"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"}
|
|
@@ -20,6 +20,8 @@ export declare const FIRST_TURN = 1;
|
|
|
20
20
|
export declare class TurnService {
|
|
21
21
|
private readonly deps;
|
|
22
22
|
private readonly publisher;
|
|
23
|
+
/** Passes since the last unbounded scan; see `sweep`. Starts due so the first pass is a full one. */
|
|
24
|
+
private passesSinceFullScan;
|
|
23
25
|
constructor(deps: KernelDeps, publisher: EventPublisher);
|
|
24
26
|
submitOrders(principal: Principal, number: number, request: SubmitOrdersRequest): Promise<OwnSubmissionView>;
|
|
25
27
|
/** Seal `number` if its trigger condition holds. Returns whether THIS call sealed it. */
|
|
@@ -39,7 +41,14 @@ export declare class TurnService {
|
|
|
39
41
|
* the announcement only when the log does not already carry it.
|
|
40
42
|
*/
|
|
41
43
|
private repairInterruptedStart;
|
|
42
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Whether the log carries an event of `type`.
|
|
46
|
+
*
|
|
47
|
+
* Only the interrupted-start repair asks this, and only about turn 1, so the log it pages is a
|
|
48
|
+
* handful of rows. The verdict used to ask it about `turn.desynced` on every sweep of a paused
|
|
49
|
+
* match, where the log is the whole match; that answer lives on the turn row now
|
|
50
|
+
* (`claimDesyncAnnouncement`), and this must not grow another steady-state caller.
|
|
51
|
+
*/
|
|
43
52
|
private hasEvent;
|
|
44
53
|
/**
|
|
45
54
|
* Opens turn `number` for every active player and arms its deadline. Idempotent: the insert is
|
|
@@ -56,6 +65,7 @@ export declare class TurnService {
|
|
|
56
65
|
sealed: number;
|
|
57
66
|
repaired: number;
|
|
58
67
|
}>;
|
|
68
|
+
private guard;
|
|
59
69
|
report(principal: Principal, number: number, request: TurnReportRequest): Promise<void>;
|
|
60
70
|
/** Authenticated turn activity wins the race with an AI vote and restores the human seat. */
|
|
61
71
|
private restorePendingPlayer;
|
|
@@ -71,19 +81,25 @@ export declare class TurnService {
|
|
|
71
81
|
* seats it finds absent, or by a vote on a seat that went quiet before anyone was present to ask,
|
|
72
82
|
* is exactly as blocking as one raised by a kick, and each of those used to leave the countdown
|
|
73
83
|
* running. `openTurn` applies the same rule to a turn opening while a prompt is already up.
|
|
84
|
+
*
|
|
85
|
+
* The pause is a no-op when there is no open turn to stop (during a seal, where the turn the
|
|
86
|
+
* absence was noticed on is already sealed and its successor opens paused instead) and when the
|
|
87
|
+
* clock is stopped already, so a second absent seat neither double-publishes nor disturbs the
|
|
88
|
+
* first pause.
|
|
74
89
|
*/
|
|
75
90
|
openTakeoverPrompt(matchId: string, playerId: string, turn: number): Promise<boolean>;
|
|
76
91
|
/**
|
|
77
|
-
*
|
|
92
|
+
* Stop the clock of a match whose last active player has just left.
|
|
78
93
|
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* noticed on is already sealed and its successor opens paused instead — and when the clock is
|
|
82
|
-
* stopped already, so a second absent seat neither double-publishes nor disturbs the first pause.
|
|
94
|
+
* There is nobody left to ask about the empty seat, so no prompt is opened and nothing else would
|
|
95
|
+
* pause the turn. `resumeAfterTakeoverVotes`, which `rejoin` calls, restarts it.
|
|
83
96
|
*/
|
|
84
|
-
|
|
97
|
+
pauseAbandonedMatch(matchId: string): Promise<void>;
|
|
98
|
+
private clearOpenDeadline;
|
|
85
99
|
/** Restart a full turn clock after the final outstanding absence vote closes. */
|
|
86
100
|
resumeAfterTakeoverVotes(matchId: string): Promise<void>;
|
|
101
|
+
/** Tell clients the open turn's restarted clock, then arm the timer that seals it. */
|
|
102
|
+
private announceDeadline;
|
|
87
103
|
/** Re-run the verdict of every unconfirmed turn and the auto-seal of the open one. */
|
|
88
104
|
reevaluate(matchId: string): Promise<void>;
|
|
89
105
|
/** Decide turn `number` from the reports on file. Idempotent and race-safe through CAS. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TurnService.d.ts","sourceRoot":"","sources":["../../src/services/TurnService.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACvB,MAAM,4BAA4B,CAAA;AACnC,OAAO,
|
|
1
|
+
{"version":3,"file":"TurnService.d.ts","sourceRoot":"","sources":["../../src/services/TurnService.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,EACxB,KAAK,iBAAiB,EACvB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAIL,KAAK,KAAK,EAEX,MAAM,oBAAoB,CAAA;AAI3B,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAA;AAC9C,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AACxC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAKtD,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,UAAU,CAAA;AAE9C,yFAAyF;AACzF,eAAO,MAAM,UAAU,IAAI,CAAA;AAmB3B;;;;;;;;;;GAUG;AACH,qBAAa,WAAW;IAKpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAL5B,qGAAqG;IACrG,OAAO,CAAC,mBAAmB,CAAkB;IAE7C,YACmB,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,cAAc,EACxC;IAEE,YAAY,CAChB,SAAS,EAAE,SAAS,EACpB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,iBAAiB,CAAC,CAkD5B;IAED,yFAAyF;IACnF,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,CAAC,CA+BrF;IAED;;;;;OAKG;YACW,YAAY;IA+D1B;;;;;;OAMG;YACW,sBAAsB;IAepC;;;;;;;OAOG;YACW,QAAQ;IAWtB;;;;OAIG;IACG,QAAQ,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAuD7D;IAED;;;;OAIG;IACG,KAAK,CAAC,KAAK,SAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC,CA8CtE;YAEa,KAAK;IAiBb,MAAM,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoD5F;IAED,6FAA6F;YAC/E,oBAAoB;IAWlC;;;;;;;;;;;;;;;;;OAiBG;IACG,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAc1F;IAED;;;;;OAKG;IACG,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAExD;YAEa,iBAAiB;IAa/B,iFAAiF;IAC3E,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAsB7D;IAED,sFAAsF;YACxE,gBAAgB;IAQ9B,sFAAsF;IAChF,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAc/C;IAED,2FAA2F;IACrF,MAAM,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAiG3D;IAED;;;;OAIG;YACW,iBAAiB;CA+BhC"}
|