@navaramap/core 0.0.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 (67) hide show
  1. package/LICENSE +8 -0
  2. package/README.md +3 -0
  3. package/dist/index.d.ts +605 -0
  4. package/dist/index.js +3042 -0
  5. package/dist/index.umd.cjs +1 -0
  6. package/package.json +38 -0
  7. package/src/abstracts/index.ts +1 -0
  8. package/src/abstracts/vec3.ts +15 -0
  9. package/src/assertions.ts +15 -0
  10. package/src/batch/attribute.test.ts +20 -0
  11. package/src/batch/attribute.ts +14 -0
  12. package/src/batch/index.ts +1 -0
  13. package/src/color/Color.ts +10 -0
  14. package/src/color/ColorMap.ts +86 -0
  15. package/src/color/index.ts +3 -0
  16. package/src/color/types.ts +4 -0
  17. package/src/constants/image.ts +1 -0
  18. package/src/constants/index.ts +1 -0
  19. package/src/error.ts +11 -0
  20. package/src/event/EventManager.test.ts +539 -0
  21. package/src/event/EventManager.ts +387 -0
  22. package/src/event/TransactionManager.test.ts +126 -0
  23. package/src/event/TransactionManager.ts +78 -0
  24. package/src/event/index.ts +2 -0
  25. package/src/event/mock.ts +36 -0
  26. package/src/eventHandler.ts +52 -0
  27. package/src/globe/Globe.ts +156 -0
  28. package/src/globe/index.ts +1 -0
  29. package/src/id.ts +9 -0
  30. package/src/index.ts +15 -0
  31. package/src/namedIndexMap.test.ts +268 -0
  32. package/src/namedIndexMap.ts +50 -0
  33. package/src/observed.ts +30 -0
  34. package/src/plugin.ts +15 -0
  35. package/src/time.ts +3 -0
  36. package/src/types/buffer.ts +4 -0
  37. package/src/types/camera.ts +16 -0
  38. package/src/types/cameraFrustum.ts +5 -0
  39. package/src/types/feature.ts +1 -0
  40. package/src/types/index.ts +13 -0
  41. package/src/types/layer.ts +1 -0
  42. package/src/types/plane.ts +5 -0
  43. package/src/types/ray.ts +5 -0
  44. package/src/types/tile.ts +1 -0
  45. package/src/types/transform.ts +5 -0
  46. package/src/types/unit.ts +7 -0
  47. package/src/types/utils.ts +20 -0
  48. package/src/types/vec.ts +9 -0
  49. package/src/types/window.ts +5 -0
  50. package/src/utils/ExtentRadianF32Like.ts +17 -0
  51. package/src/utils/FloatAttributeLike.ts +17 -0
  52. package/src/utils/ReturnedConstructedTerrainMeshLike.ts +92 -0
  53. package/src/utils/TransferableMartiniLike.ts +21 -0
  54. package/src/utils/TransferableRasterDEMDataLike.ts +36 -0
  55. package/src/utils/TransferableTileLike.ts +53 -0
  56. package/src/utils/UpsamplableTerrainGeometryLike.ts +26 -0
  57. package/src/utils/Vec3Like.ts +15 -0
  58. package/src/utils/index.ts +9 -0
  59. package/src/utils/polygon/ConstructedPolygonGeometryLike.ts +70 -0
  60. package/src/utils/polygon/PolygonMaterialLike.ts +37 -0
  61. package/src/utils/polygon/TransferablePolygonBatchedFeatureLike.ts +73 -0
  62. package/src/utils/polygon/index.ts +3 -0
  63. package/src/utils/polyline/ConstructedPolylineGeometryLike.ts +77 -0
  64. package/src/utils/polyline/PolylineMaterialLike.ts +31 -0
  65. package/src/utils/polyline/TransferablePolylineBatchedFeatureLike.ts +43 -0
  66. package/src/utils/polyline/index.ts +3 -0
  67. package/src/vite-env.d.ts +1 -0
@@ -0,0 +1,387 @@
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
+ [K in keyof Events as K extends `take_${infer P}`
12
+ ? P
13
+ : never]: 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
+ };
36
+ remove: {
37
+ key: RemoveKey;
38
+ max?: number;
39
+ };
40
+ change?: {
41
+ key: ChangeKey;
42
+ max?: number;
43
+ };
44
+ };
45
+
46
+ type TransactionCallbackParams<
47
+ AddKey extends JsEventsKey,
48
+ RemoveKey extends JsEventsKey,
49
+ ChangeKey extends JsEventsKey,
50
+ > =
51
+ | { type: "add"; event: GetJsEventValue<AddKey> }
52
+ | { type: "remove"; event: GetJsEventValue<RemoveKey> }
53
+ | { type: "change"; event: GetJsEventValue<ChangeKey> };
54
+
55
+ const maybeFree = (ev: object | undefined) => {
56
+ if (ev && "free" in ev && ev.free instanceof Function) {
57
+ ev.free();
58
+ }
59
+ };
60
+
61
+ const defaultGenerateEventId = <
62
+ AddKey extends JsEventsKey,
63
+ RemoveKey extends JsEventsKey,
64
+ ChangeKey extends JsEventsKey,
65
+ >(
66
+ ev: TransactionCallbackParams<AddKey, RemoveKey, ChangeKey>,
67
+ ) => {
68
+ const { event } = ev;
69
+
70
+ if (!isEntityEvent(event)) {
71
+ return;
72
+ }
73
+ const id = generate_id_from_entity(event);
74
+ return id;
75
+ };
76
+
77
+ export class EventManager {
78
+ stacks: EventsStacks = {
79
+ camera_transform_updated: [],
80
+ camera_frustum_updated: [],
81
+ data_requested: [],
82
+ data_requester_removed: [],
83
+ mesh_added: [],
84
+ mesh_updated: [],
85
+ mesh_removed: [],
86
+ object_transform_updated: [],
87
+ renderable_feature_added: [],
88
+ renderable_feature_changed: [],
89
+ renderable_feature_removed: [],
90
+ texture_fragment_removed: [],
91
+ texture_fragment_requested: [],
92
+ update_sample_terrain_height: [],
93
+ worker_task_delegated: [],
94
+ worker_task_removed: [],
95
+ hillshade_backfilled: [],
96
+ hillshade_canceled: [],
97
+ };
98
+ // In-flight "add" event ids per transaction key, used to abort adds whose
99
+ // matching remove arrives while they are still pending. Keyed by transaction
100
+ // so one transaction's remove phase cannot wipe another's tracking.
101
+ addedEventIds = new Map<string, Set<unknown>>();
102
+ private transactionManager = new TransactionManager();
103
+
104
+ needsUpdate() {
105
+ return Object.values(this.stacks).some((v) => !!v.length);
106
+ }
107
+
108
+ pushEvents(events: Events | undefined) {
109
+ if (!events) return;
110
+ for (const k of Object.keys(this.stacks) as JsEventsKey[]) {
111
+ // Move the stack out of WASM instead of reading a cloning getter.
112
+ const take = events[`take_${k}` as keyof Events] as unknown as (
113
+ this: Events,
114
+ ) => JsEvents[JsEventsKey];
115
+ const event = take.call(events);
116
+ if (!event) continue;
117
+ if (Array.isArray(event)) {
118
+ (this.stacks[k] as unknown[]).push(...event);
119
+ } else {
120
+ (this.stacks[k] as unknown[]).push(event);
121
+ }
122
+ }
123
+ }
124
+
125
+ forEachStack<Key extends JsEventsKey>(
126
+ key: Key,
127
+ cb: (value: GetJsEventValue<Key>) => void,
128
+ max = 100,
129
+ ) {
130
+ let idx = 0;
131
+ for (const value of this.stacks[key]) {
132
+ if (idx === max) {
133
+ break;
134
+ }
135
+
136
+ cb(value as GetJsEventValue<Key>);
137
+
138
+ maybeFree(value);
139
+
140
+ idx++;
141
+ }
142
+
143
+ // Remove processed events
144
+ this.stacks[key].splice(0, idx);
145
+ }
146
+
147
+ async forEachStackAsync<Key extends JsEventsKey>(
148
+ key: Key,
149
+ cb: (value: GetJsEventValue<Key>) => Promise<void>,
150
+ max = 100,
151
+ shouldProcess?: (value: GetJsEventValue<Key>) => boolean,
152
+ ) {
153
+ const promises = [];
154
+
155
+ let idx = 0;
156
+ const removedIndices = [];
157
+ for (const value of this.stacks[key]) {
158
+ if (idx === max) {
159
+ break;
160
+ }
161
+
162
+ const v = value as GetJsEventValue<Key>;
163
+
164
+ if (shouldProcess && !shouldProcess(v)) {
165
+ idx++;
166
+ continue;
167
+ }
168
+
169
+ promises.push(cb(v));
170
+
171
+ removedIndices.push(idx);
172
+
173
+ idx++;
174
+ }
175
+
176
+ const removedEvs = [];
177
+
178
+ let offset = 0;
179
+ for (const idx of removedIndices) {
180
+ // Remove processed events
181
+ const i = idx - offset;
182
+ removedEvs.push(this.stacks[key][i]);
183
+ this.stacks[key].splice(i, 1);
184
+ offset++;
185
+ }
186
+
187
+ // A single failing handler must not reject the whole batch: the other
188
+ // handlers' results still count and the processed events below must be
189
+ // freed exactly once regardless.
190
+ const settled = await Promise.allSettled(promises);
191
+ for (const s of settled) {
192
+ if (s.status === "rejected") {
193
+ console.error(`Event handler for "${key}" failed:`, s.reason);
194
+ }
195
+ }
196
+
197
+ for (const e of removedEvs) {
198
+ maybeFree(e);
199
+ }
200
+ }
201
+
202
+ // Remove duplicated events before it's proceeded.
203
+ // For example, if MeshAdd event and MeshRemove event are exist, it should be considered as duplicated.
204
+ removeDuplicatedTransactionEvents<
205
+ AddKey extends JsEventsKey,
206
+ RemoveKey extends JsEventsKey,
207
+ ChangeKey extends JsEventsKey,
208
+ >(options: TransactionProcessOption<AddKey, RemoveKey, ChangeKey>) {
209
+ const addedEventsMap = new Map<string, number>();
210
+ const changedEventsMap = options.change ? new Map<string, number>() : null;
211
+
212
+ this.stacks[options.add.key].forEach((added, index) => {
213
+ if (isEntityEvent(added)) {
214
+ const id = generate_id_from_entity(added);
215
+ addedEventsMap.set(id, index);
216
+ }
217
+ });
218
+
219
+ if (changedEventsMap && options.change) {
220
+ this.stacks[options.change.key].forEach((changed, index) => {
221
+ if (isEntityEvent(changed)) {
222
+ const id = generate_id_from_entity(changed);
223
+ changedEventsMap.set(id, index);
224
+ }
225
+ });
226
+ }
227
+
228
+ // Track indices to remove from each stack
229
+ const addedIndicesToRemove = new Set<number>();
230
+ const changedIndicesToRemove = new Set<number>();
231
+ const removedIndicesToSkip = new Set<number>();
232
+
233
+ this.stacks[options.remove.key].forEach((removed, removeIdx) => {
234
+ if (!isEntityEvent(removed)) return;
235
+
236
+ const removedId = generate_id_from_entity(removed);
237
+
238
+ const addedIdx = addedEventsMap.get(removedId);
239
+ let foundMatch = false;
240
+
241
+ if (addedIdx !== undefined) {
242
+ addedIndicesToRemove.add(addedIdx);
243
+ foundMatch = true;
244
+ }
245
+
246
+ if (changedEventsMap && options.change) {
247
+ const changedIdx = changedEventsMap.get(removedId);
248
+ if (changedIdx !== undefined) {
249
+ changedIndicesToRemove.add(changedIdx);
250
+ }
251
+ }
252
+
253
+ if (foundMatch) {
254
+ removedIndicesToSkip.add(removeIdx);
255
+ }
256
+ });
257
+
258
+ if (addedIndicesToRemove.size > 0) {
259
+ this.removeStacksByIndices(options.add.key, addedIndicesToRemove);
260
+ }
261
+
262
+ if (changedIndicesToRemove.size > 0 && options.change) {
263
+ this.removeStacksByIndices(options.change.key, changedIndicesToRemove);
264
+ }
265
+
266
+ if (removedIndicesToSkip.size > 0) {
267
+ this.removeStacksByIndices(options.remove.key, removedIndicesToSkip);
268
+ }
269
+ }
270
+
271
+ removeStacksByIndices(key: JsEventsKey, removedIndices: Set<number>) {
272
+ const sortedIndices = [...removedIndices].sort((a, b) => b - a);
273
+ for (const idx of sortedIndices) {
274
+ maybeFree(this.stacks[key][idx]);
275
+ this.stacks[key].splice(idx, 1);
276
+ }
277
+ }
278
+
279
+ processTransactionEvents<
280
+ AddKey extends JsEventsKey,
281
+ RemoveKey extends JsEventsKey,
282
+ ChangeKey extends JsEventsKey,
283
+ >(
284
+ transactionKey: string,
285
+ options: TransactionProcessOption<AddKey, RemoveKey, ChangeKey>,
286
+ cb: (
287
+ ev: TransactionCallbackParams<AddKey, RemoveKey, ChangeKey>,
288
+ ) => Promise<void>,
289
+ handlers?: {
290
+ shouldProcess?: (
291
+ ev: TransactionCallbackParams<AddKey, RemoveKey, ChangeKey>,
292
+ ) => boolean;
293
+ generateEventId?: (
294
+ ev: TransactionCallbackParams<AddKey, RemoveKey, ChangeKey>,
295
+ ) => string;
296
+ onAbort?: (ev: GetJsEventValue<RemoveKey>) => void | Promise<void>;
297
+ },
298
+ ) {
299
+ const {
300
+ shouldProcess,
301
+ generateEventId = defaultGenerateEventId,
302
+ onAbort,
303
+ } = handlers ?? {};
304
+
305
+ this.removeDuplicatedTransactionEvents(options);
306
+
307
+ const addedEventIds = this.addedEventIds.get(transactionKey) ?? new Set();
308
+ this.addedEventIds.set(transactionKey, addedEventIds);
309
+
310
+ const transaction = this.transactionManager
311
+ .getOrInsert(transactionKey)
312
+ .then(() =>
313
+ this.forEachStackAsync(
314
+ options.add.key,
315
+ (event) => {
316
+ if (onAbort) {
317
+ addedEventIds.add(generateEventId({ type: "add", event }));
318
+ }
319
+ return cb({ type: "add", event });
320
+ },
321
+ options.add.max,
322
+ shouldProcess
323
+ ? (event) => shouldProcess({ type: "add", event })
324
+ : undefined,
325
+ ),
326
+ )
327
+ .then(() => {
328
+ if (onAbort) {
329
+ addedEventIds.clear();
330
+ }
331
+ return this.forEachStackAsync(
332
+ options.remove.key,
333
+ (event) => cb({ type: "remove", event }),
334
+ options.remove.max,
335
+ shouldProcess
336
+ ? (event) => shouldProcess({ type: "remove", event })
337
+ : undefined,
338
+ );
339
+ });
340
+
341
+ // Handle an abort process to an add event.
342
+ if (onAbort && addedEventIds.size) {
343
+ for (const event of this.stacks[options.remove.key]) {
344
+ if (!event) continue;
345
+ const removeEv = event as GetJsEventValue<RemoveKey>;
346
+ const id = generateEventId({ type: "remove", event: removeEv });
347
+ if (id && addedEventIds.has(id)) {
348
+ // Contain both sync throws and async rejections: an abort handler
349
+ // failure must not escape into the caller (or become an unhandled
350
+ // rejection) and must not stop the remaining aborts.
351
+ try {
352
+ Promise.resolve(onAbort(removeEv)).catch((err) => {
353
+ console.error(
354
+ `onAbort handler for "${transactionKey}" failed:`,
355
+ err,
356
+ );
357
+ });
358
+ } catch (err) {
359
+ console.error(
360
+ `onAbort handler for "${transactionKey}" failed:`,
361
+ err,
362
+ );
363
+ }
364
+ addedEventIds.delete(id);
365
+ }
366
+ }
367
+ }
368
+
369
+ if (options.change) {
370
+ const change = options.change;
371
+ transaction
372
+ .then(() =>
373
+ this.forEachStackAsync(
374
+ change.key,
375
+ (event) => cb({ type: "change", event }),
376
+ change.max,
377
+ shouldProcess
378
+ ? (event) => shouldProcess({ type: "change", event })
379
+ : undefined,
380
+ ),
381
+ )
382
+ .end();
383
+ } else {
384
+ transaction.end();
385
+ }
386
+ }
387
+ }
@@ -0,0 +1,126 @@
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
+ });
@@ -0,0 +1,78 @@
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
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./EventManager";
2
+ export * from "./TransactionManager";
@@ -0,0 +1,36 @@
1
+ import type {
2
+ EntityEvent,
3
+ Events,
4
+ RenderableFeatureAddedEvent,
5
+ RenderableFeatureChangedEvent,
6
+ } from "@navaramap/engine";
7
+ import { noop } from "lodash-es";
8
+
9
+ import type { JsEvents, JsEventsKey } from "./EventManager";
10
+
11
+ export const makeRenderableFeatures = <
12
+ Ev extends
13
+ | RenderableFeatureAddedEvent
14
+ | RenderableFeatureChangedEvent
15
+ | EntityEvent,
16
+ >(
17
+ ind: number,
18
+ gen: number,
19
+ ) => {
20
+ return { ind, gen, free: noop } as Ev;
21
+ };
22
+
23
+ // The real `Events` exposes each stack only via a `take_<key>()` method
24
+ // (see EventManager.pushEvents); mimic that shape for any key.
25
+ export const makeEvent = (events: { [K in JsEventsKey]?: JsEvents[K] }) =>
26
+ new Proxy(
27
+ {},
28
+ {
29
+ get: (_target, prop) => {
30
+ if (typeof prop === "string" && prop.startsWith("take_")) {
31
+ return () => events[prop.slice("take_".length) as JsEventsKey];
32
+ }
33
+ return undefined;
34
+ },
35
+ },
36
+ ) as Events;
@@ -0,0 +1,52 @@
1
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
2
+ export type BaseEventMap = Record<string, (...args: any[]) => unknown>;
3
+
4
+ export class EventHandler<
5
+ T extends BaseEventMap = BaseEventMap,
6
+ K extends keyof T = keyof T,
7
+ > {
8
+ events: { [E in K]?: Set<T[E]> } = {};
9
+ onceEvents: { [E in K]?: Set<T[E]> } = {};
10
+
11
+ on<E extends K>(k: E, f: T[E]) {
12
+ if (!this.events[k]) {
13
+ this.events[k] = new Set();
14
+ }
15
+ this.events[k].add(f);
16
+ }
17
+
18
+ off<E extends K>(k: E, f: T[E]) {
19
+ this.events[k]?.delete(f);
20
+ this.onceEvents[k]?.delete(f);
21
+ }
22
+
23
+ clear<E extends K>(k: E) {
24
+ this.events[k]?.clear();
25
+ this.onceEvents[k]?.clear();
26
+ }
27
+
28
+ size<E extends K>(k: E) {
29
+ return this.events[k]?.size || this.onceEvents[k]?.size;
30
+ }
31
+
32
+ /**
33
+ * Register a callback that will be executed only once and then removed
34
+ */
35
+ once<E extends K>(k: E, f: T[E]) {
36
+ if (!this.onceEvents[k]) {
37
+ this.onceEvents[k] = new Set();
38
+ }
39
+ this.onceEvents[k].add(f);
40
+ }
41
+
42
+ emit<E extends K>(k: E, ...args: Parameters<T[E]>) {
43
+ // Execute regular callbacks
44
+ this.events[k]?.forEach((c) => c(...args));
45
+
46
+ // Execute once callbacks and then remove them
47
+ if (this.onceEvents[k]?.size) {
48
+ this.onceEvents[k]?.forEach((c) => c(...args));
49
+ this.onceEvents[k]?.clear();
50
+ }
51
+ }
52
+ }