@navaramap/core 0.2.1 → 0.3.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.
@@ -1,438 +0,0 @@
1
- import { type Events } from "@navaramap/engine";
2
-
3
- import { generate_id_from_entity, isEntityEvent } from "../id";
4
-
5
- import { TransactionManager } from "./TransactionManager";
6
-
7
- // The WASM `Events` class exposes each stack only through a move-out
8
- // `take_<key>()` method (reading a getter would deep-clone the array and every
9
- // element). Stack keys and value types are derived from those methods.
10
- export type JsEvents = {
11
- [
12
- K in keyof Events as K extends `take_${infer P}` ? P : never
13
- ]: Events[K] extends (...args: never[]) => infer R ? R : never;
14
- };
15
- export type JsEventsKey = keyof JsEvents;
16
-
17
- type EventsStacks = {
18
- [K in JsEventsKey]-?: JsEvents[K] extends unknown[]
19
- ? JsEvents[K]
20
- : JsEvents[K][];
21
- };
22
-
23
- type GetJsEventValue<K extends JsEventsKey> = EventsStacks[K] extends unknown[]
24
- ? EventsStacks[K][number]
25
- : EventsStacks[K];
26
-
27
- type TransactionProcessOption<
28
- AddKey extends JsEventsKey,
29
- RemoveKey extends JsEventsKey,
30
- ChangeKey extends JsEventsKey,
31
- > = {
32
- add: {
33
- key: AddKey;
34
- max?: number;
35
- /** Dispatch order for the pending stack. Events a `shouldProcess` gate
36
- * leaves behind are re-sorted against newer ones every pass, so this turns
37
- * the FIFO stack into a priority queue (stable: equal keys stay FIFO). */
38
- order?: (a: GetJsEventValue<AddKey>, b: GetJsEventValue<AddKey>) => number;
39
- };
40
- remove: {
41
- key: RemoveKey;
42
- max?: number;
43
- };
44
- change?: {
45
- key: ChangeKey;
46
- max?: number;
47
- };
48
- };
49
-
50
- type TransactionCallbackParams<
51
- AddKey extends JsEventsKey,
52
- RemoveKey extends JsEventsKey,
53
- ChangeKey extends JsEventsKey,
54
- > =
55
- | { type: "add"; event: GetJsEventValue<AddKey> }
56
- | { type: "remove"; event: GetJsEventValue<RemoveKey> }
57
- | { type: "change"; event: GetJsEventValue<ChangeKey> };
58
-
59
- const maybeFree = (ev: object | undefined) => {
60
- if (ev && "free" in ev && ev.free instanceof Function) {
61
- ev.free();
62
- }
63
- };
64
-
65
- const defaultGenerateEventId = <
66
- AddKey extends JsEventsKey,
67
- RemoveKey extends JsEventsKey,
68
- ChangeKey extends JsEventsKey,
69
- >(
70
- ev: TransactionCallbackParams<AddKey, RemoveKey, ChangeKey>,
71
- ) => {
72
- const { event } = ev;
73
-
74
- if (!isEntityEvent(event)) {
75
- return;
76
- }
77
- const id = generate_id_from_entity(event);
78
- return id;
79
- };
80
-
81
- export class EventManager {
82
- stacks: EventsStacks = {
83
- camera_transform_updated: [],
84
- camera_frustum_updated: [],
85
- camera_flight_ended: [],
86
- data_requested: [],
87
- data_requester_removed: [],
88
- mesh_added: [],
89
- mesh_updated: [],
90
- mesh_geometry_replaced: [],
91
- mesh_removed: [],
92
- object_transform_updated: [],
93
- renderable_feature_added: [],
94
- renderable_feature_changed: [],
95
- renderable_feature_removed: [],
96
- texture_fragment_removed: [],
97
- texture_fragment_requested: [],
98
- update_sample_terrain_height: [],
99
- update_terrain_height_range: [],
100
- worker_task_delegated: [],
101
- worker_task_removed: [],
102
- hillshade_backfilled: [],
103
- hillshade_canceled: [],
104
- };
105
- // In-flight "add" event ids per transaction key, used to abort adds whose
106
- // matching remove arrives while they are still pending. Keyed by transaction
107
- // so one transaction's remove phase cannot wipe another's tracking.
108
- addedEventIds = new Map<string, Set<unknown>>();
109
- // Length of each stack's already-sorted prefix (see `sortPending`).
110
- private sortedLengths: Partial<Record<JsEventsKey, number>> = {};
111
- private transactionManager = new TransactionManager();
112
-
113
- needsUpdate() {
114
- return Object.values(this.stacks).some((v) => !!v.length);
115
- }
116
-
117
- pushEvents(events: Events | undefined) {
118
- if (!events) return;
119
- for (const k of Object.keys(this.stacks) as JsEventsKey[]) {
120
- // Move the stack out of WASM instead of reading a cloning getter.
121
- const take = events[`take_${k}` as keyof Events] as unknown as (
122
- this: Events,
123
- ) => JsEvents[JsEventsKey];
124
- const event = take.call(events);
125
- if (!event) continue;
126
- if (Array.isArray(event)) {
127
- (this.stacks[k] as unknown[]).push(...event);
128
- } else {
129
- (this.stacks[k] as unknown[]).push(event);
130
- }
131
- }
132
- }
133
-
134
- forEachStack<Key extends JsEventsKey>(
135
- key: Key,
136
- cb: (value: GetJsEventValue<Key>) => void,
137
- max = 100,
138
- ) {
139
- let idx = 0;
140
- for (const value of this.stacks[key]) {
141
- if (idx === max) {
142
- break;
143
- }
144
-
145
- cb(value as GetJsEventValue<Key>);
146
-
147
- maybeFree(value);
148
-
149
- idx++;
150
- }
151
-
152
- // Remove processed events
153
- this.stacks[key].splice(0, idx);
154
- const sorted = this.sortedLengths[key];
155
- if (sorted !== undefined) {
156
- this.sortedLengths[key] = Math.max(0, sorted - idx);
157
- }
158
- }
159
-
160
- /** Order the stack for dispatch. Pushes only append, so only the events
161
- * added since the last pass are sorted and merged into the already sorted
162
- * prefix (the prefix wins ties, keeping equal keys FIFO). */
163
- private sortPending<Key extends JsEventsKey>(
164
- key: Key,
165
- order: (a: GetJsEventValue<Key>, b: GetJsEventValue<Key>) => number,
166
- ) {
167
- const stack = this.stacks[key] as GetJsEventValue<Key>[];
168
- const sortedLength = Math.min(this.sortedLengths[key] ?? 0, stack.length);
169
- if (sortedLength < stack.length) {
170
- const added = stack.splice(sortedLength).sort(order);
171
- const merged: GetJsEventValue<Key>[] = [];
172
- let i = 0;
173
- let j = 0;
174
- while (i < stack.length && j < added.length) {
175
- merged.push(order(stack[i], added[j]) <= 0 ? stack[i++] : added[j++]);
176
- }
177
- for (; i < stack.length; i++) merged.push(stack[i]);
178
- for (; j < added.length; j++) merged.push(added[j]);
179
- this.stacks[key] = merged as EventsStacks[Key];
180
- }
181
- this.sortedLengths[key] = this.stacks[key].length;
182
- }
183
-
184
- private removeAt(key: JsEventsKey, index: number) {
185
- this.stacks[key].splice(index, 1);
186
- const sorted = this.sortedLengths[key];
187
- if (sorted !== undefined && index < sorted) {
188
- this.sortedLengths[key] = sorted - 1;
189
- }
190
- }
191
-
192
- async forEachStackAsync<Key extends JsEventsKey>(
193
- key: Key,
194
- cb: (value: GetJsEventValue<Key>) => Promise<void>,
195
- max = 100,
196
- shouldProcess?: (value: GetJsEventValue<Key>) => boolean,
197
- order?: (a: GetJsEventValue<Key>, b: GetJsEventValue<Key>) => number,
198
- ) {
199
- const promises = [];
200
-
201
- if (order) {
202
- this.sortPending(key, order);
203
- }
204
-
205
- let idx = 0;
206
- const removedIndices = [];
207
- for (const value of this.stacks[key]) {
208
- if (idx === max) {
209
- break;
210
- }
211
-
212
- const v = value as GetJsEventValue<Key>;
213
-
214
- if (shouldProcess && !shouldProcess(v)) {
215
- idx++;
216
- continue;
217
- }
218
-
219
- promises.push(cb(v));
220
-
221
- removedIndices.push(idx);
222
-
223
- idx++;
224
- }
225
-
226
- const removedEvs = [];
227
-
228
- let offset = 0;
229
- for (const idx of removedIndices) {
230
- // Remove processed events
231
- const i = idx - offset;
232
- removedEvs.push(this.stacks[key][i]);
233
- this.removeAt(key, i);
234
- offset++;
235
- }
236
-
237
- // A single failing handler must not reject the whole batch: the other
238
- // handlers' results still count and the processed events below must be
239
- // freed exactly once regardless.
240
- const settled = await Promise.allSettled(promises);
241
- for (const s of settled) {
242
- if (s.status === "rejected") {
243
- console.error(`Event handler for "${key}" failed:`, s.reason);
244
- }
245
- }
246
-
247
- for (const e of removedEvs) {
248
- maybeFree(e);
249
- }
250
- }
251
-
252
- // Remove duplicated events before it's proceeded.
253
- // For example, if MeshAdd event and MeshRemove event are exist, it should be considered as duplicated.
254
- removeDuplicatedTransactionEvents<
255
- AddKey extends JsEventsKey,
256
- RemoveKey extends JsEventsKey,
257
- ChangeKey extends JsEventsKey,
258
- >(options: TransactionProcessOption<AddKey, RemoveKey, ChangeKey>) {
259
- const addedEventsMap = new Map<string, number>();
260
- const changedEventsMap = options.change ? new Map<string, number>() : null;
261
-
262
- this.stacks[options.add.key].forEach((added, index) => {
263
- if (isEntityEvent(added)) {
264
- const id = generate_id_from_entity(added);
265
- addedEventsMap.set(id, index);
266
- }
267
- });
268
-
269
- if (changedEventsMap && options.change) {
270
- this.stacks[options.change.key].forEach((changed, index) => {
271
- if (isEntityEvent(changed)) {
272
- const id = generate_id_from_entity(changed);
273
- changedEventsMap.set(id, index);
274
- }
275
- });
276
- }
277
-
278
- // Track indices to remove from each stack
279
- const addedIndicesToRemove = new Set<number>();
280
- const changedIndicesToRemove = new Set<number>();
281
- const removedIndicesToSkip = new Set<number>();
282
-
283
- this.stacks[options.remove.key].forEach((removed, removeIdx) => {
284
- if (!isEntityEvent(removed)) return;
285
-
286
- const removedId = generate_id_from_entity(removed);
287
-
288
- const addedIdx = addedEventsMap.get(removedId);
289
- let foundMatch = false;
290
-
291
- if (addedIdx !== undefined) {
292
- addedIndicesToRemove.add(addedIdx);
293
- foundMatch = true;
294
- }
295
-
296
- if (changedEventsMap && options.change) {
297
- const changedIdx = changedEventsMap.get(removedId);
298
- if (changedIdx !== undefined) {
299
- changedIndicesToRemove.add(changedIdx);
300
- }
301
- }
302
-
303
- if (foundMatch) {
304
- removedIndicesToSkip.add(removeIdx);
305
- }
306
- });
307
-
308
- if (addedIndicesToRemove.size > 0) {
309
- this.removeStacksByIndices(options.add.key, addedIndicesToRemove);
310
- }
311
-
312
- if (changedIndicesToRemove.size > 0 && options.change) {
313
- this.removeStacksByIndices(options.change.key, changedIndicesToRemove);
314
- }
315
-
316
- if (removedIndicesToSkip.size > 0) {
317
- this.removeStacksByIndices(options.remove.key, removedIndicesToSkip);
318
- }
319
- }
320
-
321
- removeStacksByIndices(key: JsEventsKey, removedIndices: Set<number>) {
322
- const sortedIndices = [...removedIndices].sort((a, b) => b - a);
323
- for (const idx of sortedIndices) {
324
- maybeFree(this.stacks[key][idx]);
325
- this.removeAt(key, idx);
326
- }
327
- }
328
-
329
- processTransactionEvents<
330
- AddKey extends JsEventsKey,
331
- RemoveKey extends JsEventsKey,
332
- ChangeKey extends JsEventsKey,
333
- >(
334
- transactionKey: string,
335
- options: TransactionProcessOption<AddKey, RemoveKey, ChangeKey>,
336
- cb: (
337
- ev: TransactionCallbackParams<AddKey, RemoveKey, ChangeKey>,
338
- ) => Promise<void>,
339
- handlers?: {
340
- shouldProcess?: (
341
- ev: TransactionCallbackParams<AddKey, RemoveKey, ChangeKey>,
342
- ) => boolean;
343
- generateEventId?: (
344
- ev: TransactionCallbackParams<AddKey, RemoveKey, ChangeKey>,
345
- ) => string;
346
- onAbort?: (ev: GetJsEventValue<RemoveKey>) => void | Promise<void>;
347
- },
348
- ) {
349
- const {
350
- shouldProcess,
351
- generateEventId = defaultGenerateEventId,
352
- onAbort,
353
- } = handlers ?? {};
354
-
355
- this.removeDuplicatedTransactionEvents(options);
356
-
357
- const addedEventIds = this.addedEventIds.get(transactionKey) ?? new Set();
358
- this.addedEventIds.set(transactionKey, addedEventIds);
359
-
360
- const transaction = this.transactionManager
361
- .getOrInsert(transactionKey)
362
- .then(() =>
363
- this.forEachStackAsync(
364
- options.add.key,
365
- (event) => {
366
- if (onAbort) {
367
- addedEventIds.add(generateEventId({ type: "add", event }));
368
- }
369
- return cb({ type: "add", event });
370
- },
371
- options.add.max,
372
- shouldProcess
373
- ? (event) => shouldProcess({ type: "add", event })
374
- : undefined,
375
- options.add.order,
376
- ),
377
- )
378
- .then(() => {
379
- if (onAbort) {
380
- addedEventIds.clear();
381
- }
382
- return this.forEachStackAsync(
383
- options.remove.key,
384
- (event) => cb({ type: "remove", event }),
385
- options.remove.max,
386
- shouldProcess
387
- ? (event) => shouldProcess({ type: "remove", event })
388
- : undefined,
389
- );
390
- });
391
-
392
- // Handle an abort process to an add event.
393
- if (onAbort && addedEventIds.size) {
394
- for (const event of this.stacks[options.remove.key]) {
395
- if (!event) continue;
396
- const removeEv = event as GetJsEventValue<RemoveKey>;
397
- const id = generateEventId({ type: "remove", event: removeEv });
398
- if (id && addedEventIds.has(id)) {
399
- // Contain both sync throws and async rejections: an abort handler
400
- // failure must not escape into the caller (or become an unhandled
401
- // rejection) and must not stop the remaining aborts.
402
- try {
403
- Promise.resolve(onAbort(removeEv)).catch((err) => {
404
- console.error(
405
- `onAbort handler for "${transactionKey}" failed:`,
406
- err,
407
- );
408
- });
409
- } catch (err) {
410
- console.error(
411
- `onAbort handler for "${transactionKey}" failed:`,
412
- err,
413
- );
414
- }
415
- addedEventIds.delete(id);
416
- }
417
- }
418
- }
419
-
420
- if (options.change) {
421
- const change = options.change;
422
- transaction
423
- .then(() =>
424
- this.forEachStackAsync(
425
- change.key,
426
- (event) => cb({ type: "change", event }),
427
- change.max,
428
- shouldProcess
429
- ? (event) => shouldProcess({ type: "change", event })
430
- : undefined,
431
- ),
432
- )
433
- .end();
434
- } else {
435
- transaction.end();
436
- }
437
- }
438
- }
@@ -1,126 +0,0 @@
1
- import { expect, it, vi } from "vitest";
2
-
3
- import { TransactionManager } from "./TransactionManager";
4
-
5
- it("should manage transaction", async () => {
6
- const transactionManager = new TransactionManager();
7
- const transaction = transactionManager.getOrInsert("test");
8
-
9
- const mockFn = vi.fn();
10
- const mockFnDuplicated = vi.fn();
11
- const mockFnNext = vi.fn();
12
-
13
- // First frame
14
- transaction
15
- .then(async () => mockFn())
16
- .then(async () => mockFnNext())
17
- .end();
18
-
19
- // Second frame
20
- transaction.then(async () => mockFnDuplicated());
21
- transaction.then(async () => mockFnDuplicated());
22
-
23
- await vi.waitUntil(() => !transaction.continuable);
24
- expect(mockFn).toBeCalledTimes(1);
25
- expect(mockFnNext).toBeCalledTimes(0);
26
-
27
- // Third frame
28
- transaction
29
- .then(async () => mockFn())
30
- .then(async () => mockFnNext())
31
- .end();
32
- transaction
33
- .then(async () => mockFnDuplicated())
34
- .then(async () => mockFnDuplicated());
35
-
36
- await vi.waitUntil(() => !transaction.next?.continuable);
37
- expect(mockFn).toBeCalledTimes(1);
38
- expect(mockFnNext).toBeCalledTimes(1);
39
-
40
- expect(transaction.next?.next?.continuable).toBeTruthy();
41
-
42
- // Forth frame
43
- transaction
44
- .then(async () => mockFn())
45
- .then(async () => mockFnNext())
46
- .end();
47
-
48
- expect(mockFn).toBeCalledTimes(1);
49
- expect(mockFnNext).toBeCalledTimes(1);
50
- expect(mockFnDuplicated).not.toBeCalled();
51
-
52
- expect(transaction.continuable).toBeTruthy();
53
- expect(transaction.next?.continuable).toBeFalsy();
54
- expect(transaction.next?.next?.continuable).toBeFalsy();
55
- });
56
-
57
- it("should continue the transaction chain after a rejected callback", async () => {
58
- const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
59
- const transactionManager = new TransactionManager();
60
- const transaction = transactionManager.getOrInsert("test");
61
-
62
- const mockFnNext = vi.fn();
63
- const mockFnRecovered = vi.fn();
64
-
65
- // First frame: the first-phase callback fails. Before the fix this left
66
- // `currentPending` set forever, wedging the whole transaction id.
67
- transaction
68
- .then(async () => {
69
- throw new Error("boom");
70
- })
71
- .then(async () => mockFnNext())
72
- .end();
73
-
74
- // Later frames replay the same chain; both phases must eventually run.
75
- await vi.waitUntil(() => {
76
- transaction
77
- .then(async () => mockFnRecovered())
78
- .then(async () => mockFnNext())
79
- .end();
80
- return (
81
- mockFnRecovered.mock.calls.length > 0 && mockFnNext.mock.calls.length > 0
82
- );
83
- });
84
-
85
- expect(consoleError).toHaveBeenCalledWith(
86
- expect.stringContaining('Transaction "test" callback failed:'),
87
- expect.any(Error),
88
- );
89
- consoleError.mockRestore();
90
- });
91
-
92
- it("should continue the transaction chain after a synchronously throwing callback", async () => {
93
- const consoleError = vi.spyOn(console, "error").mockImplementation(() => {});
94
- const transactionManager = new TransactionManager();
95
- const transaction = transactionManager.getOrInsert("test");
96
-
97
- const mockFnNext = vi.fn();
98
- const mockFnRecovered = vi.fn();
99
-
100
- // First frame: the callback throws before ever returning a promise. This
101
- // must be contained just like a rejection, or the continuable flags stay
102
- // down forever.
103
- transaction
104
- .then(() => {
105
- throw new Error("boom");
106
- })
107
- .then(async () => mockFnNext())
108
- .end();
109
-
110
- // Later frames replay the same chain; both phases must eventually run.
111
- await vi.waitUntil(() => {
112
- transaction
113
- .then(async () => mockFnRecovered())
114
- .then(async () => mockFnNext())
115
- .end();
116
- return (
117
- mockFnRecovered.mock.calls.length > 0 && mockFnNext.mock.calls.length > 0
118
- );
119
- });
120
-
121
- expect(consoleError).toHaveBeenCalledWith(
122
- expect.stringContaining('Transaction "test" callback failed:'),
123
- expect.any(Error),
124
- );
125
- consoleError.mockRestore();
126
- });
@@ -1,78 +0,0 @@
1
- export class TransactionManager {
2
- private record: Record<string, Transaction | undefined> = {};
3
- getOrInsert(id: string) {
4
- if (this.record[id]) {
5
- return this.record[id];
6
- }
7
- this.record[id] = new Transaction(id);
8
- return this.record[id];
9
- }
10
- }
11
-
12
- export class Transaction {
13
- id: string;
14
- continuable: boolean;
15
- currentPending?: Promise<void>;
16
- next?: Transaction;
17
- parent?: Transaction;
18
-
19
- constructor(id: string, continuable = true) {
20
- this.id = id;
21
- this.continuable = continuable;
22
- }
23
-
24
- then(cb: () => Promise<void>) {
25
- if (!this.next) {
26
- this.next = new Transaction(this.id, false);
27
- this.next.parent = this;
28
- }
29
- if (!this.currentPending && this.continuable) {
30
- this.continuable = false;
31
- this.next.continuable = false;
32
- // A failed callback must be contained here: leaving the continuable
33
- // flags down (and `currentPending` possibly set) would wedge this
34
- // transaction id forever, silently stopping every event that flows
35
- // through it.
36
- const settle = () => {
37
- this.currentPending = undefined;
38
- if (this.next) {
39
- this.next.continuable = true;
40
- }
41
- };
42
- const fail = (err: unknown) => {
43
- console.error(`Transaction "${this.id}" callback failed:`, err);
44
- settle();
45
- };
46
- try {
47
- this.currentPending = cb().then(settle, fail);
48
- } catch (err) {
49
- // `cb` threw synchronously, before returning a promise.
50
- fail(err);
51
- }
52
- }
53
- return this.next;
54
- }
55
-
56
- end() {
57
- if (!this.continuable) {
58
- return;
59
- }
60
-
61
- let parent: Transaction | undefined = this.parent;
62
- if (!parent) {
63
- // Root parent should be true.
64
- this.continuable = true;
65
- return;
66
- }
67
-
68
- this.continuable = false;
69
-
70
- while (true) {
71
- if (!parent.parent) {
72
- break;
73
- }
74
- parent = parent.parent;
75
- }
76
- parent.continuable = true;
77
- }
78
- }