@jgengine/core 0.26.0 → 0.28.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 (143) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/dist/ai/policeResponse.d.ts +253 -0
  3. package/dist/ai/policeResponse.js +421 -0
  4. package/dist/anim/actionLayer.d.ts +47 -0
  5. package/dist/anim/actionLayer.js +63 -0
  6. package/dist/anim/ikSolver.d.ts +14 -0
  7. package/dist/anim/ikSolver.js +37 -0
  8. package/dist/anim/locomotionGraph.d.ts +58 -3
  9. package/dist/anim/locomotionGraph.js +142 -15
  10. package/dist/anim/ragdollRig.d.ts +53 -0
  11. package/dist/anim/ragdollRig.js +132 -0
  12. package/dist/anim/skeletonStandard.d.ts +140 -0
  13. package/dist/anim/skeletonStandard.js +326 -0
  14. package/dist/combat/damageRuntime.d.ts +14 -1
  15. package/dist/combat/damageRuntime.js +62 -4
  16. package/dist/combat/death.js +7 -3
  17. package/dist/combat/deathReason.d.ts +7 -1
  18. package/dist/combat/effects.d.ts +25 -0
  19. package/dist/combat/effects.js +16 -0
  20. package/dist/combat/statusRuntime.d.ts +2 -0
  21. package/dist/combat/statusRuntime.js +1 -0
  22. package/dist/economy/loanBook.d.ts +182 -0
  23. package/dist/economy/loanBook.js +303 -0
  24. package/dist/economy/market.d.ts +161 -0
  25. package/dist/economy/market.js +248 -0
  26. package/dist/economy/transactionLedger.d.ts +162 -0
  27. package/dist/economy/transactionLedger.js +301 -0
  28. package/dist/editor/creatorStorage.js +3 -1
  29. package/dist/editor/simulation.d.ts +5 -2
  30. package/dist/editor/simulation.js +29 -19
  31. package/dist/game/cameraConfig.d.ts +14 -0
  32. package/dist/game/clipRoles.d.ts +5 -4
  33. package/dist/game/clipRoles.js +34 -4
  34. package/dist/game/events.d.ts +27 -0
  35. package/dist/game/gameSave.d.ts +142 -0
  36. package/dist/game/gameSave.js +174 -0
  37. package/dist/game/playableGame.d.ts +74 -6
  38. package/dist/game/saveSlotIndex.d.ts +54 -0
  39. package/dist/game/saveSlotIndex.js +71 -0
  40. package/dist/game/saveSlots.d.ts +2 -0
  41. package/dist/game/saveSlots.js +1 -1
  42. package/dist/game/saveStore.d.ts +8 -0
  43. package/dist/game/saveStore.js +16 -0
  44. package/dist/gameplay.d.ts +17 -2
  45. package/dist/gameplay.js +15 -0
  46. package/dist/inventory/carryLoad.d.ts +48 -0
  47. package/dist/inventory/carryLoad.js +44 -0
  48. package/dist/inventory/equipment.d.ts +95 -0
  49. package/dist/inventory/equipment.js +169 -0
  50. package/dist/inventory/inventoryModel.d.ts +15 -6
  51. package/dist/inventory/inventoryModel.js +54 -5
  52. package/dist/inventory/shapedGrid.d.ts +26 -0
  53. package/dist/inventory/shapedGrid.js +47 -0
  54. package/dist/loot/lootGen.d.ts +186 -0
  55. package/dist/loot/lootGen.js +437 -0
  56. package/dist/loot/lootWeapon.d.ts +35 -0
  57. package/dist/loot/lootWeapon.js +70 -0
  58. package/dist/meta/changelog.d.ts +1 -1
  59. package/dist/meta/changelog.js +156 -1
  60. package/dist/mission/compositions.d.ts +85 -0
  61. package/dist/mission/compositions.js +130 -0
  62. package/dist/mission/mission.d.ts +435 -0
  63. package/dist/mission/mission.js +770 -0
  64. package/dist/mission/triggers.d.ts +61 -0
  65. package/dist/mission/triggers.js +108 -0
  66. package/dist/nav/laneGraph.d.ts +178 -0
  67. package/dist/nav/laneGraph.js +671 -0
  68. package/dist/nav/traffic.d.ts +222 -0
  69. package/dist/nav/traffic.js +918 -0
  70. package/dist/physics/kinematicVehicle.d.ts +36 -0
  71. package/dist/physics/kinematicVehicle.js +15 -1
  72. package/dist/physics/vehicleDamage.d.ts +136 -0
  73. package/dist/physics/vehicleDamage.js +214 -0
  74. package/dist/physics/vehicleParts.d.ts +62 -0
  75. package/dist/physics/vehicleParts.js +65 -0
  76. package/dist/progression/gearScore.d.ts +56 -0
  77. package/dist/progression/gearScore.js +59 -0
  78. package/dist/progression/skillTree.d.ts +129 -0
  79. package/dist/progression/skillTree.js +265 -0
  80. package/dist/progression/xpCurve.d.ts +60 -0
  81. package/dist/progression/xpCurve.js +141 -0
  82. package/dist/runtime/context/combat.js +11 -0
  83. package/dist/scene/builtinSceneKinds.js +2 -0
  84. package/dist/settings/accessibilitySettings.d.ts +81 -0
  85. package/dist/settings/accessibilitySettings.js +152 -0
  86. package/dist/settings/graphicsProfile.d.ts +3 -0
  87. package/dist/settings/graphicsProfile.js +4 -0
  88. package/dist/settings/settingsModel.d.ts +1 -1
  89. package/dist/settings/settingsModel.js +1 -0
  90. package/dist/sim/nearAgents.d.ts +8 -1
  91. package/dist/sim/nearAgents.js +34 -17
  92. package/dist/sim/offScreen.d.ts +2 -1
  93. package/dist/sim/offScreen.js +14 -3
  94. package/dist/sim/population.d.ts +147 -0
  95. package/dist/sim/population.js +182 -0
  96. package/dist/sim/queue.d.ts +90 -0
  97. package/dist/sim/queue.js +171 -0
  98. package/dist/sim/rides.d.ts +287 -0
  99. package/dist/sim/rides.js +471 -0
  100. package/dist/sim/routine.js +19 -11
  101. package/dist/ui/loadingTips.d.ts +41 -0
  102. package/dist/ui/loadingTips.js +64 -0
  103. package/dist/ui/menuNav.d.ts +79 -0
  104. package/dist/ui/menuNav.js +147 -0
  105. package/dist/ui/menuTransition.d.ts +50 -0
  106. package/dist/ui/menuTransition.js +67 -0
  107. package/dist/ui.d.ts +2 -0
  108. package/dist/ui.js +2 -0
  109. package/dist/vfx/effectLibrary.d.ts +98 -0
  110. package/dist/vfx/effectLibrary.js +181 -0
  111. package/dist/vfx/particleBudget.d.ts +40 -0
  112. package/dist/vfx/particleBudget.js +39 -0
  113. package/dist/vfx/particleDirector.d.ts +16 -0
  114. package/dist/vfx/particleDirector.js +18 -0
  115. package/dist/world/authoredSimulation.js +7 -1
  116. package/dist/world/captureCamera.d.ts +30 -0
  117. package/dist/world/captureCamera.js +19 -0
  118. package/dist/world/decalKind.d.ts +48 -0
  119. package/dist/world/decalKind.js +86 -0
  120. package/dist/world/features.d.ts +18 -5
  121. package/dist/world/features.js +1 -0
  122. package/dist/world/grassKind.d.ts +5 -0
  123. package/dist/world/grassKind.js +3 -0
  124. package/dist/world/terraform.d.ts +8 -0
  125. package/dist/world/terrain.d.ts +1 -0
  126. package/dist/world/terrain.js +2 -0
  127. package/dist/world.d.ts +7 -2
  128. package/dist/world.js +5 -0
  129. package/package.json +1 -1
  130. package/skills/jgengine/api.md +2 -2
  131. package/skills/jgengine-combat/SKILL.md +9 -1
  132. package/skills/jgengine-combat/api.md +5 -4
  133. package/skills/jgengine-combat/reference.md +4 -0
  134. package/skills/jgengine-gameplay/SKILL.md +86 -0
  135. package/skills/jgengine-gameplay/api.md +485 -16
  136. package/skills/jgengine-gameplay/capabilities.md +131 -4
  137. package/skills/jgengine-gameplay/reference-systems.md +118 -0
  138. package/skills/jgengine-world/SKILL.md +8 -0
  139. package/skills/jgengine-world/api.md +243 -11
  140. package/skills/jgengine-world/capabilities.md +61 -1
  141. package/skills/jgengine-world/recipes/character-animation.md +25 -2
  142. package/skills/jgengine-world/recipes/vehicle-feel.md +5 -0
  143. package/skills/jgengine-world/reference.md +1 -1
@@ -0,0 +1,182 @@
1
+ import { devtools } from "../devtools/devtools.js";
2
+ const DEFAULT_ARRIVAL_BUDGET = 4;
3
+ function countOf(value) {
4
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
5
+ }
6
+ /**
7
+ * Create the population a scene reports, with the game owning arrival and leaving policy. A
8
+ * visitor only exists once the world has made its body, so the HUD's count and the bodies on
9
+ * screen are the same list: `admit` returning `null` is a refused arrival, not a number.
10
+ *
11
+ * @capability visitor-population a crowd whose on-site and daily counts are the bodies the game made
12
+ */
13
+ export function createVisitorPopulation(options) {
14
+ const world = options.world;
15
+ let policy = { ...options.policy };
16
+ const idPrefix = options.idPrefix ?? "visitor#";
17
+ let arrivalBudget = Math.max(1, Math.floor(options.arrivalBudget ?? DEFAULT_ARRIVAL_BUDGET));
18
+ let inside = options.inside;
19
+ let members = new Map();
20
+ let sequence = Math.max(1, Math.floor(options.sequenceStart ?? 1));
21
+ let arrivalsToday = 0;
22
+ let departuresToday = 0;
23
+ let peakToday = 0;
24
+ let refused = 0;
25
+ let pending = 0;
26
+ let now = 0;
27
+ let capacityNow = 0;
28
+ function resolveCapacity(at) {
29
+ const value = typeof policy.capacity === "function" ? policy.capacity(at) : policy.capacity;
30
+ return Math.max(0, Math.floor(Number.isFinite(value) ? value : 0));
31
+ }
32
+ capacityNow = resolveCapacity(0);
33
+ function flow() {
34
+ return { onSite: members.size, capacity: capacityNow, arrivalsToday, departuresToday, now };
35
+ }
36
+ function admitOne(at) {
37
+ const visitor = { id: `${idPrefix}${sequence}`, admittedAt: at };
38
+ sequence += 1;
39
+ const position = world.admit(visitor);
40
+ if (position === null || (inside !== undefined && !inside(position))) {
41
+ if (position !== null)
42
+ world.release(visitor);
43
+ refused += 1;
44
+ return false;
45
+ }
46
+ members.set(visitor.id, visitor);
47
+ arrivalsToday += 1;
48
+ if (members.size > peakToday)
49
+ peakToday = members.size;
50
+ return true;
51
+ }
52
+ function removeVisitor(visitor) {
53
+ members.delete(visitor.id);
54
+ world.release(visitor);
55
+ departuresToday += 1;
56
+ }
57
+ function probeLine() {
58
+ return `${members.size}/${capacityNow} on site ${arrivalsToday} arrived ${departuresToday} left ${refused} refused`;
59
+ }
60
+ const unregister = options.devtoolsName === undefined ? null : devtools.probes.register(options.devtoolsName, () => probeLine());
61
+ return {
62
+ tick(dt, at) {
63
+ if (!(dt > 0) || !Number.isFinite(at))
64
+ return;
65
+ now = at;
66
+ capacityNow = resolveCapacity(at);
67
+ const read = flow();
68
+ const leave = policy.shouldLeave;
69
+ if (leave !== undefined) {
70
+ for (const visitor of members.values()) {
71
+ if (leave(visitor, read))
72
+ removeVisitor(visitor);
73
+ }
74
+ }
75
+ if (members.size < capacityNow)
76
+ pending += Math.max(0, policy.arrivalRate(read)) * dt;
77
+ let budget = arrivalBudget;
78
+ while (pending >= 1 && budget > 0 && members.size < capacityNow) {
79
+ pending -= 1;
80
+ budget -= 1;
81
+ admitOne(at);
82
+ }
83
+ if (pending < 0)
84
+ pending = 0;
85
+ },
86
+ onSite: () => members.size,
87
+ arrivalsToday: () => arrivalsToday,
88
+ departuresToday: () => departuresToday,
89
+ peakToday: () => peakToday,
90
+ ids() {
91
+ const out = [];
92
+ for (const id of members.keys())
93
+ out.push(id);
94
+ return out;
95
+ },
96
+ has: (id) => members.has(id),
97
+ visitor: (id) => members.get(id) ?? null,
98
+ leave(id) {
99
+ const visitor = members.get(id);
100
+ if (visitor === undefined)
101
+ return false;
102
+ removeVisitor(visitor);
103
+ return true;
104
+ },
105
+ adopt(id, adoption) {
106
+ if (id === "" || members.has(id) || !world.has(id))
107
+ return false;
108
+ const saved = adoption?.admittedAt;
109
+ const at = saved !== undefined && Number.isFinite(saved) ? saved : now;
110
+ members.set(id, { id, admittedAt: at });
111
+ if (members.size > peakToday)
112
+ peakToday = members.size;
113
+ if (adoption?.arrival === true)
114
+ arrivalsToday += 1;
115
+ return true;
116
+ },
117
+ rollDay() {
118
+ arrivalsToday = 0;
119
+ departuresToday = 0;
120
+ peakToday = members.size;
121
+ },
122
+ retune(patch) {
123
+ if (patch.capacity !== undefined) {
124
+ policy = { ...policy, capacity: patch.capacity };
125
+ capacityNow = resolveCapacity(now);
126
+ }
127
+ if (patch.arrivalRate !== undefined)
128
+ policy = { ...policy, arrivalRate: patch.arrivalRate };
129
+ if (patch.shouldLeave === null)
130
+ policy = { capacity: policy.capacity, arrivalRate: policy.arrivalRate };
131
+ else if (patch.shouldLeave !== undefined)
132
+ policy = { ...policy, shouldLeave: patch.shouldLeave };
133
+ if (patch.arrivalBudget !== undefined) {
134
+ arrivalBudget = Math.max(1, Math.floor(Number.isFinite(patch.arrivalBudget) ? patch.arrivalBudget : arrivalBudget));
135
+ }
136
+ if (patch.inside === null)
137
+ inside = undefined;
138
+ else if (patch.inside !== undefined)
139
+ inside = patch.inside;
140
+ },
141
+ stats: () => ({
142
+ onSite: members.size,
143
+ capacity: capacityNow,
144
+ arrivalsToday,
145
+ departuresToday,
146
+ peakToday,
147
+ refused,
148
+ pending,
149
+ }),
150
+ snapshot() {
151
+ const visitors = [];
152
+ for (const visitor of members.values())
153
+ visitors.push({ id: visitor.id, admittedAt: visitor.admittedAt });
154
+ return { visitors, arrivalsToday, departuresToday, peakToday, pending, sequence };
155
+ },
156
+ restore(next) {
157
+ for (const visitor of members.values())
158
+ world.release(visitor);
159
+ members = new Map();
160
+ arrivalsToday = countOf(next.arrivalsToday);
161
+ departuresToday = countOf(next.departuresToday);
162
+ peakToday = countOf(next.peakToday);
163
+ pending = Number.isFinite(next.pending) && next.pending > 0 ? next.pending : 0;
164
+ sequence = Math.max(1, Math.floor(Number.isFinite(next.sequence) ? next.sequence : 1));
165
+ for (const saved of next.visitors) {
166
+ if (typeof saved?.id !== "string" || saved.id === "" || members.has(saved.id))
167
+ continue;
168
+ if (!world.has(saved.id)) {
169
+ refused += 1;
170
+ continue;
171
+ }
172
+ const at = Number.isFinite(saved.admittedAt) ? saved.admittedAt : 0;
173
+ members.set(saved.id, { id: saved.id, admittedAt: at });
174
+ }
175
+ if (members.size > peakToday)
176
+ peakToday = members.size;
177
+ },
178
+ dispose() {
179
+ unregister?.();
180
+ },
181
+ };
182
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Seconds one guest occupies the head of the line before any boarding interval has been measured.
3
+ * A bootstrap only: the estimate switches to the queue's own measured throughput as soon as two
4
+ * boardings have been observed.
5
+ */
6
+ export declare const DEFAULT_QUEUE_SEED_SERVICE_SECONDS = 8;
7
+ /** Boarding events the throughput window keeps. */
8
+ export declare const DEFAULT_QUEUE_WINDOW = 8;
9
+ /** One guest waiting their turn, and when they joined the line. */
10
+ export interface QueueEntry {
11
+ readonly id: string;
12
+ readonly joinedAt: number;
13
+ }
14
+ /** A wait estimate read off the queue's own measured throughput. */
15
+ export interface QueueWait {
16
+ /** Guests in front of the reader (the whole line for a new arrival). */
17
+ readonly ahead: number;
18
+ /** Seconds that wait. */
19
+ readonly seconds: number;
20
+ /** Seconds one guest spends at the head; the seed until an interval has been measured. */
21
+ readonly perGuestSeconds: number;
22
+ /** Guests boarded per second over the measured window; `0` before any interval is observed. */
23
+ readonly throughput: number;
24
+ /** True while the estimate still rests on the seed rather than a measured interval. */
25
+ readonly seeded: boolean;
26
+ }
27
+ /** Tunables for {@link createWaitQueue}, every one retunable through a restore. */
28
+ export interface WaitQueueOptions {
29
+ /** Seconds per guest used before the queue has measured an interval. Default 8. */
30
+ readonly seedServiceSeconds?: number;
31
+ /** Boarding events the throughput window keeps. Default 8. */
32
+ readonly window?: number;
33
+ /** Register a devtools probe under this name so the perf overlay shows the live line. */
34
+ readonly devtoolsName?: string;
35
+ }
36
+ /** Serializable line state: who is waiting, and the boardings the estimate is measured from. */
37
+ export interface WaitQueueSnapshot {
38
+ entries: QueueEntry[];
39
+ boards: {
40
+ at: number;
41
+ count: number;
42
+ }[];
43
+ seedServiceSeconds: number;
44
+ window: number;
45
+ }
46
+ /**
47
+ * A real line: guests join the back, leave the head, and the wait a new arrival faces comes from how
48
+ * fast this queue has actually been boarding — not from a configured constant. A broken ride stops
49
+ * boarding, the line grows, and the estimate grows with it.
50
+ */
51
+ export interface WaitQueue {
52
+ /** Join the back of the line; false when the id is already waiting. */
53
+ enqueue(id: string, now: number): boolean;
54
+ /** Leave from anywhere in the line. */
55
+ remove(id: string): boolean;
56
+ has(id: string): boolean;
57
+ /** 0-based place in line, or `-1` when not waiting. */
58
+ positionOf(id: string): number;
59
+ /** When the guest joined, or `null` when not waiting. */
60
+ joinedAt(id: string): number | null;
61
+ size(): number;
62
+ headId(): string | null;
63
+ /** Id at a 0-based index from the head, or `null` past the end. */
64
+ at(index: number): string | null;
65
+ /** Everyone in line, head first. Allocates — debug and tests. */
66
+ ids(): readonly string[];
67
+ /** Take the guest at the head, or `null` when the line is empty. */
68
+ dequeue(): string | null;
69
+ /** Record that `count` guests left the head at `now`; the throughput window reads these. */
70
+ recordBoard(now: number, count: number): void;
71
+ /**
72
+ * Wait facing `id`, or the whole line when `id` is omitted or not waiting — what a guest joining
73
+ * now would face.
74
+ */
75
+ wait(id?: string): QueueWait;
76
+ snapshot(): WaitQueueSnapshot;
77
+ restore(next: WaitQueueSnapshot): void;
78
+ /** Drop the devtools probe registered by `devtoolsName`. Safe to call twice. */
79
+ dispose(): void;
80
+ }
81
+ /**
82
+ * Create a line whose wait estimate is derived from its own throughput. The queue owns the order and
83
+ * the measurement; the ride owns how fast guests are taken off the head, so a slower cycle raises the
84
+ * estimate without the queue being told a cycle time.
85
+ *
86
+ * `enqueue` is O(1) and `dequeue` amortized O(1) — a long line never re-indexes.
87
+ *
88
+ * @capability wait-queue a real line whose wait estimate comes from its own measured throughput
89
+ */
90
+ export declare function createWaitQueue(options?: WaitQueueOptions): WaitQueue;
@@ -0,0 +1,171 @@
1
+ import { devtools } from "../devtools/devtools.js";
2
+ /**
3
+ * Seconds one guest occupies the head of the line before any boarding interval has been measured.
4
+ * A bootstrap only: the estimate switches to the queue's own measured throughput as soon as two
5
+ * boardings have been observed.
6
+ */
7
+ export const DEFAULT_QUEUE_SEED_SERVICE_SECONDS = 8;
8
+ /** Boarding events the throughput window keeps. */
9
+ export const DEFAULT_QUEUE_WINDOW = 8;
10
+ /**
11
+ * Create a line whose wait estimate is derived from its own throughput. The queue owns the order and
12
+ * the measurement; the ride owns how fast guests are taken off the head, so a slower cycle raises the
13
+ * estimate without the queue being told a cycle time.
14
+ *
15
+ * `enqueue` is O(1) and `dequeue` amortized O(1) — a long line never re-indexes.
16
+ *
17
+ * @capability wait-queue a real line whose wait estimate comes from its own measured throughput
18
+ */
19
+ export function createWaitQueue(options = {}) {
20
+ let seedServiceSeconds = options.seedServiceSeconds ?? DEFAULT_QUEUE_SEED_SERVICE_SECONDS;
21
+ let window = Math.max(2, Math.floor(options.window ?? DEFAULT_QUEUE_WINDOW));
22
+ let entries = [];
23
+ let boards = [];
24
+ let head = 0;
25
+ const waiting = new Set();
26
+ const live = () => entries.length - head;
27
+ function compact() {
28
+ if (head > 32 && head * 2 >= entries.length) {
29
+ entries = entries.slice(head);
30
+ head = 0;
31
+ }
32
+ }
33
+ /** Guests boarded per second, from consecutive boarding intervals; `0` until one is observed. */
34
+ function throughput() {
35
+ if (boards.length < 2)
36
+ return 0;
37
+ const first = boards[0];
38
+ const last = boards[boards.length - 1];
39
+ const span = last.at - first.at;
40
+ if (!(span > 0))
41
+ return 0;
42
+ let guests = 0;
43
+ for (let index = 1; index < boards.length; index += 1)
44
+ guests += boards[index].count;
45
+ return guests / span;
46
+ }
47
+ function readWait(ahead) {
48
+ const rate = throughput();
49
+ const perGuestSeconds = rate > 0 ? 1 / rate : Math.max(0, seedServiceSeconds);
50
+ return {
51
+ ahead,
52
+ seconds: ahead * perGuestSeconds,
53
+ perGuestSeconds,
54
+ throughput: rate,
55
+ seeded: rate <= 0,
56
+ };
57
+ }
58
+ const unregister = options.devtoolsName === undefined ? null : devtools.probes.register(options.devtoolsName, () => probeLine());
59
+ function probeLine() {
60
+ const wait = readWait(live());
61
+ return `${live()} in line · ${wait.seconds.toFixed(1)}s wait · ${wait.perGuestSeconds.toFixed(2)}s per guest${wait.seeded ? " (seed)" : ""}`;
62
+ }
63
+ function indexOf(id) {
64
+ for (let index = head; index < entries.length; index += 1) {
65
+ if (entries[index].id === id)
66
+ return index;
67
+ }
68
+ return -1;
69
+ }
70
+ function positionOfId(id) {
71
+ const at = indexOf(id);
72
+ return at < 0 ? -1 : at - head;
73
+ }
74
+ return {
75
+ enqueue(id, now) {
76
+ if (waiting.has(id))
77
+ return false;
78
+ entries.push({ id, joinedAt: Number.isFinite(now) ? now : 0 });
79
+ waiting.add(id);
80
+ return true;
81
+ },
82
+ remove(id) {
83
+ if (!waiting.has(id))
84
+ return false;
85
+ waiting.delete(id);
86
+ const at = indexOf(id);
87
+ if (at === head) {
88
+ head += 1;
89
+ compact();
90
+ return true;
91
+ }
92
+ entries.splice(at, 1);
93
+ return true;
94
+ },
95
+ has: (id) => waiting.has(id),
96
+ positionOf: positionOfId,
97
+ joinedAt(id) {
98
+ if (!waiting.has(id))
99
+ return null;
100
+ const at = indexOf(id);
101
+ return at < 0 ? null : entries[at].joinedAt;
102
+ },
103
+ size: live,
104
+ headId: () => entries[head]?.id ?? null,
105
+ at(index) {
106
+ const absolute = head + Math.floor(index);
107
+ if (!(index >= 0) || absolute >= entries.length)
108
+ return null;
109
+ return entries[absolute]?.id ?? null;
110
+ },
111
+ ids() {
112
+ const out = [];
113
+ for (let index = head; index < entries.length; index += 1)
114
+ out.push(entries[index].id);
115
+ return out;
116
+ },
117
+ dequeue() {
118
+ const entry = entries[head];
119
+ if (entry === undefined)
120
+ return null;
121
+ head += 1;
122
+ waiting.delete(entry.id);
123
+ compact();
124
+ return entry.id;
125
+ },
126
+ recordBoard(now, count) {
127
+ const boarded = Math.floor(count);
128
+ if (!(boarded > 0) || !Number.isFinite(now))
129
+ return;
130
+ boards.push({ at: now, count: boarded });
131
+ if (boards.length > window)
132
+ boards = boards.slice(boards.length - window);
133
+ },
134
+ wait(id) {
135
+ const position = id === undefined ? -1 : positionOfId(id);
136
+ return readWait(position >= 0 ? position : live());
137
+ },
138
+ snapshot: () => ({
139
+ entries: entries.slice(head).map((entry) => ({ id: entry.id, joinedAt: entry.joinedAt })),
140
+ boards: boards.map((board) => ({ at: board.at, count: board.count })),
141
+ seedServiceSeconds,
142
+ window,
143
+ }),
144
+ restore(next) {
145
+ entries = [];
146
+ head = 0;
147
+ waiting.clear();
148
+ for (const entry of next.entries) {
149
+ if (typeof entry?.id !== "string" || entry.id === "")
150
+ continue;
151
+ entries.push({ id: entry.id, joinedAt: Number.isFinite(entry.joinedAt) ? entry.joinedAt : 0 });
152
+ waiting.add(entry.id);
153
+ }
154
+ boards = [];
155
+ for (const board of next.boards) {
156
+ if (!Number.isFinite(board?.at) || !(board?.count > 0))
157
+ continue;
158
+ boards.push({ at: board.at, count: Math.floor(board.count) });
159
+ }
160
+ if (boards.length > window)
161
+ boards = boards.slice(boards.length - window);
162
+ if (Number.isFinite(next.seedServiceSeconds))
163
+ seedServiceSeconds = Math.max(0, next.seedServiceSeconds);
164
+ if (Number.isFinite(next.window))
165
+ window = Math.max(2, Math.floor(next.window));
166
+ },
167
+ dispose() {
168
+ unregister?.();
169
+ },
170
+ };
171
+ }