@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.
Files changed (60) hide show
  1. package/dist/domain/entities.d.ts +31 -1
  2. package/dist/domain/entities.d.ts.map +1 -1
  3. package/dist/domain/entities.js +9 -1
  4. package/dist/domain/entities.js.map +1 -1
  5. package/dist/domain/errors.d.ts.map +1 -1
  6. package/dist/domain/errors.js.map +1 -1
  7. package/dist/logic/canonical-json.d.ts.map +1 -1
  8. package/dist/logic/canonical-json.js.map +1 -1
  9. package/dist/logic/turn-logic.d.ts.map +1 -1
  10. package/dist/logic/turn-logic.js +2 -4
  11. package/dist/logic/turn-logic.js.map +1 -1
  12. package/dist/ports/storage.d.ts +130 -12
  13. package/dist/ports/storage.d.ts.map +1 -1
  14. package/dist/services/AuthService.d.ts.map +1 -1
  15. package/dist/services/AuthService.js.map +1 -1
  16. package/dist/services/EventPublisher.d.ts.map +1 -1
  17. package/dist/services/EventPublisher.js.map +1 -1
  18. package/dist/services/LobbyService.d.ts +40 -2
  19. package/dist/services/LobbyService.d.ts.map +1 -1
  20. package/dist/services/LobbyService.js +245 -91
  21. package/dist/services/LobbyService.js.map +1 -1
  22. package/dist/services/MatchQueryService.d.ts +12 -2
  23. package/dist/services/MatchQueryService.d.ts.map +1 -1
  24. package/dist/services/MatchQueryService.js +44 -24
  25. package/dist/services/MatchQueryService.js.map +1 -1
  26. package/dist/services/RateLimiter.d.ts +10 -0
  27. package/dist/services/RateLimiter.d.ts.map +1 -1
  28. package/dist/services/RateLimiter.js +48 -5
  29. package/dist/services/RateLimiter.js.map +1 -1
  30. package/dist/services/RetentionService.d.ts +15 -0
  31. package/dist/services/RetentionService.d.ts.map +1 -1
  32. package/dist/services/RetentionService.js +20 -23
  33. package/dist/services/RetentionService.js.map +1 -1
  34. package/dist/services/SnapshotService.d.ts +55 -9
  35. package/dist/services/SnapshotService.d.ts.map +1 -1
  36. package/dist/services/SnapshotService.js +120 -41
  37. package/dist/services/SnapshotService.js.map +1 -1
  38. package/dist/services/TurnService.d.ts +23 -7
  39. package/dist/services/TurnService.d.ts.map +1 -1
  40. package/dist/services/TurnService.js +217 -76
  41. package/dist/services/TurnService.js.map +1 -1
  42. package/dist/services/createKernel.js.map +1 -1
  43. package/dist/services/guards.d.ts +9 -0
  44. package/dist/services/guards.d.ts.map +1 -0
  45. package/dist/services/guards.js +22 -0
  46. package/dist/services/guards.js.map +1 -0
  47. package/dist/testing/CountingStorage.d.ts +25 -0
  48. package/dist/testing/CountingStorage.d.ts.map +1 -0
  49. package/dist/testing/CountingStorage.js +51 -0
  50. package/dist/testing/CountingStorage.js.map +1 -0
  51. package/dist/testing/InMemoryStorage.d.ts.map +1 -1
  52. package/dist/testing/InMemoryStorage.js +83 -17
  53. package/dist/testing/InMemoryStorage.js.map +1 -1
  54. package/dist/testing/fakes.d.ts.map +1 -1
  55. package/dist/testing/fakes.js.map +1 -1
  56. package/dist/testing/index.d.ts +1 -0
  57. package/dist/testing/index.d.ts.map +1 -1
  58. package/dist/testing/index.js +1 -0
  59. package/dist/testing/index.js.map +1 -1
  60. package/package.json +4 -4
@@ -1,13 +1,23 @@
1
- import { type LobbyListing, type MatchView, type OwnSubmissionView, type PlayerView, type SealedOrdersView } from '@chaos-overlords/contracts';
2
- import type { Match, Player } from '../domain/entities';
1
+ import { type LobbyListing, type MatchEventBody, type MatchView, type OwnSubmissionView, type PlayerView, type SealedOrdersView } from '@chaos-overlords/contracts';
2
+ import { type Match, type Player } from '../domain/entities';
3
3
  import type { MultiplayerStorage } from '../ports/storage';
4
4
  export declare function toPlayerView(player: Player, hostPlayerId: string): PlayerView;
5
+ /** The `match.started` announcement: the seed and the seated roster every client bootstraps from. */
6
+ export declare function matchStartedEvent(seed: number, seated: readonly Player[], hostPlayerId: string): MatchEventBody;
5
7
  /** Read models. Every view is assembled from list reads, never one query per player. */
6
8
  export declare class MatchQueryService {
7
9
  private readonly storage;
8
10
  constructor(storage: MultiplayerStorage);
9
11
  view(match: Match): Promise<MatchView>;
10
12
  private turnView;
13
+ /**
14
+ * The public lobby list.
15
+ *
16
+ * Two queries in total, whatever the page holds: the listing itself carries the seat count and
17
+ * whether a snapshot exists, and the seats of every match that could still advertise one are read
18
+ * together. It used to be two more queries for each running match on the page, on a route no
19
+ * token guards.
20
+ */
11
21
  listPublicLobbies(limit: number): Promise<LobbyListing[]>;
12
22
  ownSubmission(match: Match, playerId: string, number: number): Promise<OwnSubmissionView>;
13
23
  /**
@@ -1 +1 @@
1
- {"version":3,"file":"MatchQueryService.d.ts","sourceRoot":"","sources":["../../src/services/MatchQueryService.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,UAAU,EACf,KAAK,gBAAgB,EAGtB,MAAM,4BAA4B,CAAA;AACnC,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,EAAQ,MAAM,oBAAoB,CAAA;AAE7D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAE1D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,UAAU,CAS7E;AAED,wFAAwF;AACxF,qBAAa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,OAAO;gBAAP,OAAO,EAAE,kBAAkB;IAElD,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC;YAwB9B,QAAQ;IAoBhB,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IA2CzD,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAM/F;;;;;;;OAOG;IACG,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;YAmB7D,WAAW;CAK1B"}
1
+ {"version":3,"file":"MatchQueryService.d.ts","sourceRoot":"","sources":["../../src/services/MatchQueryService.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,SAAS,EACd,KAAK,iBAAiB,EACtB,KAAK,UAAU,EACf,KAAK,gBAAgB,EAGtB,MAAM,4BAA4B,CAAA;AACnC,OAAO,EAAiB,KAAK,KAAK,EAAE,KAAK,MAAM,EAAa,MAAM,oBAAoB,CAAA;AAEtF,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AAG1D,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,UAAU,CAS7E;AAED,qGAAqG;AACrG,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,YAAY,EAAE,MAAM,GACnB,cAAc,CAQhB;AAED,wFAAwF;AACxF,qBAAa,iBAAiB;IAChB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAApC,YAA6B,OAAO,EAAE,kBAAkB,EAAI;IAEtD,IAAI,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,SAAS,CAAC,CAsB3C;YAEa,QAAQ;IAoBtB;;;;;;;OAOG;IACG,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA0C9D;IAEK,aAAa,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAI9F;IAED;;;;;;;OAOG;IACG,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAiB1E;IAED,OAAO,CAAC,WAAW;CAGpB"}
@@ -1,5 +1,7 @@
1
1
  import { GAME_BOUNDS, } from '@chaos-overlords/contracts';
2
+ import { activePlayers } from '../domain/entities';
2
3
  import { ConflictError, NotFoundError } from '../domain/errors';
4
+ import { requireTurn } from './guards';
3
5
  export function toPlayerView(player, hostPlayerId) {
4
6
  return {
5
7
  id: player.id,
@@ -10,6 +12,16 @@ export function toPlayerView(player, hostPlayerId) {
10
12
  isHost: player.id === hostPlayerId,
11
13
  };
12
14
  }
15
+ /** The `match.started` announcement: the seed and the seated roster every client bootstraps from. */
16
+ export function matchStartedEvent(seed, seated, hostPlayerId) {
17
+ return {
18
+ type: 'match.started',
19
+ payload: {
20
+ seed,
21
+ players: activePlayers(seated).map((player) => toPlayerView(player, hostPlayerId)),
22
+ },
23
+ };
24
+ }
13
25
  /** Read models. Every view is assembled from list reads, never one query per player. */
14
26
  export class MatchQueryService {
15
27
  storage;
@@ -60,40 +72,51 @@ export class MatchQueryService {
60
72
  reportedPlayerIds: reports.map((report) => report.playerId),
61
73
  };
62
74
  }
75
+ /**
76
+ * The public lobby list.
77
+ *
78
+ * Two queries in total, whatever the page holds: the listing itself carries the seat count and
79
+ * whether a snapshot exists, and the seats of every match that could still advertise one are read
80
+ * together. It used to be two more queries for each running match on the page, on a route no
81
+ * token guards.
82
+ */
63
83
  async listPublicLobbies(limit) {
64
- const listings = await this.storage.matches.listPublicLobbies(limit);
65
- return Promise.all(listings.map(async (listing) => {
66
- if (listing.status !== 'running')
84
+ const rows = await this.storage.matches.listPublicLobbies(limit);
85
+ const lateJoinable = rows.filter((row) => row.status === 'running' &&
86
+ row.settings.gameSettings.allowLateJoin === true &&
87
+ row.hasSnapshot);
88
+ const seatsByMatch = new Map();
89
+ for (const seat of await this.storage.players.listSeats(lateJoinable.map((row) => row.id))) {
90
+ const seats = seatsByMatch.get(seat.matchId);
91
+ if (seats)
92
+ seats.push(seat.slot);
93
+ else
94
+ seatsByMatch.set(seat.matchId, [seat.slot]);
95
+ }
96
+ return rows.map(({ hasSnapshot: _hasSnapshot, ...listing }) => {
97
+ const seats = seatsByMatch.get(listing.id);
98
+ if (seats === undefined)
67
99
  return listing;
68
- const players = await this.storage.players.listByMatch(listing.id);
69
- const current = {
70
- ...listing,
71
- playerCount: players.filter((player) => player.status === 'active').length,
72
- };
73
- if (listing.settings.gameSettings.allowLateJoin !== true)
74
- return { ...current, availableSlots: [], availableSeatSummaries: [] };
75
- if (!(await this.storage.snapshots.getLatestSummary(listing.id)))
76
- return { ...current, availableSlots: [], availableSeatSummaries: [] };
77
100
  // `joinRunning` counts every seat a human has ever held against the host's own limit, so a
78
101
  // listing that ignored it advertised seats that every join answers `match_full` for.
79
- if (players.length >= listing.settings.maxPlayers)
80
- return { ...current, availableSlots: [], availableSeatSummaries: [] };
81
- const reserved = new Set(players.map((player) => player.slot));
102
+ if (seats.length >= listing.settings.maxPlayers)
103
+ return listing;
104
+ const reserved = new Set(seats);
82
105
  const availableSlots = Array.from({ length: GAME_BOUNDS.playerCount }, (_, slot) => slot).filter((slot) => !reserved.has(slot));
83
106
  const summaries = Array.isArray(listing.settings.gameSettings.seatSummaries)
84
- ? listing.settings.gameSettings.seatSummaries
107
+ ? listing.settings.gameSettings.seatSummaries.filter(isSeatSummary)
85
108
  : [];
86
109
  return {
87
- ...current,
110
+ ...listing,
88
111
  availableSlots,
89
- availableSeatSummaries: availableSlots.map((slot) => summaries.filter(isSeatSummary).find((summary) => summary.slot === slot) ?? {
112
+ availableSeatSummaries: availableSlots.map((slot) => summaries.find((summary) => summary.slot === slot) ?? {
90
113
  slot,
91
114
  gangs: 0,
92
115
  sites: 0,
93
116
  sectors: 0,
94
117
  }),
95
118
  };
96
- }));
119
+ });
97
120
  }
98
121
  async ownSubmission(match, playerId, number) {
99
122
  const row = await this.storage.turns.getOrders(match.id, number, playerId);
@@ -125,11 +148,8 @@ export class MatchQueryService {
125
148
  }
126
149
  return { turn: number, orderSetHash: turn.orderSetHash, players: rows };
127
150
  }
128
- async requireTurn(matchId, number) {
129
- const turn = await this.storage.turns.get(matchId, number);
130
- if (!turn)
131
- throw new NotFoundError('No such turn', { reason: 'unknown_turn' });
132
- return turn;
151
+ requireTurn(matchId, number) {
152
+ return requireTurn(this.storage.turns, matchId, number);
133
153
  }
134
154
  }
135
155
  function isSeatSummary(value) {
@@ -1 +1 @@
1
- {"version":3,"file":"MatchQueryService.js","sourceRoot":"","sources":["../../src/services/MatchQueryService.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"MatchQueryService.js","sourceRoot":"","sources":["../../src/services/MatchQueryService.ts"],"names":[],"mappings":"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"}
@@ -15,6 +15,14 @@ export declare class RateLimiter {
15
15
  });
16
16
  /** Returns the retry delay in seconds when over the limit, or null when the call is allowed. */
17
17
  take(key: string): number | null;
18
+ /** What `take` would answer right now, spending nothing. */
19
+ peek(key: string): number | null;
20
+ /** How much of `key`'s budget the current window has already spent; 0 once it has rolled. */
21
+ spent(key: string): number;
22
+ /** The window `key` is spending from, or undefined once it has rolled or was never opened. */
23
+ private live;
24
+ /** Whole seconds until `entry`'s window rolls, rounded up. */
25
+ private retryAfter;
18
26
  /**
19
27
  * Drop windows that have expired, at most once per window period.
20
28
  *
@@ -24,5 +32,7 @@ export declare class RateLimiter {
24
32
  * entries can never outlive two windows.
25
33
  */
26
34
  private prune;
35
+ /** Hard bound on residency, for windows too long for `prune` to be the only collector. */
36
+ private evictOldest;
27
37
  }
28
38
  //# sourceMappingURL=RateLimiter.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RateLimiter.d.ts","sourceRoot":"","sources":["../../src/services/RateLimiter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAE7C;;;;GAIG;AACH,qBAAa,WAAW;IAKpB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA4D;IACpF,OAAO,CAAC,SAAS,CAA2B;gBAGzB,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE;IAG/D,gGAAgG;IAChG,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;IAehC;;;;;;;OAOG;IACH,OAAO,CAAC,KAAK;CAOd"}
1
+ {"version":3,"file":"RateLimiter.d.ts","sourceRoot":"","sources":["../../src/services/RateLimiter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAmB7C;;;;GAIG;AACH,qBAAa,WAAW;IAKpB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAL1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAiC;IACzD,OAAO,CAAC,SAAS,CAA2B;IAE5C,YACmB,KAAK,EAAE,KAAK,EACZ,OAAO,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,EAC3D;IAEJ,gGAAgG;IAChG,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAY/B;IAED,4DAA4D;IAC5D,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK/B;IAED,6FAA6F;IAC7F,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEzB;IAED,8FAA8F;IAC9F,OAAO,CAAC,IAAI;IAMZ,8DAA8D;IAC9D,OAAO,CAAC,UAAU;IAIlB;;;;;;;OAOG;IACH,OAAO,CAAC,KAAK;IAQb,0FAA0F;IAC1F,OAAO,CAAC,WAAW;CAOpB"}
@@ -1,3 +1,14 @@
1
+ /**
2
+ * How many distinct keys one limiter holds before the oldest are dropped.
3
+ *
4
+ * `prune` only collects windows that have EXPIRED, at most once per window period, which is right
5
+ * for a minute-long window and wrong for a day-long one: the bug-report budget's window is
6
+ * twenty-four hours, so every address that ever posted stayed resident for a day or two on an
7
+ * unauthenticated route. A Map iterates in insertion order, so the head is the oldest key and
8
+ * evicting from there is one shift. Dropping a live window forgives that key's remaining budget,
9
+ * which is the safe direction to fail on a memory bound.
10
+ */
11
+ const MAX_KEYS = 50_000;
1
12
  /**
2
13
  * Fixed-window counter per key, in memory. It protects the unauthenticated doors (create, join)
3
14
  * from brute force on one process; a horizontally scaled deployment puts a shared limiter (a WAF
@@ -15,18 +26,41 @@ export class RateLimiter {
15
26
  /** Returns the retry delay in seconds when over the limit, or null when the call is allowed. */
16
27
  take(key) {
17
28
  const now = this.clock.now().getTime();
18
- const entry = this.windows.get(key);
19
- if (!entry || now - entry.windowStart >= this.options.windowMs) {
29
+ const entry = this.live(key, now);
30
+ if (!entry) {
20
31
  this.windows.set(key, { windowStart: now, count: 1 });
21
32
  this.prune(now);
33
+ this.evictOldest();
22
34
  return null;
23
35
  }
24
- if (entry.count >= this.options.limit) {
25
- return Math.ceil((entry.windowStart + this.options.windowMs - now) / 1000);
26
- }
36
+ if (entry.count >= this.options.limit)
37
+ return this.retryAfter(entry, now);
27
38
  entry.count += 1;
28
39
  return null;
29
40
  }
41
+ /** What `take` would answer right now, spending nothing. */
42
+ peek(key) {
43
+ const now = this.clock.now().getTime();
44
+ const entry = this.live(key, now);
45
+ if (!entry || entry.count < this.options.limit)
46
+ return null;
47
+ return this.retryAfter(entry, now);
48
+ }
49
+ /** How much of `key`'s budget the current window has already spent; 0 once it has rolled. */
50
+ spent(key) {
51
+ return this.live(key, this.clock.now().getTime())?.count ?? 0;
52
+ }
53
+ /** The window `key` is spending from, or undefined once it has rolled or was never opened. */
54
+ live(key, now) {
55
+ const entry = this.windows.get(key);
56
+ if (!entry || now - entry.windowStart >= this.options.windowMs)
57
+ return undefined;
58
+ return entry;
59
+ }
60
+ /** Whole seconds until `entry`'s window rolls, rounded up. */
61
+ retryAfter(entry, now) {
62
+ return Math.ceil((entry.windowStart + this.options.windowMs - now) / 1000);
63
+ }
30
64
  /**
31
65
  * Drop windows that have expired, at most once per window period.
32
66
  *
@@ -44,5 +78,14 @@ export class RateLimiter {
44
78
  this.windows.delete(key);
45
79
  }
46
80
  }
81
+ /** Hard bound on residency, for windows too long for `prune` to be the only collector. */
82
+ evictOldest() {
83
+ while (this.windows.size > MAX_KEYS) {
84
+ const oldest = this.windows.keys().next();
85
+ if (oldest.done)
86
+ return;
87
+ this.windows.delete(oldest.value);
88
+ }
89
+ }
47
90
  }
48
91
  //# sourceMappingURL=RateLimiter.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"RateLimiter.js","sourceRoot":"","sources":["../../src/services/RateLimiter.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,OAAO,WAAW;IAKH;IACA;IALF,OAAO,GAAG,IAAI,GAAG,EAAkD,CAAA;IAC5E,SAAS,GAAG,MAAM,CAAC,iBAAiB,CAAA;IAE5C,YACmB,KAAY,EACZ,OAA4C;QAD5C,UAAK,GAAL,KAAK,CAAO;QACZ,YAAO,GAAP,OAAO,CAAqC;IAC5D,CAAC;IAEJ,gGAAgG;IAChG,IAAI,CAAC,GAAW;QACd,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,CAAA;QACtC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;QACnC,IAAI,CAAC,KAAK,IAAI,GAAG,GAAG,KAAK,CAAC,WAAW,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YAC/D,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,WAAW,EAAE,GAAG,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC,CAAA;YACrD,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YACf,OAAO,IAAI,CAAA;QACb,CAAC;QACD,IAAI,KAAK,CAAC,KAAK,IAAI,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;YACtC,OAAO,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,WAAW,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,GAAG,GAAG,CAAC,GAAG,IAAI,CAAC,CAAA;QAC5E,CAAC;QACD,KAAK,CAAC,KAAK,IAAI,CAAC,CAAA;QAChB,OAAO,IAAI,CAAA;IACb,CAAC;IAED;;;;;;;OAOG;IACK,KAAK,CAAC,GAAW;QACvB,IAAI,GAAG,GAAG,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ;YAAE,OAAM;QACxD,IAAI,CAAC,SAAS,GAAG,GAAG,CAAA;QACpB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACxC,IAAI,GAAG,GAAG,KAAK,CAAC,WAAW,IAAI,IAAI,CAAC,OAAO,CAAC,QAAQ;gBAAE,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAA;QAChF,CAAC;IACH,CAAC;CACF"}
1
+ {"version":3,"file":"RateLimiter.js","sourceRoot":"","sources":["../../src/services/RateLimiter.ts"],"names":[],"mappings":"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"}
@@ -12,6 +12,17 @@ export interface RetentionPolicy {
12
12
  * the ordinary end of a match on a public server, and nothing else ever collected it.
13
13
  */
14
14
  abandonedLiveMaxAgeMs: number;
15
+ /**
16
+ * Age after which a running or desynced match is deleted whatever its roster says. 0 keeps them
17
+ * forever.
18
+ *
19
+ * The roster test alone is not enough to collect anything. A player stops being `active` through
20
+ * `leave`, `kick` or a missed deadline, and the game's default is an untimed match with no
21
+ * deadline to miss, so two friends whose clients both died leave two `active` rows that nothing
22
+ * ever clears. That is the ordinary end of an untimed match, and without this window those
23
+ * matches, their orders, their events and up to five megabytes of snapshot are immortal.
24
+ */
25
+ silentLiveMaxAgeMs: number;
15
26
  /** Matches deleted per sweep, so one pass cannot monopolise the database. */
16
27
  batchSize: number;
17
28
  }
@@ -33,5 +44,9 @@ export declare class RetentionService {
33
44
  collect(): Promise<number>;
34
45
  private collectTerminated;
35
46
  private collectAbandonedLive;
47
+ /** The same delete without the roster test, on a window long enough to stand in for it. */
48
+ private collectSilentLive;
49
+ /** One retention window: a window of 0 is switched off and reads neither the clock nor storage. */
50
+ private collectWindow;
36
51
  }
37
52
  //# sourceMappingURL=RetentionService.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"RetentionService.d.ts","sourceRoot":"","sources":["../../src/services/RetentionService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAKxC,MAAM,WAAW,eAAe;IAC9B,mGAAmG;IACnG,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;;;;OAQG;IACH,qBAAqB,EAAE,MAAM,CAAA;IAC7B,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAA;CAClB;AAID,eAAO,MAAM,iBAAiB,EAAE,eAI/B,CAAA;AAED;;;;;;;;;GASG;AACH,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM;gBADN,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC,EACtD,MAAM,GAAE,eAAmC;IAGxD,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC;YAIlB,iBAAiB;YAiBjB,oBAAoB;CAenC"}
1
+ {"version":3,"file":"RetentionService.d.ts","sourceRoot":"","sources":["../../src/services/RetentionService.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,QAAQ,CAAA;AAKxC,MAAM,WAAW,eAAe;IAC9B,mGAAmG;IACnG,QAAQ,EAAE,MAAM,CAAA;IAChB;;;;;;;;OAQG;IACH,qBAAqB,EAAE,MAAM,CAAA;IAC7B;;;;;;;;;OASG;IACH,kBAAkB,EAAE,MAAM,CAAA;IAC1B,6EAA6E;IAC7E,SAAS,EAAE,MAAM,CAAA;CAClB;AAID,eAAO,MAAM,iBAAiB,EAAE,eAK/B,CAAA;AAED;;;;;;;;;GASG;AACH,qBAAa,gBAAgB;IAEzB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAFzB,YACmB,IAAI,EAAE,IAAI,CAAC,UAAU,EAAE,SAAS,GAAG,OAAO,GAAG,QAAQ,CAAC,EACtD,MAAM,GAAE,eAAmC,EAC1D;IAEE,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAM/B;IAED,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,oBAAoB;IAS5B,2FAA2F;IAC3F,OAAO,CAAC,iBAAiB;IASzB,mGAAmG;YACrF,aAAa;CAW5B"}
@@ -4,6 +4,7 @@ const DAY_MS = 24 * 60 * 60 * 1000;
4
4
  export const DEFAULT_RETENTION = {
5
5
  maxAgeMs: 30 * DAY_MS,
6
6
  abandonedLiveMaxAgeMs: 90 * DAY_MS,
7
+ silentLiveMaxAgeMs: 180 * DAY_MS,
7
8
  batchSize: 50,
8
9
  };
9
10
  /**
@@ -24,32 +25,28 @@ export class RetentionService {
24
25
  this.policy = policy;
25
26
  }
26
27
  async collect() {
27
- return (await this.collectTerminated()) + (await this.collectAbandonedLive());
28
+ return ((await this.collectTerminated()) +
29
+ (await this.collectAbandonedLive()) +
30
+ (await this.collectSilentLive()));
28
31
  }
29
- async collectTerminated() {
30
- if (this.policy.maxAgeMs <= 0)
31
- return 0;
32
- const before = new Date(this.deps.clock.now().getTime() - this.policy.maxAgeMs);
33
- const deleted = await this.deps.storage.matches.deleteInactive(COLLECTABLE, before, this.policy.batchSize);
34
- if (deleted > 0) {
35
- this.deps.logger.info('retention deleted inactive matches', {
36
- deleted,
37
- before: before.toISOString(),
38
- });
39
- }
40
- return deleted;
32
+ collectTerminated() {
33
+ return this.collectWindow(this.policy.maxAgeMs, 'retention deleted inactive matches', (before) => this.deps.storage.matches.deleteInactive(COLLECTABLE, before, this.policy.batchSize));
34
+ }
35
+ collectAbandonedLive() {
36
+ return this.collectWindow(this.policy.abandonedLiveMaxAgeMs, 'retention deleted long-abandoned live matches', (before) => this.deps.storage.matches.deleteAbandonedLive(before, this.policy.batchSize, true));
37
+ }
38
+ /** The same delete without the roster test, on a window long enough to stand in for it. */
39
+ collectSilentLive() {
40
+ return this.collectWindow(this.policy.silentLiveMaxAgeMs, 'retention deleted silent live matches', (before) => this.deps.storage.matches.deleteAbandonedLive(before, this.policy.batchSize, false));
41
41
  }
42
- async collectAbandonedLive() {
43
- if (this.policy.abandonedLiveMaxAgeMs <= 0)
42
+ /** One retention window: a window of 0 is switched off and reads neither the clock nor storage. */
43
+ async collectWindow(maxAgeMs, message, run) {
44
+ if (maxAgeMs <= 0)
44
45
  return 0;
45
- const before = new Date(this.deps.clock.now().getTime() - this.policy.abandonedLiveMaxAgeMs);
46
- const deleted = await this.deps.storage.matches.deleteAbandonedLive(before, this.policy.batchSize);
47
- if (deleted > 0) {
48
- this.deps.logger.info('retention deleted long-abandoned live matches', {
49
- deleted,
50
- before: before.toISOString(),
51
- });
52
- }
46
+ const before = new Date(this.deps.clock.now().getTime() - maxAgeMs);
47
+ const deleted = await run(before);
48
+ if (deleted > 0)
49
+ this.deps.logger.info(message, { deleted, before: before.toISOString() });
53
50
  return deleted;
54
51
  }
55
52
  }
@@ -1 +1 @@
1
- {"version":3,"file":"RetentionService.js","sourceRoot":"","sources":["../../src/services/RetentionService.ts"],"names":[],"mappings":"AAGA,qGAAqG;AACrG,MAAM,WAAW,GAA2B,CAAC,UAAU,EAAE,WAAW,EAAE,OAAO,CAAC,CAAA;AAmB9E,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAElC,MAAM,CAAC,MAAM,iBAAiB,GAAoB;IAChD,QAAQ,EAAE,EAAE,GAAG,MAAM;IACrB,qBAAqB,EAAE,EAAE,GAAG,MAAM;IAClC,SAAS,EAAE,EAAE;CACd,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,OAAO,gBAAgB;IAER;IACA;IAFnB,YACmB,IAAsD,EACtD,SAA0B,iBAAiB;QAD3C,SAAI,GAAJ,IAAI,CAAkD;QACtD,WAAM,GAAN,MAAM,CAAqC;IAC3D,CAAC;IAEJ,KAAK,CAAC,OAAO;QACX,OAAO,CAAC,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,oBAAoB,EAAE,CAAC,CAAA;IAC/E,CAAC;IAEO,KAAK,CAAC,iBAAiB;QAC7B,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC;YAAE,OAAO,CAAC,CAAA;QACvC,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAA;QAC/E,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAC5D,WAAW,EACX,MAAM,EACN,IAAI,CAAC,MAAM,CAAC,SAAS,CACtB,CAAA;QACD,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;YAChB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,oCAAoC,EAAE;gBAC1D,OAAO;gBACP,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE;aAC7B,CAAC,CAAA;QACJ,CAAC;QACD,OAAO,OAAO,CAAA;IAChB,CAAC;IAEO,KAAK,CAAC,oBAAoB;QAChC,IAAI,IAAI,CAAC,MAAM,CAAC,qBAAqB,IAAI,CAAC;YAAE,OAAO,CAAC,CAAA;QACpD,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,qBAAqB,CAAC,CAAA;QAC5F,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CACjE,MAAM,EACN,IAAI,CAAC,MAAM,CAAC,SAAS,CACtB,CAAA;QACD,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;YAChB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,+CAA+C,EAAE;gBACrE,OAAO;gBACP,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE;aAC7B,CAAC,CAAA;QACJ,CAAC;QACD,OAAO,OAAO,CAAA;IAChB,CAAC;CACF"}
1
+ {"version":3,"file":"RetentionService.js","sourceRoot":"","sources":["../../src/services/RetentionService.ts"],"names":[],"mappings":"AAGA,qGAAqG;AACrG,MAAM,WAAW,GAA2B,CAAC,UAAU,EAAE,WAAW,EAAE,OAAO,CAAC,CAAA;AA8B9E,MAAM,MAAM,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAElC,MAAM,CAAC,MAAM,iBAAiB,GAAoB;IAChD,QAAQ,EAAE,EAAE,GAAG,MAAM;IACrB,qBAAqB,EAAE,EAAE,GAAG,MAAM;IAClC,kBAAkB,EAAE,GAAG,GAAG,MAAM;IAChC,SAAS,EAAE,EAAE;CACd,CAAA;AAED;;;;;;;;;GASG;AACH,MAAM,OAAO,gBAAgB;IAER,IAAI;IACJ,MAAM;IAFzB,YACmB,IAAsD,EACtD,MAAM,GAAoB,iBAAiB;oBAD3C,IAAI;sBACJ,MAAM;IACtB,CAAC;IAEJ,KAAK,CAAC,OAAO;QACX,OAAO,CACL,CAAC,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC;YAChC,CAAC,MAAM,IAAI,CAAC,oBAAoB,EAAE,CAAC;YACnC,CAAC,MAAM,IAAI,CAAC,iBAAiB,EAAE,CAAC,CACjC,CAAA;IACH,CAAC;IAEO,iBAAiB;QACvB,OAAO,IAAI,CAAC,aAAa,CACvB,IAAI,CAAC,MAAM,CAAC,QAAQ,EACpB,oCAAoC,EACpC,CAAC,MAAM,EAAE,EAAE,CACT,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CACvF,CAAA;IACH,CAAC;IAEO,oBAAoB;QAC1B,OAAO,IAAI,CAAC,aAAa,CACvB,IAAI,CAAC,MAAM,CAAC,qBAAqB,EACjC,+CAA+C,EAC/C,CAAC,MAAM,EAAE,EAAE,CACT,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CACrF,CAAA;IACH,CAAC;IAED,2FAA2F;IACnF,iBAAiB;QACvB,OAAO,IAAI,CAAC,aAAa,CACvB,IAAI,CAAC,MAAM,CAAC,kBAAkB,EAC9B,uCAAuC,EACvC,CAAC,MAAM,EAAE,EAAE,CACT,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,mBAAmB,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,KAAK,CAAC,CACtF,CAAA;IACH,CAAC;IAED,mGAAmG;IAC3F,KAAK,CAAC,aAAa,CACzB,QAAgB,EAChB,OAAe,EACf,GAAsC;QAEtC,IAAI,QAAQ,IAAI,CAAC;YAAE,OAAO,CAAC,CAAA;QAC3B,MAAM,MAAM,GAAG,IAAI,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,CAAA;QACnE,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,MAAM,CAAC,CAAA;QACjC,IAAI,OAAO,GAAG,CAAC;YAAE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC,CAAA;QAC1F,OAAO,OAAO,CAAA;IAChB,CAAC;CACF"}
@@ -14,23 +14,69 @@ export declare class SnapshotService {
14
14
  private readonly turns;
15
15
  constructor(deps: KernelDeps, publisher: EventPublisher, turns: TurnService);
16
16
  upload(principal: Principal, request: UploadSnapshotRequest): Promise<void>;
17
+ /**
18
+ * A periodic checkpoint of a turn the match has already agreed on.
19
+ *
20
+ * Only two kinds of snapshot used to exist, the host's turn-0 bootstrap and desync repairs, so a
21
+ * client reconnecting at turn 80 replayed eighty turns from the bootstrap — eighty sequential
22
+ * fetches, each under its own deadline, and eighty full turn resolutions, before the player saw
23
+ * anything. The design doc talks about matches of hundreds of turns.
24
+ *
25
+ * It is not a second way to say what a turn's state was. The turn is CONFIRMED, so the verdict
26
+ * has already been reached and written on the turn row, and the checkpoint is refused unless it
27
+ * claims exactly that hash: it can only agree with what every client already computed. The host
28
+ * writes it because the host is the one client the protocol already asks for bytes, storage stays
29
+ * bounded by the per-match snapshot pruning, and the rate stays bounded by the upload budget.
30
+ *
31
+ * Nothing is announced. A checkpoint changes nothing about the match; it is there for whoever
32
+ * reconnects next, and `getLatest` is what finds it.
33
+ */
34
+ private storeCheckpoint;
35
+ /**
36
+ * Write an accepted snapshot: the row, the seat summaries it carries, then the prune.
37
+ *
38
+ * The merge is judged before anything is written: a refusal after the row landed would leave a
39
+ * stored snapshot the caller was told was rejected, with no `snapshot.available` and no verdict.
40
+ * Re-running the settings schema over the merge is what holds the blob to its cap, which is why
41
+ * every upload path comes through here before its write.
42
+ */
43
+ private store;
44
+ private snapshotOf;
17
45
  /**
18
46
  * Late-join hints are optional metadata; the snapshot the players are waiting on is not. A
19
47
  * transient failure of this write is logged and never fails the upload that already succeeded.
20
48
  */
21
49
  private publishSeatSummaries;
22
50
  /**
23
- * A recovery snapshot may only claim a state hash that the players themselves already computed
24
- * in the greatest number.
51
+ * A repair must name the turn that actually diverged.
52
+ *
53
+ * "Below `currentTurn` while the match is desynced" was too loose, and the corroboration check
54
+ * below cannot make up the difference: it counts reports, and a turn with none — the successor a
55
+ * seal opened before anybody had reported on it — has no candidates to contradict, so it returned
56
+ * early and let the host store any hash it liked for that turn. `evaluateConsensus` then had an
57
+ * authoritative hash to judge against and could only confirm or wait, never desync. That is
58
+ * exactly the "host arbitrates a disagreement it is a party to" case the corroboration rule
59
+ * exists to prevent; it needs the turn's own status to close it.
60
+ */
61
+ private requireDesyncedTurn;
62
+ /**
63
+ * Who may post a repair, and which hash they may claim.
64
+ *
65
+ * The hash is the hard rule and it binds everyone: the snapshot for a desynced turn becomes the
66
+ * state every other client is told to converge on, so a client free to name any hash could
67
+ * resolve a deliberate desync in favour of a doctored one. A hash that more active players
68
+ * reported than any other cannot be minted by one of them.
25
69
  *
26
- * This is what keeps the host from arbitrating a disagreement it is a party to. The snapshot for
27
- * a desynced turn becomes the hash every other client is told to converge on, so a host free to
28
- * name any hash could resolve a deliberate desync in favour of a doctored state. A hash that more
29
- * active players reported than any other cannot be minted by one of them. A tie leaves nothing to
30
- * count and the host breaks it; turns with no reports at all (a bootstrap snapshot for a
31
- * reconnecting client) are unconstrained, because there is no consensus to contradict yet.
70
+ * Who may post it follows from that. A hash that is the SOLE most-reported one is already the
71
+ * majority's, so whoever holds it may post the repair whether or not they are the host. That is
72
+ * the one case a host-only rule could not serve at all: a host that is itself the odd one out can
73
+ * never name a majority hash, so no repair existed and the match stayed paused until retention
74
+ * collected it. A TIE — most of all the 1-1 split of a two-player match — is different: there is
75
+ * no majority, nothing to count, and no third party to ask, so the host breaks it as before and a
76
+ * peer may not. Letting either side of a tie impose its state would hand a two-player match to
77
+ * whoever uploaded first.
32
78
  */
33
- private requireCorroboration;
79
+ private requireRepairAuthority;
34
80
  /**
35
81
  * Bound what one live match holds. A snapshot is a megabyte of base64 and a long match can desync
36
82
  * many times; retention only collects matches that are over, so without this a single match that
@@ -1 +1 @@
1
- {"version":3,"file":"SnapshotService.d.ts","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EAEjB,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC3B,MAAM,4BAA4B,CAAA;AAKnC,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;AACtD,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAQhD;;;;GAIG;AACH,qBAAa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAFL,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,cAAc,EACzB,KAAK,EAAE,WAAW;IAG/B,MAAM,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC;IAuDjF;;;OAGG;YACW,oBAAoB;IAYlC;;;;;;;;;;OAUG;YACW,oBAAoB;IAiBlC;;;;;;OAMG;YACW,iBAAiB;IASzB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;IAM9C,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC;CAKhE;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,YAAY,EAC1B,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,GACpD,YAAY,CASd"}
1
+ {"version":3,"file":"SnapshotService.d.ts","sourceRoot":"","sources":["../../src/services/SnapshotService.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,YAAY,EAEjB,KAAK,YAAY,EACjB,KAAK,qBAAqB,EAC3B,MAAM,4BAA4B,CAAA;AAKnC,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,KAAK,EAAE,WAAW,EAAE,MAAM,eAAe,CAAA;AAQhD;;;;GAIG;AACH,qBAAa,eAAe;IAExB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,KAAK;IAHxB,YACmB,IAAI,EAAE,UAAU,EAChB,SAAS,EAAE,cAAc,EACzB,KAAK,EAAE,WAAW,EACjC;IAEE,MAAM,CAAC,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6ChF;IAED;;;;;;;;;;;;;;;;OAgBG;YACW,eAAe;IAkB7B;;;;;;;OAOG;YACW,KAAK;IAWnB,OAAO,CAAC,UAAU;IAkBlB;;;OAGG;YACW,oBAAoB;IAYlC;;;;;;;;;;OAUG;IACH,OAAO,CAAC,mBAAmB;IAO3B;;;;;;;;;;;;;;;;OAgBG;YACW,sBAAsB;IAwBpC;;;;;;OAMG;YACW,iBAAiB;IASzB,MAAM,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAInD;IAEK,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAI9D;CACF;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,YAAY,EAAE,YAAY,EAC1B,aAAa,EAAE,qBAAqB,CAAC,eAAe,CAAC,GACpD,YAAY,CASd"}