@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.
- package/dist/index.d.ts +116 -91
- package/dist/index.js +273 -483
- package/dist/index.umd.cjs +1 -1
- package/package.json +1 -1
- package/src/color/ColorMap.test.ts +15 -0
- package/src/color/ColorMap.ts +7 -6
- package/src/event/JobDispatcher.test.ts +484 -0
- package/src/event/JobDispatcher.ts +387 -0
- package/src/event/index.ts +1 -2
- package/src/event/mock.ts +2 -2
- package/src/globe/Globe.ts +3 -26
- package/src/utils/ReturnedConstructedTerrainMeshLike.ts +5 -0
- package/src/utils/polyline/ConstructedPolylineGeometryLike.ts +23 -16
- package/src/event/EventManager.test.ts +0 -635
- package/src/event/EventManager.ts +0 -438
- package/src/event/TransactionManager.test.ts +0 -126
- package/src/event/TransactionManager.ts +0 -78
|
@@ -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
|
-
}
|