@chaos-overlords/kernel 0.1.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 (104) hide show
  1. package/LICENSE +674 -0
  2. package/NOTICE +21 -0
  3. package/README.md +28 -0
  4. package/dist/domain/entities.d.ts +91 -0
  5. package/dist/domain/entities.d.ts.map +1 -0
  6. package/dist/domain/entities.js +9 -0
  7. package/dist/domain/entities.js.map +1 -0
  8. package/dist/domain/errors.d.ts +30 -0
  9. package/dist/domain/errors.d.ts.map +1 -0
  10. package/dist/domain/errors.js +45 -0
  11. package/dist/domain/errors.js.map +1 -0
  12. package/dist/index.d.ts +7 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +7 -0
  15. package/dist/index.js.map +1 -0
  16. package/dist/logic/canonical-json.d.ts +17 -0
  17. package/dist/logic/canonical-json.d.ts.map +1 -0
  18. package/dist/logic/canonical-json.js +44 -0
  19. package/dist/logic/canonical-json.js.map +1 -0
  20. package/dist/logic/crypto.d.ts +29 -0
  21. package/dist/logic/crypto.d.ts.map +1 -0
  22. package/dist/logic/crypto.js +76 -0
  23. package/dist/logic/crypto.js.map +1 -0
  24. package/dist/logic/index.d.ts +5 -0
  25. package/dist/logic/index.d.ts.map +1 -0
  26. package/dist/logic/index.js +5 -0
  27. package/dist/logic/index.js.map +1 -0
  28. package/dist/logic/password.d.ts +4 -0
  29. package/dist/logic/password.d.ts.map +1 -0
  30. package/dist/logic/password.js +38 -0
  31. package/dist/logic/password.js.map +1 -0
  32. package/dist/logic/turn-logic.d.ts +52 -0
  33. package/dist/logic/turn-logic.d.ts.map +1 -0
  34. package/dist/logic/turn-logic.js +101 -0
  35. package/dist/logic/turn-logic.js.map +1 -0
  36. package/dist/ports/index.d.ts +3 -0
  37. package/dist/ports/index.d.ts.map +1 -0
  38. package/dist/ports/index.js +3 -0
  39. package/dist/ports/index.js.map +1 -0
  40. package/dist/ports/runtime.d.ts +38 -0
  41. package/dist/ports/runtime.d.ts.map +1 -0
  42. package/dist/ports/runtime.js +7 -0
  43. package/dist/ports/runtime.js.map +1 -0
  44. package/dist/ports/storage.d.ts +141 -0
  45. package/dist/ports/storage.d.ts.map +1 -0
  46. package/dist/ports/storage.js +2 -0
  47. package/dist/ports/storage.js.map +1 -0
  48. package/dist/services/AuthService.d.ts +13 -0
  49. package/dist/services/AuthService.d.ts.map +1 -0
  50. package/dist/services/AuthService.js +24 -0
  51. package/dist/services/AuthService.js.map +1 -0
  52. package/dist/services/EventPublisher.d.ts +20 -0
  53. package/dist/services/EventPublisher.d.ts.map +1 -0
  54. package/dist/services/EventPublisher.js +37 -0
  55. package/dist/services/EventPublisher.js.map +1 -0
  56. package/dist/services/LobbyService.d.ts +45 -0
  57. package/dist/services/LobbyService.d.ts.map +1 -0
  58. package/dist/services/LobbyService.js +366 -0
  59. package/dist/services/LobbyService.js.map +1 -0
  60. package/dist/services/MatchQueryService.d.ts +24 -0
  61. package/dist/services/MatchQueryService.d.ts.map +1 -0
  62. package/dist/services/MatchQueryService.js +99 -0
  63. package/dist/services/MatchQueryService.js.map +1 -0
  64. package/dist/services/RateLimiter.d.ts +28 -0
  65. package/dist/services/RateLimiter.d.ts.map +1 -0
  66. package/dist/services/RateLimiter.js +48 -0
  67. package/dist/services/RateLimiter.js.map +1 -0
  68. package/dist/services/RetentionService.d.ts +23 -0
  69. package/dist/services/RetentionService.d.ts.map +1 -0
  70. package/dist/services/RetentionService.js +36 -0
  71. package/dist/services/RetentionService.js.map +1 -0
  72. package/dist/services/SnapshotService.d.ts +40 -0
  73. package/dist/services/SnapshotService.d.ts.map +1 -0
  74. package/dist/services/SnapshotService.js +137 -0
  75. package/dist/services/SnapshotService.js.map +1 -0
  76. package/dist/services/TurnService.d.ts +63 -0
  77. package/dist/services/TurnService.d.ts.map +1 -0
  78. package/dist/services/TurnService.js +392 -0
  79. package/dist/services/TurnService.js.map +1 -0
  80. package/dist/services/createKernel.d.ts +23 -0
  81. package/dist/services/createKernel.d.ts.map +1 -0
  82. package/dist/services/createKernel.js +22 -0
  83. package/dist/services/createKernel.js.map +1 -0
  84. package/dist/services/deps.d.ts +11 -0
  85. package/dist/services/deps.d.ts.map +1 -0
  86. package/dist/services/deps.js +2 -0
  87. package/dist/services/deps.js.map +1 -0
  88. package/dist/services/index.d.ts +10 -0
  89. package/dist/services/index.d.ts.map +1 -0
  90. package/dist/services/index.js +10 -0
  91. package/dist/services/index.js.map +1 -0
  92. package/dist/testing/InMemoryStorage.d.ts +28 -0
  93. package/dist/testing/InMemoryStorage.d.ts.map +1 -0
  94. package/dist/testing/InMemoryStorage.js +279 -0
  95. package/dist/testing/InMemoryStorage.js.map +1 -0
  96. package/dist/testing/fakes.d.ts +38 -0
  97. package/dist/testing/fakes.d.ts.map +1 -0
  98. package/dist/testing/fakes.js +44 -0
  99. package/dist/testing/fakes.js.map +1 -0
  100. package/dist/testing/index.d.ts +3 -0
  101. package/dist/testing/index.d.ts.map +1 -0
  102. package/dist/testing/index.js +3 -0
  103. package/dist/testing/index.js.map +1 -0
  104. package/package.json +68 -0
@@ -0,0 +1,101 @@
1
+ /** Every human seat still being waited on has marked ready. An empty roster is never "ready". */
2
+ export function allActiveReady(players, orders) {
3
+ const active = players.filter((player) => player.status === 'active' || player.status === 'takeoverPending');
4
+ if (active.length === 0)
5
+ return false;
6
+ const readyIds = new Set(orders.filter((row) => row.ready).map((row) => row.playerId));
7
+ return active.every((player) => readyIds.has(player.id));
8
+ }
9
+ /**
10
+ * The state hashes that the most active players reported, tied if more than one.
11
+ *
12
+ * This is the corroboration a recovery snapshot is held to. Letting the host name any hash at all
13
+ * would make the host's client authoritative over a disagreement it is itself a party to: upload a
14
+ * doctored snapshot after a deliberate desync and every other client is told to adopt it. A hash
15
+ * that more players than any other already computed independently cannot be minted by one of them.
16
+ *
17
+ * A genuine tie (most of all, the 1-1 split of a two-player match) leaves nothing to count, and
18
+ * there is no third party to ask, so the host breaks it. Lockstep of three or more is where this
19
+ * bites, and that is the case worth defending: consistency is what matters, so converging on the
20
+ * majority's state is right even when the host's own client happens to be the correct one.
21
+ */
22
+ export function authoritativeCandidates(players, reports) {
23
+ const active = new Set(players.filter((player) => player.status === 'active').map((p) => p.id));
24
+ const counts = new Map();
25
+ for (const report of reports) {
26
+ if (!active.has(report.playerId))
27
+ continue;
28
+ counts.set(report.stateHash, (counts.get(report.stateHash) ?? 0) + 1);
29
+ }
30
+ const best = Math.max(0, ...counts.values());
31
+ if (best === 0)
32
+ return [];
33
+ return [...counts.entries()]
34
+ .filter(([, count]) => count === best)
35
+ .map(([stateHash]) => stateHash)
36
+ .sort();
37
+ }
38
+ /**
39
+ * Compare the post-turn state hashes the active players reported.
40
+ *
41
+ * Without an authoritative hash: once everyone has reported, unanimous agreement confirms the
42
+ * turn and any disagreement flags a desync. With one (the host uploaded a snapshot for this
43
+ * turn), a report is counted only when it matches it, so stragglers reloading the snapshot can
44
+ * converge without tripping a second desync.
45
+ */
46
+ export function evaluateConsensus(players, reports, authoritativeHash) {
47
+ const active = players.filter((player) => player.status === 'active');
48
+ if (active.length === 0)
49
+ return { kind: 'pending' };
50
+ const byPlayer = new Map(reports.map((report) => [report.playerId, report]));
51
+ const activeReports = active.map((player) => byPlayer.get(player.id));
52
+ if (activeReports.some((report) => report === undefined))
53
+ return { kind: 'pending' };
54
+ const present = activeReports;
55
+ if (authoritativeHash !== null) {
56
+ if (present.every((report) => report.stateHash === authoritativeHash)) {
57
+ return {
58
+ kind: 'confirmed',
59
+ stateHash: authoritativeHash,
60
+ finished: present.every((report) => report.finished),
61
+ };
62
+ }
63
+ return { kind: 'pending' };
64
+ }
65
+ const first = present[0];
66
+ if (first && present.every((report) => report.stateHash === first.stateHash)) {
67
+ return {
68
+ kind: 'confirmed',
69
+ stateHash: first.stateHash,
70
+ finished: present.every((report) => report.finished),
71
+ };
72
+ }
73
+ return {
74
+ kind: 'desynced',
75
+ reports: present.map((report) => ({ playerId: report.playerId, stateHash: report.stateHash })),
76
+ candidateStateHashes: authoritativeCandidates(players, reports),
77
+ };
78
+ }
79
+ /**
80
+ * Host takes slot 0; everyone else follows the match's monotonic join sequence. The sequence, not
81
+ * the join timestamp, is the key: two players seated in the same millisecond would otherwise be
82
+ * ordered by their random ids, and the slot order decides resolution order on every client.
83
+ */
84
+ export function assignSlots(players, hostPlayerId) {
85
+ const ordered = [...players]
86
+ .filter((player) => player.status === 'active')
87
+ .sort((a, b) => {
88
+ if (a.id === b.id)
89
+ return 0;
90
+ if (a.id === hostPlayerId)
91
+ return -1;
92
+ if (b.id === hostPlayerId)
93
+ return 1;
94
+ return a.joinOrder - b.joinOrder || a.id.localeCompare(b.id);
95
+ });
96
+ return ordered.map((player, slot) => ({ playerId: player.id, slot }));
97
+ }
98
+ export function turnDeadline(openedAt, turnTimerSeconds) {
99
+ return turnTimerSeconds > 0 ? new Date(openedAt.getTime() + turnTimerSeconds * 1000) : null;
100
+ }
101
+ //# sourceMappingURL=turn-logic.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn-logic.js","sourceRoot":"","sources":["../../src/logic/turn-logic.ts"],"names":[],"mappings":"AAEA,iGAAiG;AACjG,MAAM,UAAU,cAAc,CAAC,OAA0B,EAAE,MAA6B;IACtF,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAC3B,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,QAAQ,IAAI,MAAM,CAAC,MAAM,KAAK,iBAAiB,CAC9E,CAAA;IACD,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,KAAK,CAAA;IACrC,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAA;IACtF,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAA;AAC1D,CAAC;AAYD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,uBAAuB,CACrC,OAA0B,EAC1B,OAA8B;IAE9B,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;IAC/F,MAAM,MAAM,GAAG,IAAI,GAAG,EAAkB,CAAA;IACxC,KAAK,MAAM,MAAM,IAAI,OAAO,EAAE,CAAC;QAC7B,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC;YAAE,SAAQ;QAC1C,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAA;IACvE,CAAC;IACD,MAAM,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,CAAC,CAAA;IAC5C,IAAI,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,CAAA;IACzB,OAAO,CAAC,GAAG,MAAM,CAAC,OAAO,EAAE,CAAC;SACzB,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,IAAI,CAAC;SACrC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,EAAE,CAAC,SAAS,CAAC;SAC/B,IAAI,EAAE,CAAA;AACX,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,iBAAiB,CAC/B,OAA0B,EAC1B,OAA8B,EAC9B,iBAAgC;IAEhC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC,CAAA;IACrE,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IACnD,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC,CAAA;IAC5E,MAAM,aAAa,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC,CAAA;IACrE,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,KAAK,SAAS,CAAC;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IACpF,MAAM,OAAO,GAAG,aAA6B,CAAA;IAE7C,IAAI,iBAAiB,KAAK,IAAI,EAAE,CAAC;QAC/B,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,KAAK,iBAAiB,CAAC,EAAE,CAAC;YACtE,OAAO;gBACL,IAAI,EAAE,WAAW;gBACjB,SAAS,EAAE,iBAAiB;gBAC5B,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC;aACrD,CAAA;QACH,CAAC;QACD,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,CAAA;IAC5B,CAAC;IAED,MAAM,KAAK,GAAG,OAAO,CAAC,CAAC,CAAC,CAAA;IACxB,IAAI,KAAK,IAAI,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,SAAS,KAAK,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;QAC7E,OAAO;YACL,IAAI,EAAE,WAAW;YACjB,SAAS,EAAE,KAAK,CAAC,SAAS;YAC1B,QAAQ,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,QAAQ,CAAC;SACrD,CAAA;IACH,CAAC;IACD,OAAO;QACL,IAAI,EAAE,UAAU;QAChB,OAAO,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,CAAC,SAAS,EAAE,CAAC,CAAC;QAC9F,oBAAoB,EAAE,uBAAuB,CAAC,OAAO,EAAE,OAAO,CAAC;KAChE,CAAA;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,WAAW,CACzB,OAA0B,EAC1B,YAAoB;IAEpB,MAAM,OAAO,GAAG,CAAC,GAAG,OAAO,CAAC;SACzB,MAAM,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC,MAAM,KAAK,QAAQ,CAAC;SAC9C,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE;QACb,IAAI,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,EAAE;YAAE,OAAO,CAAC,CAAA;QAC3B,IAAI,CAAC,CAAC,EAAE,KAAK,YAAY;YAAE,OAAO,CAAC,CAAC,CAAA;QACpC,IAAI,CAAC,CAAC,EAAE,KAAK,YAAY;YAAE,OAAO,CAAC,CAAA;QACnC,OAAO,CAAC,CAAC,SAAS,GAAG,CAAC,CAAC,SAAS,IAAI,CAAC,CAAC,EAAE,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;IAC9D,CAAC,CAAC,CAAA;IACJ,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,IAAI,EAAE,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,MAAM,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC,CAAA;AACvE,CAAC;AAED,MAAM,UAAU,YAAY,CAAC,QAAc,EAAE,gBAAwB;IACnE,OAAO,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,OAAO,EAAE,GAAG,gBAAgB,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAA;AAC7F,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './runtime';
2
+ export * from './storage';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/ports/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA"}
@@ -0,0 +1,3 @@
1
+ export * from './runtime';
2
+ export * from './storage';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ports/index.ts"],"names":[],"mappings":"AAAA,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA"}
@@ -0,0 +1,38 @@
1
+ import type { PersistedEvent } from '../domain/entities';
2
+ export interface Clock {
3
+ now(): Date;
4
+ }
5
+ export interface Logger {
6
+ debug(msg: string, fields?: Record<string, unknown>): void;
7
+ info(msg: string, fields?: Record<string, unknown>): void;
8
+ warn(msg: string, fields?: Record<string, unknown>): void;
9
+ error(msg: string, fields?: Record<string, unknown>): void;
10
+ }
11
+ export declare const noopLogger: Logger;
12
+ /** Fan-out of a freshly persisted event to whoever is streaming that match. */
13
+ export interface EventNotifier {
14
+ notify(event: PersistedEvent): Promise<void>;
15
+ }
16
+ /**
17
+ * Wakes the turn service at a turn's deadline. Sealing is idempotent, so a duplicate or late
18
+ * wake is harmless; a MISSED wake is what the periodic `TurnService.sweep` safety net covers.
19
+ */
20
+ export interface DeadlineScheduler {
21
+ schedule(input: {
22
+ matchId: string;
23
+ turn: number;
24
+ dueAt: Date;
25
+ }): Promise<void>;
26
+ }
27
+ /**
28
+ * Opens the server-sent event stream for one match. Each runtime owns the response because the
29
+ * fan-out lives in a different place (an in-process hub on Node, a Durable Object on Cloudflare).
30
+ */
31
+ export interface EventStreamOpener {
32
+ open(input: {
33
+ matchId: string;
34
+ afterSeq: number;
35
+ signal: AbortSignal;
36
+ }): Promise<Response>;
37
+ }
38
+ //# sourceMappingURL=runtime.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../../src/ports/runtime.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AAExD,MAAM,WAAW,KAAK;IACpB,GAAG,IAAI,IAAI,CAAA;CACZ;AAED,MAAM,WAAW,MAAM;IACrB,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IAC1D,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACzD,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACzD,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;CAC3D;AAED,eAAO,MAAM,UAAU,EAAE,MAKxB,CAAA;AAED,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC7C;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,IAAI,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CAC/E;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,IAAI,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,WAAW,CAAA;KAAE,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAA;CAC3F"}
@@ -0,0 +1,7 @@
1
+ export const noopLogger = {
2
+ debug() { },
3
+ info() { },
4
+ warn() { },
5
+ error() { },
6
+ };
7
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../../src/ports/runtime.ts"],"names":[],"mappings":"AAaA,MAAM,CAAC,MAAM,UAAU,GAAW;IAChC,KAAK,KAAI,CAAC;IACV,IAAI,KAAI,CAAC;IACT,IAAI,KAAI,CAAC;IACT,KAAK,KAAI,CAAC;CACX,CAAA"}
@@ -0,0 +1,141 @@
1
+ import type { LobbyListing, MatchEventBody, MatchStatus, TurnStatus } from '@chaos-overlords/contracts';
2
+ import type { Match, PersistedEvent, Player, SealedSlot, Snapshot, Turn, TurnOrders, TurnReport } from '../domain/entities';
3
+ /**
4
+ * Persistence ports. There are NO transactions: D1 has none, so every invariant that two writers
5
+ * could race on is a single conditional statement whose row count says who won. Each `transition`
6
+ * is such a compare-and-swap; `claimSeat`/`releaseSeat` are atomic counters; `submitOrders` is
7
+ * conditional on the turn still being open; `append` allocates its own sequence number.
8
+ *
9
+ * Writes that a unique constraint can refuse answer `false` instead of throwing, so the services
10
+ * can retry (a join code collision) without knowing anything about a driver's error shapes.
11
+ */
12
+ export interface MatchRepository {
13
+ /** False when the id or the join code is already taken; the caller retries with a fresh code. */
14
+ create(match: Match): Promise<boolean>;
15
+ get(id: string): Promise<Match | null>;
16
+ getByJoinCode(joinCode: string): Promise<Match | null>;
17
+ listPublicLobbies(limit: number): Promise<LobbyListing[]>;
18
+ /**
19
+ * Atomically take a seat while the match is in the lobby and below capacity. Returns the seat's
20
+ * position in the match's monotonic join sequence, or null when no seat was available.
21
+ */
22
+ claimSeat(matchId: string): Promise<number | null>;
23
+ releaseSeat(matchId: string): Promise<void>;
24
+ /**
25
+ * Deletes matches in one of `statuses` last touched before `before`, with everything they own
26
+ * (players, turns, orders, reports, snapshots, events cascade). Returns how many went. Live
27
+ * matches are never in scope: the caller passes only terminal or never-started statuses.
28
+ */
29
+ deleteInactive(statuses: readonly MatchStatus[], before: Date, limit: number): Promise<number>;
30
+ /** Compare-and-swap on status; returns false when the match was not in one of `from`. */
31
+ transition(matchId: string, from: readonly MatchStatus[], patch: {
32
+ status?: MatchStatus;
33
+ seed?: number;
34
+ currentTurn?: number;
35
+ hostPlayerId?: string;
36
+ updatedAt: Date;
37
+ }): Promise<boolean>;
38
+ }
39
+ export interface PlayerRepository {
40
+ /**
41
+ * Insert the player only while their match is still in the lobby, in ONE statement. False when
42
+ * the match has started (or is gone), which is what keeps a seat claimed a moment before the
43
+ * host pressed start from becoming an unseated player in a running match.
44
+ */
45
+ create(player: Player): Promise<boolean>;
46
+ get(id: string): Promise<Player | null>;
47
+ /** Never matches a revoked membership, whose token hash is null. */
48
+ getByTokenHash(tokenHash: string): Promise<Player | null>;
49
+ /** Ordered by slot, then join order, then id. */
50
+ listByMatch(matchId: string): Promise<Player[]>;
51
+ setStatus(playerId: string, status: Player['status']): Promise<void>;
52
+ /** Compare-and-swap a player status; exactly one return/takeover race may win. */
53
+ transitionStatus(playerId: string, from: readonly Player['status'][], status: Player['status']): Promise<boolean>;
54
+ /** Clears the token hash, so the player's bearer token stops authenticating immediately. */
55
+ revokeToken(playerId: string): Promise<void>;
56
+ /**
57
+ * Seat every player in ONE statement. A loop of updates could fail partway and leave a running
58
+ * match with some players still unseated, after the status change that made the roster final has
59
+ * already committed — and nothing downstream repairs seating.
60
+ */
61
+ assignSlots(assignments: ReadonlyArray<{
62
+ playerId: string;
63
+ slot: number;
64
+ }>): Promise<void>;
65
+ delete(playerId: string): Promise<void>;
66
+ }
67
+ export interface TurnRepository {
68
+ /**
69
+ * Insert the turn and one empty orders row per player. False when the turn already exists, which
70
+ * is how the seal repair can re-run the open step without publishing a second `turn.opened`.
71
+ */
72
+ open(turn: Turn, playerIds: readonly string[]): Promise<boolean>;
73
+ get(matchId: string, number: number): Promise<Turn | null>;
74
+ /**
75
+ * Overwrite a player's orders only while the turn is `open`, in ONE statement.
76
+ * Returns false when the turn is no longer open or the player has no row.
77
+ */
78
+ submitOrders(matchId: string, number: number, playerId: string, submission: {
79
+ orders: TurnOrders['orders'];
80
+ ordersHash: string | null;
81
+ ready: boolean;
82
+ submittedAt: Date;
83
+ }): Promise<boolean>;
84
+ getOrders(matchId: string, number: number, playerId: string): Promise<TurnOrders | null>;
85
+ listOrders(matchId: string, number: number): Promise<TurnOrders[]>;
86
+ transition(matchId: string, number: number, from: readonly TurnStatus[], patch: {
87
+ status: TurnStatus;
88
+ sealedAt?: Date;
89
+ orderSetHash?: string;
90
+ sealedSlots?: readonly SealedSlot[];
91
+ }): Promise<boolean>;
92
+ /** Move an open turn's deadline, e.g. when a match resumes after a desync pause. */
93
+ rescheduleDeadline(matchId: string, number: number, deadlineAt: Date | null): Promise<boolean>;
94
+ upsertReport(report: TurnReport): Promise<void>;
95
+ listReports(matchId: string, number: number): Promise<TurnReport[]>;
96
+ /** Turns of a match still awaiting a verdict: status `sealed` or `desynced`, ascending. */
97
+ listUnsettled(matchId: string): Promise<Turn[]>;
98
+ /** Open turns whose deadline has passed, oldest first. */
99
+ listExpiredOpen(now: Date, limit: number): Promise<Array<Pick<Turn, 'matchId' | 'number'>>>;
100
+ /**
101
+ * Live matches whose current turn is not open, so there is nothing for anyone to play: a seal
102
+ * that died between marking the turn sealed and opening its successor, or a start that died
103
+ * between running the match and opening turn 1 (no row at all). The repair sweep finishes both,
104
+ * which is why a missing turn counts as stalled rather than being skipped.
105
+ */
106
+ listStalledSeals(limit: number): Promise<Array<Pick<Turn, 'matchId' | 'number'>>>;
107
+ }
108
+ export interface SnapshotRepository {
109
+ put(snapshot: Snapshot): Promise<void>;
110
+ get(matchId: string, turn: number): Promise<Snapshot | null>;
111
+ getLatest(matchId: string): Promise<Snapshot | null>;
112
+ /**
113
+ * Keep only the `keep` newest turns' snapshots of a match, dropping the rest. Returns how many
114
+ * went. Retention collects whole terminated matches; this bounds what a single LIVE match holds,
115
+ * which is otherwise a megabyte per desynced turn with nothing to stop it.
116
+ */
117
+ prune(matchId: string, keep: number): Promise<number>;
118
+ }
119
+ export interface EventRepository {
120
+ /**
121
+ * Append one event, allocating its sequence number inside the insert as `max(seq) + 1` for the
122
+ * match. The sequence is therefore gapless and a committed `seq` implies every lower one is
123
+ * committed too, which is what lets a stream cursor move forward and never skip an event. Two
124
+ * concurrent appends race on the primary key; the loser retries. Returns the persisted event.
125
+ */
126
+ append(event: MatchEventBody & {
127
+ matchId: string;
128
+ createdAt: string;
129
+ }): Promise<PersistedEvent>;
130
+ listAfter(matchId: string, afterSeq: number, limit: number): Promise<PersistedEvent[]>;
131
+ /** Highest sequence number persisted for the match, or 0 when the log is empty. */
132
+ lastSeq(matchId: string): Promise<number>;
133
+ }
134
+ export interface MultiplayerStorage {
135
+ matches: MatchRepository;
136
+ players: PlayerRepository;
137
+ turns: TurnRepository;
138
+ snapshots: SnapshotRepository;
139
+ events: EventRepository;
140
+ }
141
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../src/ports/storage.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../../src/ports/storage.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ import type { Match, Player } from '../domain/entities';
2
+ import type { MultiplayerStorage } from '../ports/storage';
3
+ export interface Principal {
4
+ player: Player;
5
+ match: Match;
6
+ }
7
+ export declare class AuthService {
8
+ private readonly storage;
9
+ constructor(storage: MultiplayerStorage);
10
+ /** Resolves a bearer token to its player and match. Never reveals which half failed. */
11
+ authenticate(token: string): Promise<Principal>;
12
+ }
13
+ //# sourceMappingURL=AuthService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuthService.d.ts","sourceRoot":"","sources":["../../src/services/AuthService.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAA;AAGvD,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAE1D,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAA;IACd,KAAK,EAAE,KAAK,CAAA;CACb;AAED,qBAAa,WAAW;IACV,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,kBAAkB;IAExD,wFAAwF;IAClF,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,CAAC;CAQtD"}
@@ -0,0 +1,24 @@
1
+ import { UnauthorizedError } from '../domain/errors';
2
+ import { hashToken, TOKEN_PREFIX } from '../logic/crypto';
3
+ export class AuthService {
4
+ storage;
5
+ constructor(storage) {
6
+ this.storage = storage;
7
+ }
8
+ /** Resolves a bearer token to its player and match. Never reveals which half failed. */
9
+ async authenticate(token) {
10
+ if (!token.startsWith(TOKEN_PREFIX))
11
+ throw invalid();
12
+ const player = await this.storage.players.getByTokenHash(await hashToken(token));
13
+ if (!player)
14
+ throw invalid();
15
+ const match = await this.storage.matches.get(player.matchId);
16
+ if (!match)
17
+ throw invalid();
18
+ return { player, match };
19
+ }
20
+ }
21
+ function invalid() {
22
+ return new UnauthorizedError('Invalid or expired player token', { reason: 'invalid_token' });
23
+ }
24
+ //# sourceMappingURL=AuthService.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AuthService.js","sourceRoot":"","sources":["../../src/services/AuthService.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAA;AAQzD,MAAM,OAAO,WAAW;IACO;IAA7B,YAA6B,OAA2B;QAA3B,YAAO,GAAP,OAAO,CAAoB;IAAG,CAAC;IAE5D,wFAAwF;IACxF,KAAK,CAAC,YAAY,CAAC,KAAa;QAC9B,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,YAAY,CAAC;YAAE,MAAM,OAAO,EAAE,CAAA;QACpD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC,MAAM,SAAS,CAAC,KAAK,CAAC,CAAC,CAAA;QAChF,IAAI,CAAC,MAAM;YAAE,MAAM,OAAO,EAAE,CAAA;QAC5B,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAA;QAC5D,IAAI,CAAC,KAAK;YAAE,MAAM,OAAO,EAAE,CAAA;QAC3B,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAA;IAC1B,CAAC;CACF;AAED,SAAS,OAAO;IACd,OAAO,IAAI,iBAAiB,CAAC,iCAAiC,EAAE,EAAE,MAAM,EAAE,eAAe,EAAE,CAAC,CAAA;AAC9F,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { MatchEventBody } from '@chaos-overlords/contracts';
2
+ import type { PersistedEvent } from '../domain/entities';
3
+ import type { KernelDeps } from './deps';
4
+ /**
5
+ * Appends an event to a match's log and fans it out.
6
+ *
7
+ * The sequence number is allocated by the insert itself, so the log has no gaps: a client (or a
8
+ * stream cursor) that holds `seq` is guaranteed to have been offered every event below it. That is
9
+ * the property a separate counter could not give — a crash or a reorder between "take a number" and
10
+ * "write the row" would publish a hole that a monotonic cursor then skips forever.
11
+ *
12
+ * Fan-out is only a wake-up hint. It happens after the event is durable and its failure is logged,
13
+ * never propagated: the next drain of the log delivers the event anyway.
14
+ */
15
+ export declare class EventPublisher {
16
+ private readonly deps;
17
+ constructor(deps: Pick<KernelDeps, 'storage' | 'notifier' | 'clock' | 'logger'>);
18
+ publish(matchId: string, body: MatchEventBody): Promise<PersistedEvent>;
19
+ }
20
+ //# sourceMappingURL=EventPublisher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EventPublisher.d.ts","sourceRoot":"","sources":["../../src/services/EventPublisher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAA;AAChE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAA;AACxD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAExC;;;;;;;;;;GAUG;AACH,qBAAa,cAAc;IAEvB,OAAO,CAAC,QAAQ,CAAC,IAAI;gBAAJ,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,GAAG,UAAU,GAAG,OAAO,GAAG,QAAQ,CAAC;IAGhF,OAAO,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;CAkB9E"}
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Appends an event to a match's log and fans it out.
3
+ *
4
+ * The sequence number is allocated by the insert itself, so the log has no gaps: a client (or a
5
+ * stream cursor) that holds `seq` is guaranteed to have been offered every event below it. That is
6
+ * the property a separate counter could not give — a crash or a reorder between "take a number" and
7
+ * "write the row" would publish a hole that a monotonic cursor then skips forever.
8
+ *
9
+ * Fan-out is only a wake-up hint. It happens after the event is durable and its failure is logged,
10
+ * never propagated: the next drain of the log delivers the event anyway.
11
+ */
12
+ export class EventPublisher {
13
+ deps;
14
+ constructor(deps) {
15
+ this.deps = deps;
16
+ }
17
+ async publish(matchId, body) {
18
+ const event = await this.deps.storage.events.append({
19
+ ...body,
20
+ matchId,
21
+ createdAt: this.deps.clock.now().toISOString(),
22
+ });
23
+ try {
24
+ await this.deps.notifier.notify(event);
25
+ }
26
+ catch (error) {
27
+ // The event is durable; a lost notification is recovered by the stream's next catch-up read.
28
+ this.deps.logger.warn('event notification failed', {
29
+ matchId,
30
+ seq: event.seq,
31
+ error: String(error),
32
+ });
33
+ }
34
+ return event;
35
+ }
36
+ }
37
+ //# sourceMappingURL=EventPublisher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EventPublisher.js","sourceRoot":"","sources":["../../src/services/EventPublisher.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;GAUG;AACH,MAAM,OAAO,cAAc;IAEN;IADnB,YACmB,IAAmE;QAAnE,SAAI,GAAJ,IAAI,CAA+D;IACnF,CAAC;IAEJ,KAAK,CAAC,OAAO,CAAC,OAAe,EAAE,IAAoB;QACjD,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC;YAClD,GAAG,IAAI;YACP,OAAO;YACP,SAAS,EAAE,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,WAAW,EAAE;SAC/C,CAAC,CAAA;QACF,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;QACxC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,6FAA6F;YAC7F,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,2BAA2B,EAAE;gBACjD,OAAO;gBACP,GAAG,EAAE,KAAK,CAAC,GAAG;gBACd,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC;aACrB,CAAC,CAAA;QACJ,CAAC;QACD,OAAO,KAAK,CAAA;IACd,CAAC;CACF"}
@@ -0,0 +1,45 @@
1
+ import { type CreateMatchRequest, type JoinMatchRequest, type MembershipView, type TakeoverVoteRequest } from '@chaos-overlords/contracts';
2
+ import type { Principal } from './AuthService';
3
+ import type { KernelDeps } from './deps';
4
+ import type { EventPublisher } from './EventPublisher';
5
+ import { type TurnService } from './TurnService';
6
+ export interface LobbyServiceOptions {
7
+ /** Generates the player/match ids; defaults to `crypto.randomUUID`. */
8
+ newId?: () => string;
9
+ }
10
+ /** Match creation, joining, leaving, kicking and the start transition. */
11
+ export declare class LobbyService {
12
+ private readonly deps;
13
+ private readonly publisher;
14
+ private readonly turns;
15
+ private readonly query;
16
+ private readonly newId;
17
+ constructor(deps: KernelDeps, publisher: EventPublisher, turns: TurnService, options?: LobbyServiceOptions);
18
+ createMatch(request: CreateMatchRequest): Promise<MembershipView>;
19
+ join(request: JoinMatchRequest): Promise<MembershipView>;
20
+ leave(principal: Principal): Promise<void>;
21
+ kick(principal: Principal, targetPlayerId: string): Promise<void>;
22
+ /**
23
+ * Record one present player's latest choice. AI control is deliberately unanimous: a single
24
+ * `wait` vote preserves the human seat, and there is no timeout that silently changes it.
25
+ */
26
+ voteOnTakeover(principal: Principal, targetPlayerId: string, request: TakeoverVoteRequest): Promise<void>;
27
+ start(principal: Principal): Promise<void>;
28
+ private remove;
29
+ private currentTakeoverVotes;
30
+ /**
31
+ * Undo a join that could not be completed. The token was never returned, so the row is
32
+ * unreachable: leaving it would hold a seat and keep `allActiveReady` waiting forever on a player
33
+ * who does not exist. Deleting the row is safe precisely because nobody ever held its token.
34
+ */
35
+ private rollbackJoin;
36
+ private abandon;
37
+ /**
38
+ * Inserts the match with a join code no other match holds. The uniqueness is the database's to
39
+ * enforce, not ours to check first: a `getByJoinCode` probe before the insert would be a race, so
40
+ * a refused insert is what drives the retry.
41
+ */
42
+ private createWithFreshJoinCode;
43
+ private membership;
44
+ }
45
+ //# sourceMappingURL=LobbyService.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"LobbyService.d.ts","sourceRoot":"","sources":["../../src/services/LobbyService.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,kBAAkB,EACvB,KAAK,gBAAgB,EAErB,KAAK,cAAc,EACnB,KAAK,mBAAmB,EACzB,MAAM,4BAA4B,CAAA;AAMnC,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;AAEtD,OAAO,EAAc,KAAK,WAAW,EAAE,MAAM,eAAe,CAAA;AAS5D,MAAM,WAAW,mBAAmB;IAClC,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,MAAM,CAAA;CACrB;AAED,0EAA0E;AAC1E,qBAAa,YAAY;IAKrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK;IANxB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAmB;IACzC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAc;gBAGjB,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,cAAc,EACzB,KAAK,EAAE,WAAW,EACnC,OAAO,GAAE,mBAAwB;IAM7B,WAAW,CAAC,OAAO,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC;IA2CjE,IAAI,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,cAAc,CAAC;IAqDxD,KAAK,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1C,IAAI,CAAC,SAAS,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAevE;;;OAGG;IACG,cAAc,CAClB,SAAS,EAAE,SAAS,EACpB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,mBAAmB,GAC3B,OAAO,CAAC,IAAI,CAAC;IAqDV,KAAK,CAAC,SAAS,EAAE,SAAS,GAAG,OAAO,CAAC,IAAI,CAAC;YAoClC,MAAM;YAgDN,oBAAoB;IAqClC;;;;OAIG;YACW,YAAY;YAaZ,OAAO;IAcrB;;;;OAIG;YACW,uBAAuB;YAUvB,UAAU;CAQzB"}