@ddd-ts/event-sourcing-firestore 0.0.41 → 0.0.43
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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/projection/firestore.projector.d.ts +17 -4
- package/dist/projection/firestore.projector.d.ts.map +1 -1
- package/dist/projection/firestore.projector.js +145 -29
- package/dist/projection/firestore.projector.mjs +145 -29
- package/package.json +10 -10
- package/dist/projection/event-coordinator.d.ts +0 -16
- package/dist/projection/event-coordinator.d.ts.map +0 -1
- package/dist/projection/event-coordinator.js +0 -47
- package/dist/projection/event-coordinator.mjs +0 -47
- package/dist/utils/promise-with-resolvers.d.ts +0 -7
- package/dist/utils/promise-with-resolvers.d.ts.map +0 -1
- package/dist/utils/promise-with-resolvers.js +0 -17
- package/dist/utils/promise-with-resolvers.mjs +0 -16
package/dist/index.d.ts
CHANGED
|
@@ -7,5 +7,5 @@ export { FirestoreEventLakeAggregateStore, MakeFirestoreEventLakeAggregateStore,
|
|
|
7
7
|
export { FirestoreProjectedStreamStorageLayer, FirestoreLakeSourceFilter, FirestoreStreamSourceFilter, } from "./firestore.projected-stream.storage-layer";
|
|
8
8
|
export { FirestoreProjectedStreamReader } from "./firestore.projected-stream.reader";
|
|
9
9
|
export { FirestoreSnapshotter } from "./firestore.snapshotter";
|
|
10
|
-
export { FirestoreProjector, FirestoreQueueStore, Task, AlreadyEnqueuedError, ClaimerId, } from "./projection/firestore.projector";
|
|
10
|
+
export { FirestoreProjector, FirestoreQueueStore, Task, AlreadyEnqueuedError, ClaimerId, type ProjectorLogger, } from "./projection/firestore.projector";
|
|
11
11
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,MAAM,sCAAsC,CAAC;AACtF,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,gCAAgC,EAAE,MAAM,wCAAwC,CAAC;AAC1F,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAE3E,OAAO,EACL,kCAAkC,EAClC,sCAAsC,GACvC,MAAM,0CAA0C,CAAC;AAElD,OAAO,EACL,gCAAgC,EAChC,oCAAoC,GACrC,MAAM,wCAAwC,CAAC;AAEhD,OAAO,EACL,oCAAoC,EACpC,yBAAyB,EACzB,2BAA2B,GAC5B,MAAM,4CAA4C,CAAC;AAEpD,OAAO,EAAE,8BAA8B,EAAE,MAAM,qCAAqC,CAAC;AAErF,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,IAAI,EACJ,oBAAoB,EACpB,SAAS,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,MAAM,sCAAsC,CAAC;AACtF,OAAO,EAAE,uBAAuB,EAAE,MAAM,8BAA8B,CAAC;AACvE,OAAO,EAAE,gCAAgC,EAAE,MAAM,wCAAwC,CAAC;AAC1F,OAAO,EAAE,yBAAyB,EAAE,MAAM,gCAAgC,CAAC;AAE3E,OAAO,EACL,kCAAkC,EAClC,sCAAsC,GACvC,MAAM,0CAA0C,CAAC;AAElD,OAAO,EACL,gCAAgC,EAChC,oCAAoC,GACrC,MAAM,wCAAwC,CAAC;AAEhD,OAAO,EACL,oCAAoC,EACpC,yBAAyB,EACzB,2BAA2B,GAC5B,MAAM,4CAA4C,CAAC;AAEpD,OAAO,EAAE,8BAA8B,EAAE,MAAM,qCAAqC,CAAC;AAErF,OAAO,EAAE,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAE/D,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,IAAI,EACJ,oBAAoB,EACpB,SAAS,EACT,KAAK,eAAe,GACrB,MAAM,kCAAkC,CAAC"}
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { type IEsEvent, type ISavedChange, EventId, ProjectedStreamReader, Cursor, CheckpointId, ESProjection, type IFact, type Serialized, Lock } from "@ddd-ts/core";
|
|
1
|
+
import { CheckpointId, Cursor, ESProjection, EventId, type IEsEvent, type IFact, type ISavedChange, Lock, ProjectedStreamReader, type Serialized } from "@ddd-ts/core";
|
|
3
2
|
import { Mapping, MicrosecondTimestamp } from "@ddd-ts/shape";
|
|
4
3
|
import { DefaultConverter, FirestoreTransaction } from "@ddd-ts/store-firestore";
|
|
4
|
+
import { Firestore, WriteBatch } from "firebase-admin/firestore";
|
|
5
|
+
export interface ProjectorLogger {
|
|
6
|
+
debug(message: string, context?: Record<string, unknown>): void;
|
|
7
|
+
info(message: string, context?: Record<string, unknown>): void;
|
|
8
|
+
warn(message: string, context?: Record<string, unknown>): void;
|
|
9
|
+
error(message: string, context?: Record<string, unknown>): void;
|
|
10
|
+
}
|
|
5
11
|
interface FirestoreProjectorConfig {
|
|
6
12
|
retry: {
|
|
7
13
|
attempts: number;
|
|
@@ -12,7 +18,10 @@ interface FirestoreProjectorConfig {
|
|
|
12
18
|
enqueue: {
|
|
13
19
|
batchSize: number;
|
|
14
20
|
};
|
|
21
|
+
logger?: ProjectorLogger;
|
|
22
|
+
/** @deprecated Use `logger.error` instead */
|
|
15
23
|
onProcessError: (error: Error) => void;
|
|
24
|
+
/** @deprecated Use `logger.error` instead */
|
|
16
25
|
onEnqueueError: (error: Error) => void;
|
|
17
26
|
}
|
|
18
27
|
export declare class FirestoreProjector {
|
|
@@ -22,10 +31,14 @@ export declare class FirestoreProjector {
|
|
|
22
31
|
config: FirestoreProjectorConfig;
|
|
23
32
|
_unclaim: boolean;
|
|
24
33
|
constructor(projection: ESProjection<IEsEvent>, reader: ProjectedStreamReader<IEsEvent>, queue: FirestoreQueueStore, config?: FirestoreProjectorConfig);
|
|
34
|
+
private get logger();
|
|
25
35
|
breathe(): AsyncGenerator<readonly [number, () => void], void, unknown>;
|
|
26
|
-
private
|
|
27
|
-
private
|
|
36
|
+
private processingCheckpoints;
|
|
37
|
+
private coalescedCounts;
|
|
38
|
+
private pendingCursors;
|
|
39
|
+
private prunePendingCursors;
|
|
28
40
|
handle(savedChange: ISavedChange<IEsEvent>): Promise<void>;
|
|
41
|
+
private handleOne;
|
|
29
42
|
private getCursor;
|
|
30
43
|
private attempt;
|
|
31
44
|
private getQueueHead;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"firestore.projector.d.ts","sourceRoot":"","sources":["../../src/projection/firestore.projector.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"firestore.projector.d.ts","sourceRoot":"","sources":["../../src/projection/firestore.projector.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
const require_runtime = require('../_virtual/_rolldown/runtime.js');
|
|
2
|
-
const require_event_coordinator = require('./event-coordinator.js');
|
|
3
2
|
let _ddd_ts_core = require("@ddd-ts/core");
|
|
4
3
|
let _ddd_ts_store_firestore = require("@ddd-ts/store-firestore");
|
|
5
4
|
let firebase_admin_firestore = require("firebase-admin/firestore");
|
|
@@ -18,6 +17,12 @@ const TaskState = {
|
|
|
18
17
|
};
|
|
19
18
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
19
|
const RETENTION = _ddd_ts_shape.MicrosecondTimestamp.MONTH;
|
|
20
|
+
const defaultLogger = {
|
|
21
|
+
debug: (msg, ctx) => console.debug(msg, ctx ?? ""),
|
|
22
|
+
info: (msg, ctx) => console.info(msg, ctx ?? ""),
|
|
23
|
+
warn: (msg, ctx) => console.warn(msg, ctx ?? ""),
|
|
24
|
+
error: (msg, ctx) => console.error(msg, ctx ?? "")
|
|
25
|
+
};
|
|
21
26
|
var FirestoreProjector = class {
|
|
22
27
|
_unclaim = true;
|
|
23
28
|
constructor(projection, reader, queue, config = {
|
|
@@ -28,6 +33,7 @@ var FirestoreProjector = class {
|
|
|
28
33
|
backoff: 1.5
|
|
29
34
|
},
|
|
30
35
|
enqueue: { batchSize: 100 },
|
|
36
|
+
logger: defaultLogger,
|
|
31
37
|
onProcessError: (error) => {
|
|
32
38
|
console.error("Error processing event:", error);
|
|
33
39
|
},
|
|
@@ -40,6 +46,9 @@ var FirestoreProjector = class {
|
|
|
40
46
|
this.queue = queue;
|
|
41
47
|
this.config = config;
|
|
42
48
|
}
|
|
49
|
+
get logger() {
|
|
50
|
+
return this.config.logger ?? defaultLogger;
|
|
51
|
+
}
|
|
43
52
|
async *breathe() {
|
|
44
53
|
const { attempts, minDelay, maxDelay, backoff } = this.config.retry;
|
|
45
54
|
for (let i = 0; i < attempts; i++) {
|
|
@@ -52,32 +61,106 @@ var FirestoreProjector = class {
|
|
|
52
61
|
await wait((backoff * i + 1) * minDelay + jitter);
|
|
53
62
|
}
|
|
54
63
|
}
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
processingCheckpoints = /* @__PURE__ */ new Map();
|
|
65
|
+
coalescedCounts = /* @__PURE__ */ new Map();
|
|
66
|
+
pendingCursors = /* @__PURE__ */ new Map();
|
|
67
|
+
prunePendingCursors(checkpointId, eventIds) {
|
|
57
68
|
const key = checkpointId.serialize();
|
|
58
|
-
|
|
59
|
-
if (!
|
|
60
|
-
|
|
61
|
-
coordinator.onEmpty(() => this.eventCoordinators.delete(key));
|
|
62
|
-
this.eventCoordinators.set(key, coordinator);
|
|
63
|
-
}
|
|
64
|
-
return coordinator;
|
|
69
|
+
const pending = this.pendingCursors.get(key);
|
|
70
|
+
if (!pending) return;
|
|
71
|
+
for (const eventId of eventIds) pending.delete(eventId.serialize());
|
|
65
72
|
}
|
|
66
73
|
async handle(savedChange) {
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
74
|
+
const key = this.projection.getCheckpointId(savedChange).serialize();
|
|
75
|
+
const cursor = await this.getCursor(savedChange);
|
|
76
|
+
if (!cursor) throw new Error(`Cursor not found for event ${savedChange.id.serialize()}`);
|
|
77
|
+
if (this.processingCheckpoints.has(key)) {
|
|
78
|
+
if (!this.pendingCursors.has(key)) this.pendingCursors.set(key, /* @__PURE__ */ new Map());
|
|
79
|
+
const eventId = savedChange.id.serialize();
|
|
80
|
+
this.pendingCursors.get(key).set(eventId, {
|
|
81
|
+
savedChange,
|
|
82
|
+
cursor
|
|
83
|
+
});
|
|
84
|
+
const existing = this.processingCheckpoints.get(key);
|
|
85
|
+
if (!existing || cursor.isAfter(existing.cursor)) this.processingCheckpoints.set(key, {
|
|
86
|
+
savedChange,
|
|
87
|
+
cursor
|
|
88
|
+
});
|
|
89
|
+
const coalesced = (this.coalescedCounts.get(key) ?? 0) + 1;
|
|
90
|
+
this.coalescedCounts.set(key, coalesced);
|
|
91
|
+
this.logger.debug(`debounced: Checkpoint<${key}> is processing, Event<${eventId}> coalesced (${coalesced} pending)`, {
|
|
92
|
+
checkpointId: key,
|
|
93
|
+
eventId,
|
|
94
|
+
coalesced
|
|
95
|
+
});
|
|
73
96
|
return;
|
|
74
97
|
}
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
98
|
+
this.processingCheckpoints.set(key, null);
|
|
99
|
+
try {
|
|
100
|
+
await this.handleOne(savedChange, cursor);
|
|
101
|
+
const nextPending = () => {
|
|
102
|
+
const debounced = this.processingCheckpoints.get(key);
|
|
103
|
+
if (debounced) {
|
|
104
|
+
this.processingCheckpoints.set(key, null);
|
|
105
|
+
return {
|
|
106
|
+
type: "debounced",
|
|
107
|
+
savedChange: debounced.savedChange,
|
|
108
|
+
cursor: debounced.cursor,
|
|
109
|
+
eventId: debounced.savedChange.id.serialize()
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const remaining = this.pendingCursors.get(key);
|
|
113
|
+
if (remaining && remaining.size > 0) {
|
|
114
|
+
const [eventId, straggler] = remaining.entries().next().value;
|
|
115
|
+
remaining.delete(eventId);
|
|
116
|
+
return {
|
|
117
|
+
type: "straggler",
|
|
118
|
+
savedChange: straggler.savedChange,
|
|
119
|
+
cursor: straggler.cursor,
|
|
120
|
+
eventId
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
};
|
|
125
|
+
let iterations = 0;
|
|
126
|
+
for (let next = nextPending(); next; next = nextPending()) {
|
|
127
|
+
iterations++;
|
|
128
|
+
if (iterations > 10) this.logger.warn(`high-iteration: Checkpoint<${key}> re-run loop at iteration ${iterations}`, {
|
|
129
|
+
checkpointId: key,
|
|
130
|
+
iterations
|
|
131
|
+
});
|
|
132
|
+
if (next.type === "debounced") this.logger.debug(`re-run: Checkpoint<${key}> iteration ${iterations}, targeting Event<${next.eventId}>`, {
|
|
133
|
+
checkpointId: key,
|
|
134
|
+
iterations,
|
|
135
|
+
eventId: next.eventId
|
|
136
|
+
});
|
|
137
|
+
else {
|
|
138
|
+
const remaining = this.pendingCursors.get(key);
|
|
139
|
+
this.logger.info(`straggler: Checkpoint<${key}> handling Event<${next.eventId}> not covered by any slice (${remaining?.size ?? 0} remaining)`, {
|
|
140
|
+
checkpointId: key,
|
|
141
|
+
eventId: next.eventId,
|
|
142
|
+
remainingStragglers: remaining?.size ?? 0
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
await this.handleOne(next.savedChange, next.cursor);
|
|
146
|
+
}
|
|
147
|
+
} finally {
|
|
148
|
+
this.processingCheckpoints.delete(key);
|
|
149
|
+
this.pendingCursors.delete(key);
|
|
150
|
+
this.coalescedCounts.delete(key);
|
|
80
151
|
}
|
|
152
|
+
}
|
|
153
|
+
async handleOne(savedChange, target) {
|
|
154
|
+
const checkpointId = this.projection.getCheckpointId(savedChange);
|
|
155
|
+
const eventId = savedChange.id.serialize();
|
|
156
|
+
const checkpointKey = checkpointId.serialize();
|
|
157
|
+
const startedAt = Date.now();
|
|
158
|
+
this.logger.info(`processing: Checkpoint<${checkpointKey}> targeting Event<${eventId}>`, {
|
|
159
|
+
checkpointId: checkpointKey,
|
|
160
|
+
eventId,
|
|
161
|
+
target: target.toString(),
|
|
162
|
+
targetRef: target.ref
|
|
163
|
+
});
|
|
81
164
|
const errors = [];
|
|
82
165
|
for await (const [attempt, reset] of this.breathe()) {
|
|
83
166
|
const source = this.projection.getSource(savedChange);
|
|
@@ -88,13 +171,24 @@ var FirestoreProjector = class {
|
|
|
88
171
|
}
|
|
89
172
|
if (status === Status.SUCCESS) {
|
|
90
173
|
await this.queue.cleanup(checkpointId);
|
|
91
|
-
|
|
174
|
+
const durationMs = Date.now() - startedAt;
|
|
175
|
+
this.logger.info(`processed: Checkpoint<${checkpointKey}> caught up to Event<${eventId}> in ${durationMs}ms`, {
|
|
176
|
+
checkpointId: checkpointKey,
|
|
177
|
+
eventId,
|
|
178
|
+
durationMs
|
|
179
|
+
});
|
|
92
180
|
return;
|
|
93
181
|
}
|
|
94
182
|
errors.push(message);
|
|
95
183
|
}
|
|
96
|
-
|
|
97
|
-
|
|
184
|
+
const { attempts } = this.config.retry;
|
|
185
|
+
this.logger.error(`failed: Checkpoint<${checkpointKey}> exhausted ${attempts} retries for Event<${eventId}>`, {
|
|
186
|
+
checkpointId: checkpointKey,
|
|
187
|
+
eventId,
|
|
188
|
+
attempts,
|
|
189
|
+
errors
|
|
190
|
+
});
|
|
191
|
+
throw new Error(`Failed to handle event ${eventId}: ${errors.join(", ")}`);
|
|
98
192
|
}
|
|
99
193
|
async getCursor(savedChange) {
|
|
100
194
|
return await this.reader.getCursor(savedChange);
|
|
@@ -108,7 +202,10 @@ var FirestoreProjector = class {
|
|
|
108
202
|
}
|
|
109
203
|
if (!isTargetAfterHead) {
|
|
110
204
|
const processed = await this.checkIsProcessed(checkpointId, target);
|
|
111
|
-
if (processed === TaskState.PROCESSED)
|
|
205
|
+
if (processed === TaskState.PROCESSED) {
|
|
206
|
+
this.prunePendingCursors(checkpointId, [target.eventId]);
|
|
207
|
+
return [Status.SUCCESS, "Target event already processed"];
|
|
208
|
+
}
|
|
112
209
|
if (processed === TaskState.MISSING) {
|
|
113
210
|
const [status, message] = await this.enqueueOne(checkpointId, target);
|
|
114
211
|
if (status === Status.DEFERRED) return [Status.FAILURE, message];
|
|
@@ -137,14 +234,25 @@ var FirestoreProjector = class {
|
|
|
137
234
|
const settings = this.projection.getTaskSettings(e);
|
|
138
235
|
return Task.new(e, settings);
|
|
139
236
|
});
|
|
140
|
-
|
|
237
|
+
const result = await this.queue.enqueue(checkpointId, tasks);
|
|
238
|
+
const [status] = result;
|
|
239
|
+
if (status === Status.SUCCESS) this.prunePendingCursors(checkpointId, tasks.map((t) => t.id));
|
|
240
|
+
const checkpointKey = checkpointId.serialize();
|
|
241
|
+
this.logger.debug(`enqueued: Checkpoint<${checkpointKey}> added ${tasks.length} tasks to queue`, {
|
|
242
|
+
checkpointId: checkpointKey,
|
|
243
|
+
count: tasks.length
|
|
244
|
+
});
|
|
245
|
+
return result;
|
|
141
246
|
}
|
|
142
247
|
async enqueueOne(checkpointId, target) {
|
|
143
248
|
const event = await this.reader.get(target);
|
|
144
249
|
if (!event) throw new Error(`Event not found for cursor ${target.ref}`);
|
|
145
250
|
const settings = this.projection.getTaskSettings(event);
|
|
146
251
|
const task = Task.new(event, settings);
|
|
147
|
-
|
|
252
|
+
const result = await this.queue.enqueue(checkpointId, [task]);
|
|
253
|
+
const [status] = result;
|
|
254
|
+
if (status === Status.SUCCESS) this.prunePendingCursors(checkpointId, [task.id]);
|
|
255
|
+
return result;
|
|
148
256
|
}
|
|
149
257
|
async checkIsProcessed(checkpointId, cursor) {
|
|
150
258
|
return await this.queue.isProcessed(checkpointId, cursor);
|
|
@@ -174,6 +282,13 @@ var FirestoreProjector = class {
|
|
|
174
282
|
if ((await this.projection.process(filtered, context)).some((id) => id?.equals(targetEventId))) return [Status.SUCCESS, "Target event processed successfully"];
|
|
175
283
|
return [Status.DEFERRED, "Target event not processed yet"];
|
|
176
284
|
} catch (e) {
|
|
285
|
+
const checkpointKey = checkpointId.serialize();
|
|
286
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
287
|
+
const truncated = errorMessage.length > 200 ? `${errorMessage.slice(0, 200)}...` : errorMessage;
|
|
288
|
+
this.logger.error(`error: Checkpoint<${checkpointKey}> processing failed: ${truncated}`, {
|
|
289
|
+
checkpointId: checkpointKey,
|
|
290
|
+
error: e
|
|
291
|
+
});
|
|
177
292
|
this.config.onProcessError(e);
|
|
178
293
|
if (this._unclaim) await this.queue.unclaim(checkpointId, tasks);
|
|
179
294
|
if (!hasTarget) return [Status.DEFERRED, "Target event not in claimed batch, deferring"];
|
|
@@ -267,7 +382,6 @@ var FirestoreQueueStore = class {
|
|
|
267
382
|
const originalClaimIds = task.claimIds;
|
|
268
383
|
task.checkTimeout();
|
|
269
384
|
if (originalClaimIds.length > task.claimIds.length) expiredTasks.push(task);
|
|
270
|
-
if (task.claimedAt || task.claimer) expiredTasks.push(task);
|
|
271
385
|
}
|
|
272
386
|
if (expiredTasks.length > 0) {
|
|
273
387
|
const batch = this.collection.firestore.batch();
|
|
@@ -497,7 +611,9 @@ var Task = class Task extends (0, _ddd_ts_shape.Shape)({
|
|
|
497
611
|
static deserializeWithLastUpdateTime(data, timestamp) {
|
|
498
612
|
return Task.deserialize({
|
|
499
613
|
...data,
|
|
500
|
-
lastUpdateTime: timestamp
|
|
614
|
+
lastUpdateTime: timestamp,
|
|
615
|
+
claimIds: data.claimIds || [],
|
|
616
|
+
claimsMetadata: data.claimsMetadata || {}
|
|
501
617
|
});
|
|
502
618
|
}
|
|
503
619
|
static batch(tasks) {
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { EventCoordinator } from "./event-coordinator.mjs";
|
|
2
1
|
import { Cursor, EventId, Lock, ProjectedStreamReader } from "@ddd-ts/core";
|
|
3
2
|
import { DefaultConverter } from "@ddd-ts/store-firestore";
|
|
4
3
|
import { FieldValue, Timestamp } from "firebase-admin/firestore";
|
|
@@ -17,6 +16,12 @@ const TaskState = {
|
|
|
17
16
|
};
|
|
18
17
|
const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
18
|
const RETENTION = MicrosecondTimestamp.MONTH;
|
|
19
|
+
const defaultLogger = {
|
|
20
|
+
debug: (msg, ctx) => console.debug(msg, ctx ?? ""),
|
|
21
|
+
info: (msg, ctx) => console.info(msg, ctx ?? ""),
|
|
22
|
+
warn: (msg, ctx) => console.warn(msg, ctx ?? ""),
|
|
23
|
+
error: (msg, ctx) => console.error(msg, ctx ?? "")
|
|
24
|
+
};
|
|
20
25
|
var FirestoreProjector = class {
|
|
21
26
|
_unclaim = true;
|
|
22
27
|
constructor(projection, reader, queue, config = {
|
|
@@ -27,6 +32,7 @@ var FirestoreProjector = class {
|
|
|
27
32
|
backoff: 1.5
|
|
28
33
|
},
|
|
29
34
|
enqueue: { batchSize: 100 },
|
|
35
|
+
logger: defaultLogger,
|
|
30
36
|
onProcessError: (error) => {
|
|
31
37
|
console.error("Error processing event:", error);
|
|
32
38
|
},
|
|
@@ -39,6 +45,9 @@ var FirestoreProjector = class {
|
|
|
39
45
|
this.queue = queue;
|
|
40
46
|
this.config = config;
|
|
41
47
|
}
|
|
48
|
+
get logger() {
|
|
49
|
+
return this.config.logger ?? defaultLogger;
|
|
50
|
+
}
|
|
42
51
|
async *breathe() {
|
|
43
52
|
const { attempts, minDelay, maxDelay, backoff } = this.config.retry;
|
|
44
53
|
for (let i = 0; i < attempts; i++) {
|
|
@@ -51,32 +60,106 @@ var FirestoreProjector = class {
|
|
|
51
60
|
await wait((backoff * i + 1) * minDelay + jitter);
|
|
52
61
|
}
|
|
53
62
|
}
|
|
54
|
-
|
|
55
|
-
|
|
63
|
+
processingCheckpoints = /* @__PURE__ */ new Map();
|
|
64
|
+
coalescedCounts = /* @__PURE__ */ new Map();
|
|
65
|
+
pendingCursors = /* @__PURE__ */ new Map();
|
|
66
|
+
prunePendingCursors(checkpointId, eventIds) {
|
|
56
67
|
const key = checkpointId.serialize();
|
|
57
|
-
|
|
58
|
-
if (!
|
|
59
|
-
|
|
60
|
-
coordinator.onEmpty(() => this.eventCoordinators.delete(key));
|
|
61
|
-
this.eventCoordinators.set(key, coordinator);
|
|
62
|
-
}
|
|
63
|
-
return coordinator;
|
|
68
|
+
const pending = this.pendingCursors.get(key);
|
|
69
|
+
if (!pending) return;
|
|
70
|
+
for (const eventId of eventIds) pending.delete(eventId.serialize());
|
|
64
71
|
}
|
|
65
72
|
async handle(savedChange) {
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
73
|
+
const key = this.projection.getCheckpointId(savedChange).serialize();
|
|
74
|
+
const cursor = await this.getCursor(savedChange);
|
|
75
|
+
if (!cursor) throw new Error(`Cursor not found for event ${savedChange.id.serialize()}`);
|
|
76
|
+
if (this.processingCheckpoints.has(key)) {
|
|
77
|
+
if (!this.pendingCursors.has(key)) this.pendingCursors.set(key, /* @__PURE__ */ new Map());
|
|
78
|
+
const eventId = savedChange.id.serialize();
|
|
79
|
+
this.pendingCursors.get(key).set(eventId, {
|
|
80
|
+
savedChange,
|
|
81
|
+
cursor
|
|
82
|
+
});
|
|
83
|
+
const existing = this.processingCheckpoints.get(key);
|
|
84
|
+
if (!existing || cursor.isAfter(existing.cursor)) this.processingCheckpoints.set(key, {
|
|
85
|
+
savedChange,
|
|
86
|
+
cursor
|
|
87
|
+
});
|
|
88
|
+
const coalesced = (this.coalescedCounts.get(key) ?? 0) + 1;
|
|
89
|
+
this.coalescedCounts.set(key, coalesced);
|
|
90
|
+
this.logger.debug(`debounced: Checkpoint<${key}> is processing, Event<${eventId}> coalesced (${coalesced} pending)`, {
|
|
91
|
+
checkpointId: key,
|
|
92
|
+
eventId,
|
|
93
|
+
coalesced
|
|
94
|
+
});
|
|
72
95
|
return;
|
|
73
96
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
97
|
+
this.processingCheckpoints.set(key, null);
|
|
98
|
+
try {
|
|
99
|
+
await this.handleOne(savedChange, cursor);
|
|
100
|
+
const nextPending = () => {
|
|
101
|
+
const debounced = this.processingCheckpoints.get(key);
|
|
102
|
+
if (debounced) {
|
|
103
|
+
this.processingCheckpoints.set(key, null);
|
|
104
|
+
return {
|
|
105
|
+
type: "debounced",
|
|
106
|
+
savedChange: debounced.savedChange,
|
|
107
|
+
cursor: debounced.cursor,
|
|
108
|
+
eventId: debounced.savedChange.id.serialize()
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const remaining = this.pendingCursors.get(key);
|
|
112
|
+
if (remaining && remaining.size > 0) {
|
|
113
|
+
const [eventId, straggler] = remaining.entries().next().value;
|
|
114
|
+
remaining.delete(eventId);
|
|
115
|
+
return {
|
|
116
|
+
type: "straggler",
|
|
117
|
+
savedChange: straggler.savedChange,
|
|
118
|
+
cursor: straggler.cursor,
|
|
119
|
+
eventId
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
};
|
|
124
|
+
let iterations = 0;
|
|
125
|
+
for (let next = nextPending(); next; next = nextPending()) {
|
|
126
|
+
iterations++;
|
|
127
|
+
if (iterations > 10) this.logger.warn(`high-iteration: Checkpoint<${key}> re-run loop at iteration ${iterations}`, {
|
|
128
|
+
checkpointId: key,
|
|
129
|
+
iterations
|
|
130
|
+
});
|
|
131
|
+
if (next.type === "debounced") this.logger.debug(`re-run: Checkpoint<${key}> iteration ${iterations}, targeting Event<${next.eventId}>`, {
|
|
132
|
+
checkpointId: key,
|
|
133
|
+
iterations,
|
|
134
|
+
eventId: next.eventId
|
|
135
|
+
});
|
|
136
|
+
else {
|
|
137
|
+
const remaining = this.pendingCursors.get(key);
|
|
138
|
+
this.logger.info(`straggler: Checkpoint<${key}> handling Event<${next.eventId}> not covered by any slice (${remaining?.size ?? 0} remaining)`, {
|
|
139
|
+
checkpointId: key,
|
|
140
|
+
eventId: next.eventId,
|
|
141
|
+
remainingStragglers: remaining?.size ?? 0
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
await this.handleOne(next.savedChange, next.cursor);
|
|
145
|
+
}
|
|
146
|
+
} finally {
|
|
147
|
+
this.processingCheckpoints.delete(key);
|
|
148
|
+
this.pendingCursors.delete(key);
|
|
149
|
+
this.coalescedCounts.delete(key);
|
|
79
150
|
}
|
|
151
|
+
}
|
|
152
|
+
async handleOne(savedChange, target) {
|
|
153
|
+
const checkpointId = this.projection.getCheckpointId(savedChange);
|
|
154
|
+
const eventId = savedChange.id.serialize();
|
|
155
|
+
const checkpointKey = checkpointId.serialize();
|
|
156
|
+
const startedAt = Date.now();
|
|
157
|
+
this.logger.info(`processing: Checkpoint<${checkpointKey}> targeting Event<${eventId}>`, {
|
|
158
|
+
checkpointId: checkpointKey,
|
|
159
|
+
eventId,
|
|
160
|
+
target: target.toString(),
|
|
161
|
+
targetRef: target.ref
|
|
162
|
+
});
|
|
80
163
|
const errors = [];
|
|
81
164
|
for await (const [attempt, reset] of this.breathe()) {
|
|
82
165
|
const source = this.projection.getSource(savedChange);
|
|
@@ -87,13 +170,24 @@ var FirestoreProjector = class {
|
|
|
87
170
|
}
|
|
88
171
|
if (status === Status.SUCCESS) {
|
|
89
172
|
await this.queue.cleanup(checkpointId);
|
|
90
|
-
|
|
173
|
+
const durationMs = Date.now() - startedAt;
|
|
174
|
+
this.logger.info(`processed: Checkpoint<${checkpointKey}> caught up to Event<${eventId}> in ${durationMs}ms`, {
|
|
175
|
+
checkpointId: checkpointKey,
|
|
176
|
+
eventId,
|
|
177
|
+
durationMs
|
|
178
|
+
});
|
|
91
179
|
return;
|
|
92
180
|
}
|
|
93
181
|
errors.push(message);
|
|
94
182
|
}
|
|
95
|
-
|
|
96
|
-
|
|
183
|
+
const { attempts } = this.config.retry;
|
|
184
|
+
this.logger.error(`failed: Checkpoint<${checkpointKey}> exhausted ${attempts} retries for Event<${eventId}>`, {
|
|
185
|
+
checkpointId: checkpointKey,
|
|
186
|
+
eventId,
|
|
187
|
+
attempts,
|
|
188
|
+
errors
|
|
189
|
+
});
|
|
190
|
+
throw new Error(`Failed to handle event ${eventId}: ${errors.join(", ")}`);
|
|
97
191
|
}
|
|
98
192
|
async getCursor(savedChange) {
|
|
99
193
|
return await this.reader.getCursor(savedChange);
|
|
@@ -107,7 +201,10 @@ var FirestoreProjector = class {
|
|
|
107
201
|
}
|
|
108
202
|
if (!isTargetAfterHead) {
|
|
109
203
|
const processed = await this.checkIsProcessed(checkpointId, target);
|
|
110
|
-
if (processed === TaskState.PROCESSED)
|
|
204
|
+
if (processed === TaskState.PROCESSED) {
|
|
205
|
+
this.prunePendingCursors(checkpointId, [target.eventId]);
|
|
206
|
+
return [Status.SUCCESS, "Target event already processed"];
|
|
207
|
+
}
|
|
111
208
|
if (processed === TaskState.MISSING) {
|
|
112
209
|
const [status, message] = await this.enqueueOne(checkpointId, target);
|
|
113
210
|
if (status === Status.DEFERRED) return [Status.FAILURE, message];
|
|
@@ -136,14 +233,25 @@ var FirestoreProjector = class {
|
|
|
136
233
|
const settings = this.projection.getTaskSettings(e);
|
|
137
234
|
return Task.new(e, settings);
|
|
138
235
|
});
|
|
139
|
-
|
|
236
|
+
const result = await this.queue.enqueue(checkpointId, tasks);
|
|
237
|
+
const [status] = result;
|
|
238
|
+
if (status === Status.SUCCESS) this.prunePendingCursors(checkpointId, tasks.map((t) => t.id));
|
|
239
|
+
const checkpointKey = checkpointId.serialize();
|
|
240
|
+
this.logger.debug(`enqueued: Checkpoint<${checkpointKey}> added ${tasks.length} tasks to queue`, {
|
|
241
|
+
checkpointId: checkpointKey,
|
|
242
|
+
count: tasks.length
|
|
243
|
+
});
|
|
244
|
+
return result;
|
|
140
245
|
}
|
|
141
246
|
async enqueueOne(checkpointId, target) {
|
|
142
247
|
const event = await this.reader.get(target);
|
|
143
248
|
if (!event) throw new Error(`Event not found for cursor ${target.ref}`);
|
|
144
249
|
const settings = this.projection.getTaskSettings(event);
|
|
145
250
|
const task = Task.new(event, settings);
|
|
146
|
-
|
|
251
|
+
const result = await this.queue.enqueue(checkpointId, [task]);
|
|
252
|
+
const [status] = result;
|
|
253
|
+
if (status === Status.SUCCESS) this.prunePendingCursors(checkpointId, [task.id]);
|
|
254
|
+
return result;
|
|
147
255
|
}
|
|
148
256
|
async checkIsProcessed(checkpointId, cursor) {
|
|
149
257
|
return await this.queue.isProcessed(checkpointId, cursor);
|
|
@@ -173,6 +281,13 @@ var FirestoreProjector = class {
|
|
|
173
281
|
if ((await this.projection.process(filtered, context)).some((id) => id?.equals(targetEventId))) return [Status.SUCCESS, "Target event processed successfully"];
|
|
174
282
|
return [Status.DEFERRED, "Target event not processed yet"];
|
|
175
283
|
} catch (e) {
|
|
284
|
+
const checkpointKey = checkpointId.serialize();
|
|
285
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
286
|
+
const truncated = errorMessage.length > 200 ? `${errorMessage.slice(0, 200)}...` : errorMessage;
|
|
287
|
+
this.logger.error(`error: Checkpoint<${checkpointKey}> processing failed: ${truncated}`, {
|
|
288
|
+
checkpointId: checkpointKey,
|
|
289
|
+
error: e
|
|
290
|
+
});
|
|
176
291
|
this.config.onProcessError(e);
|
|
177
292
|
if (this._unclaim) await this.queue.unclaim(checkpointId, tasks);
|
|
178
293
|
if (!hasTarget) return [Status.DEFERRED, "Target event not in claimed batch, deferring"];
|
|
@@ -266,7 +381,6 @@ var FirestoreQueueStore = class {
|
|
|
266
381
|
const originalClaimIds = task.claimIds;
|
|
267
382
|
task.checkTimeout();
|
|
268
383
|
if (originalClaimIds.length > task.claimIds.length) expiredTasks.push(task);
|
|
269
|
-
if (task.claimedAt || task.claimer) expiredTasks.push(task);
|
|
270
384
|
}
|
|
271
385
|
if (expiredTasks.length > 0) {
|
|
272
386
|
const batch = this.collection.firestore.batch();
|
|
@@ -496,7 +610,9 @@ var Task = class Task extends Shape({
|
|
|
496
610
|
static deserializeWithLastUpdateTime(data, timestamp) {
|
|
497
611
|
return Task.deserialize({
|
|
498
612
|
...data,
|
|
499
|
-
lastUpdateTime: timestamp
|
|
613
|
+
lastUpdateTime: timestamp,
|
|
614
|
+
claimIds: data.claimIds || [],
|
|
615
|
+
claimsMetadata: data.claimsMetadata || {}
|
|
500
616
|
});
|
|
501
617
|
}
|
|
502
618
|
static batch(tasks) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ddd-ts/event-sourcing-firestore",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.43",
|
|
4
4
|
"types": "dist/index.d.ts",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -10,19 +10,19 @@
|
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@ddd-ts/core": "0.0.
|
|
14
|
-
"@ddd-ts/shape": "0.0.
|
|
15
|
-
"@ddd-ts/store-firestore": "0.0.
|
|
16
|
-
"@ddd-ts/traits": "0.0.
|
|
17
|
-
"@ddd-ts/types": "0.0.
|
|
13
|
+
"@ddd-ts/core": "0.0.43",
|
|
14
|
+
"@ddd-ts/shape": "0.0.43",
|
|
15
|
+
"@ddd-ts/store-firestore": "0.0.43",
|
|
16
|
+
"@ddd-ts/traits": "0.0.43",
|
|
17
|
+
"@ddd-ts/types": "0.0.43",
|
|
18
18
|
"@opentelemetry/api": "1.6.0",
|
|
19
19
|
"firebase-admin": "^13.2.0"
|
|
20
20
|
},
|
|
21
21
|
"devDependencies": {
|
|
22
|
-
"@ddd-ts/shape": "0.0.
|
|
23
|
-
"@ddd-ts/tests": "0.0.
|
|
24
|
-
"@ddd-ts/tools": "0.0.
|
|
25
|
-
"@ddd-ts/types": "0.0.
|
|
22
|
+
"@ddd-ts/shape": "0.0.43",
|
|
23
|
+
"@ddd-ts/tests": "0.0.43",
|
|
24
|
+
"@ddd-ts/tools": "0.0.43",
|
|
25
|
+
"@ddd-ts/types": "0.0.43",
|
|
26
26
|
"@types/jest": "^29.5.1"
|
|
27
27
|
},
|
|
28
28
|
"exports": {
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { IEsEvent, ISavedChange } from "@ddd-ts/core";
|
|
2
|
-
export declare class EventCoordinator {
|
|
3
|
-
private eventProcessing;
|
|
4
|
-
private currentEventId;
|
|
5
|
-
private lastEvent;
|
|
6
|
-
private isRunning;
|
|
7
|
-
private _onEmpty;
|
|
8
|
-
addEvent(event: ISavedChange<IEsEvent>): void;
|
|
9
|
-
start(event: ISavedChange<IEsEvent>): () => void;
|
|
10
|
-
waitCurrentEvent(): Promise<void>;
|
|
11
|
-
cleanEvent(event: ISavedChange<IEsEvent>): void;
|
|
12
|
-
canProceed(event: ISavedChange<IEsEvent>): boolean;
|
|
13
|
-
onEmpty(callback: () => void): void;
|
|
14
|
-
checkEmpty(): void;
|
|
15
|
-
}
|
|
16
|
-
//# sourceMappingURL=event-coordinator.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"event-coordinator.d.ts","sourceRoot":"","sources":["../../src/projection/event-coordinator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAG3D,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,eAAe,CAAkD;IACzE,OAAO,CAAC,cAAc,CAAuB;IAC7C,OAAO,CAAC,SAAS,CAAuC;IACxD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAY;IAE5B,QAAQ,CAAC,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC;IAYtC,KAAK,CAAC,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC;IAU7B,gBAAgB;IAMtB,UAAU,CAAC,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC;IAYxC,UAAU,CAAC,KAAK,EAAE,YAAY,CAAC,QAAQ,CAAC;IAMxC,OAAO,CAAC,QAAQ,EAAE,MAAM,IAAI;IAI5B,UAAU;CAKX"}
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
const require_promise_with_resolvers = require('../utils/promise-with-resolvers.js');
|
|
2
|
-
|
|
3
|
-
//#region src/projection/event-coordinator.ts
|
|
4
|
-
var EventCoordinator = class {
|
|
5
|
-
eventProcessing = /* @__PURE__ */ new Map();
|
|
6
|
-
currentEventId = null;
|
|
7
|
-
lastEvent = null;
|
|
8
|
-
isRunning = false;
|
|
9
|
-
_onEmpty = () => {};
|
|
10
|
-
addEvent(event) {
|
|
11
|
-
const eventId = event.id.serialize();
|
|
12
|
-
this.eventProcessing.set(eventId, require_promise_with_resolvers.promiseWithResolvers());
|
|
13
|
-
if (this.lastEvent === null || this.lastEvent.revision < event.revision) this.lastEvent = event;
|
|
14
|
-
}
|
|
15
|
-
start(event) {
|
|
16
|
-
if (this.isRunning) throw new Error("Event processing already in progress");
|
|
17
|
-
this.currentEventId = event.id.serialize();
|
|
18
|
-
this.isRunning = true;
|
|
19
|
-
return () => this.cleanEvent(event);
|
|
20
|
-
}
|
|
21
|
-
async waitCurrentEvent() {
|
|
22
|
-
if (!this.currentEventId) return;
|
|
23
|
-
await this.eventProcessing.get(this.currentEventId)?.promise;
|
|
24
|
-
}
|
|
25
|
-
cleanEvent(event) {
|
|
26
|
-
const eventId = event.id.serialize();
|
|
27
|
-
this.eventProcessing.get(eventId)?.resolve();
|
|
28
|
-
this.eventProcessing.delete(eventId);
|
|
29
|
-
this.isRunning = false;
|
|
30
|
-
this.currentEventId = null;
|
|
31
|
-
this.checkEmpty();
|
|
32
|
-
}
|
|
33
|
-
canProceed(event) {
|
|
34
|
-
if (this.isRunning) return false;
|
|
35
|
-
const eventId = event.id.serialize();
|
|
36
|
-
return this.lastEvent?.id.serialize() === eventId;
|
|
37
|
-
}
|
|
38
|
-
onEmpty(callback) {
|
|
39
|
-
this._onEmpty = callback;
|
|
40
|
-
}
|
|
41
|
-
checkEmpty() {
|
|
42
|
-
if (this.eventProcessing.size === 0) this._onEmpty();
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
//#endregion
|
|
47
|
-
exports.EventCoordinator = EventCoordinator;
|
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { promiseWithResolvers } from "../utils/promise-with-resolvers.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/projection/event-coordinator.ts
|
|
4
|
-
var EventCoordinator = class {
|
|
5
|
-
eventProcessing = /* @__PURE__ */ new Map();
|
|
6
|
-
currentEventId = null;
|
|
7
|
-
lastEvent = null;
|
|
8
|
-
isRunning = false;
|
|
9
|
-
_onEmpty = () => {};
|
|
10
|
-
addEvent(event) {
|
|
11
|
-
const eventId = event.id.serialize();
|
|
12
|
-
this.eventProcessing.set(eventId, promiseWithResolvers());
|
|
13
|
-
if (this.lastEvent === null || this.lastEvent.revision < event.revision) this.lastEvent = event;
|
|
14
|
-
}
|
|
15
|
-
start(event) {
|
|
16
|
-
if (this.isRunning) throw new Error("Event processing already in progress");
|
|
17
|
-
this.currentEventId = event.id.serialize();
|
|
18
|
-
this.isRunning = true;
|
|
19
|
-
return () => this.cleanEvent(event);
|
|
20
|
-
}
|
|
21
|
-
async waitCurrentEvent() {
|
|
22
|
-
if (!this.currentEventId) return;
|
|
23
|
-
await this.eventProcessing.get(this.currentEventId)?.promise;
|
|
24
|
-
}
|
|
25
|
-
cleanEvent(event) {
|
|
26
|
-
const eventId = event.id.serialize();
|
|
27
|
-
this.eventProcessing.get(eventId)?.resolve();
|
|
28
|
-
this.eventProcessing.delete(eventId);
|
|
29
|
-
this.isRunning = false;
|
|
30
|
-
this.currentEventId = null;
|
|
31
|
-
this.checkEmpty();
|
|
32
|
-
}
|
|
33
|
-
canProceed(event) {
|
|
34
|
-
if (this.isRunning) return false;
|
|
35
|
-
const eventId = event.id.serialize();
|
|
36
|
-
return this.lastEvent?.id.serialize() === eventId;
|
|
37
|
-
}
|
|
38
|
-
onEmpty(callback) {
|
|
39
|
-
this._onEmpty = callback;
|
|
40
|
-
}
|
|
41
|
-
checkEmpty() {
|
|
42
|
-
if (this.eventProcessing.size === 0) this._onEmpty();
|
|
43
|
-
}
|
|
44
|
-
};
|
|
45
|
-
|
|
46
|
-
//#endregion
|
|
47
|
-
export { EventCoordinator };
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"promise-with-resolvers.d.ts","sourceRoot":"","sources":["../../src/utils/promise-with-resolvers.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,gBAAgB,CAAC,CAAC;IACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACpB,OAAO,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,IAAI,CAAC;IAC5B,MAAM,EAAE,CAAC,MAAM,CAAC,EAAE,GAAG,KAAK,IAAI,CAAC;CAChC;AAED,eAAO,MAAM,oBAAoB,GAAI,CAAC,OAAK,gBAAgB,CAAC,CAAC,CAQ5D,CAAC"}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
//#region src/utils/promise-with-resolvers.ts
|
|
3
|
-
const promiseWithResolvers = () => {
|
|
4
|
-
let resolve;
|
|
5
|
-
let reject;
|
|
6
|
-
return {
|
|
7
|
-
promise: new Promise((res, rej) => {
|
|
8
|
-
resolve = res;
|
|
9
|
-
reject = rej;
|
|
10
|
-
}),
|
|
11
|
-
resolve,
|
|
12
|
-
reject
|
|
13
|
-
};
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
//#endregion
|
|
17
|
-
exports.promiseWithResolvers = promiseWithResolvers;
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
//#region src/utils/promise-with-resolvers.ts
|
|
2
|
-
const promiseWithResolvers = () => {
|
|
3
|
-
let resolve;
|
|
4
|
-
let reject;
|
|
5
|
-
return {
|
|
6
|
-
promise: new Promise((res, rej) => {
|
|
7
|
-
resolve = res;
|
|
8
|
-
reject = rej;
|
|
9
|
-
}),
|
|
10
|
-
resolve,
|
|
11
|
-
reject
|
|
12
|
-
};
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
//#endregion
|
|
16
|
-
export { promiseWithResolvers };
|