@enbox/dwn-sdk-js 0.3.0 → 0.3.2
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/browser.mjs +6 -6
- package/dist/browser.mjs.map +3 -3
- package/dist/esm/generated/precompiled-validators.js +704 -342
- package/dist/esm/generated/precompiled-validators.js.map +1 -1
- package/dist/esm/src/core/dwn-error.js +1 -0
- package/dist/esm/src/core/dwn-error.js.map +1 -1
- package/dist/esm/src/event-stream/event-emitter-event-log.js +152 -22
- package/dist/esm/src/event-stream/event-emitter-event-log.js.map +1 -1
- package/dist/esm/src/handlers/messages-subscribe.js +7 -0
- package/dist/esm/src/handlers/messages-subscribe.js.map +1 -1
- package/dist/esm/src/handlers/protocols-configure.js +1 -1
- package/dist/esm/src/handlers/protocols-configure.js.map +1 -1
- package/dist/esm/src/handlers/records-subscribe.js +7 -0
- package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
- package/dist/esm/src/handlers/records-write.js +3 -2
- package/dist/esm/src/handlers/records-write.js.map +1 -1
- package/dist/esm/src/interfaces/messages-subscribe.js.map +1 -1
- package/dist/esm/src/interfaces/records-subscribe.js.map +1 -1
- package/dist/esm/src/store/storage-controller.js +1 -1
- package/dist/esm/src/store/storage-controller.js.map +1 -1
- package/dist/esm/src/utils/messages.js +41 -1
- package/dist/esm/src/utils/messages.js.map +1 -1
- package/dist/esm/tests/event-emitter-event-log.spec.js +278 -84
- package/dist/esm/tests/event-emitter-event-log.spec.js.map +1 -1
- package/dist/esm/tests/handlers/messages-subscribe.spec.js +288 -0
- package/dist/esm/tests/handlers/messages-subscribe.spec.js.map +1 -1
- package/dist/esm/tests/utils/test-data-generator.js.map +1 -1
- package/dist/types/generated/precompiled-validators.d.ts +50 -34
- package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
- package/dist/types/src/core/dwn-error.d.ts +1 -0
- package/dist/types/src/core/dwn-error.d.ts.map +1 -1
- package/dist/types/src/event-stream/event-emitter-event-log.d.ts +34 -4
- package/dist/types/src/event-stream/event-emitter-event-log.d.ts.map +1 -1
- package/dist/types/src/handlers/messages-subscribe.d.ts +1 -1
- package/dist/types/src/handlers/messages-subscribe.d.ts.map +1 -1
- package/dist/types/src/handlers/records-subscribe.d.ts +1 -1
- package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
- package/dist/types/src/handlers/records-write.d.ts.map +1 -1
- package/dist/types/src/index.d.ts +1 -1
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/interfaces/messages-subscribe.d.ts +4 -3
- package/dist/types/src/interfaces/messages-subscribe.d.ts.map +1 -1
- package/dist/types/src/interfaces/records-subscribe.d.ts +4 -3
- package/dist/types/src/interfaces/records-subscribe.d.ts.map +1 -1
- package/dist/types/src/types/messages-types.d.ts +14 -4
- package/dist/types/src/types/messages-types.d.ts.map +1 -1
- package/dist/types/src/types/records-types.d.ts +8 -4
- package/dist/types/src/types/records-types.d.ts.map +1 -1
- package/dist/types/src/types/subscriptions.d.ts +80 -20
- package/dist/types/src/types/subscriptions.d.ts.map +1 -1
- package/dist/types/src/utils/messages.d.ts.map +1 -1
- package/dist/types/tests/handlers/messages-subscribe.spec.d.ts.map +1 -1
- package/dist/types/tests/utils/test-data-generator.d.ts +5 -4
- package/dist/types/tests/utils/test-data-generator.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/core/dwn-error.ts +1 -0
- package/src/event-stream/event-emitter-event-log.ts +174 -27
- package/src/handlers/messages-subscribe.ts +8 -1
- package/src/handlers/protocols-configure.ts +1 -1
- package/src/handlers/records-subscribe.ts +8 -1
- package/src/handlers/records-write.ts +3 -2
- package/src/index.ts +1 -1
- package/src/interfaces/messages-subscribe.ts +4 -3
- package/src/interfaces/records-subscribe.ts +4 -3
- package/src/store/storage-controller.ts +1 -1
- package/src/types/messages-types.ts +12 -4
- package/src/types/records-types.ts +6 -4
- package/src/types/subscriptions.ts +86 -20
- package/src/utils/messages.ts +47 -1
|
@@ -7,6 +7,9 @@ import type {
|
|
|
7
7
|
EventLogSubscribeOptions,
|
|
8
8
|
EventSubscription,
|
|
9
9
|
MessageEvent,
|
|
10
|
+
ProgressGapInfo,
|
|
11
|
+
ProgressGapReason,
|
|
12
|
+
ProgressToken,
|
|
10
13
|
SubscriptionListener,
|
|
11
14
|
} from '../types/subscriptions.js';
|
|
12
15
|
|
|
@@ -21,7 +24,7 @@ const EVENTS_LISTENER_CHANNEL = 'events';
|
|
|
21
24
|
* Payload shape used internally by mitt. We bundle the three EventListener
|
|
22
25
|
* arguments into a single object because mitt emits one value per event.
|
|
23
26
|
*/
|
|
24
|
-
type EmitterPayload = { tenant: string; event: MessageEvent; indexes: KeyValues; seq: number };
|
|
27
|
+
type EmitterPayload = { tenant: string; event: MessageEvent; indexes: KeyValues; seq: number; messageCid: string };
|
|
25
28
|
|
|
26
29
|
/**
|
|
27
30
|
* mitt event map — every channel name maps to an `EmitterPayload`.
|
|
@@ -30,11 +33,12 @@ type EmitterPayload = { tenant: string; event: MessageEvent; indexes: KeyValues;
|
|
|
30
33
|
type EmitterEvents = Record<string, EmitterPayload>;
|
|
31
34
|
|
|
32
35
|
/**
|
|
33
|
-
* Internal storage entry — the event plus its indexes.
|
|
36
|
+
* Internal storage entry — the event plus its indexes and message CID.
|
|
34
37
|
*/
|
|
35
38
|
type StoredEntry = {
|
|
36
39
|
event : MessageEvent;
|
|
37
40
|
indexes : KeyValues;
|
|
41
|
+
messageCid : string;
|
|
38
42
|
};
|
|
39
43
|
|
|
40
44
|
export interface EventEmitterEventLogConfig {
|
|
@@ -77,14 +81,92 @@ export class EventEmitterEventLog implements EventLog {
|
|
|
77
81
|
*/
|
|
78
82
|
private tenantSeqs: Map<string, number> = new Map();
|
|
79
83
|
|
|
84
|
+
/**
|
|
85
|
+
* Epoch for this EventLog instance. Generated once at construction as a
|
|
86
|
+
* UUID v4. Changes every restart (correct for in-memory — state is lost).
|
|
87
|
+
*/
|
|
88
|
+
private readonly epoch: string;
|
|
89
|
+
|
|
80
90
|
constructor(config: EventEmitterEventLogConfig = {}) {
|
|
81
91
|
this.maxEventsPerTenant = config.maxEventsPerTenant ?? 10_000;
|
|
92
|
+
this.epoch = crypto.randomUUID();
|
|
82
93
|
|
|
83
94
|
if (config.errorHandler) {
|
|
84
95
|
this.errorHandler = config.errorHandler;
|
|
85
96
|
}
|
|
86
97
|
}
|
|
87
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Derives a stable `streamId` for a given tenant. Deterministic — same
|
|
101
|
+
* tenant always produces the same streamId on any instance.
|
|
102
|
+
*/
|
|
103
|
+
private async getStreamId(tenant: string): Promise<string> {
|
|
104
|
+
const bytes = new TextEncoder().encode(tenant);
|
|
105
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', bytes);
|
|
106
|
+
const hashArray = new Uint8Array(hashBuffer);
|
|
107
|
+
// Take first 16 hex chars (64 bits) of the hash for a compact stable ID.
|
|
108
|
+
return Array.from(hashArray.slice(0, 8), (b: number) => b.toString(16).padStart(2, '0')).join('');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Constructs a {@link ProgressToken} from internal state.
|
|
113
|
+
*/
|
|
114
|
+
private async buildToken(tenant: string, seq: number, messageCid: string): Promise<ProgressToken> {
|
|
115
|
+
return {
|
|
116
|
+
streamId : await this.getStreamId(tenant),
|
|
117
|
+
epoch : this.epoch,
|
|
118
|
+
position : String(seq),
|
|
119
|
+
messageCid,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Validates a cursor against the current EventLog state. Throws
|
|
125
|
+
* `DwnError(EventLogProgressGap)` with {@link ProgressGapInfo} metadata
|
|
126
|
+
* if the cursor cannot be resumed.
|
|
127
|
+
*/
|
|
128
|
+
private async validateCursor(tenant: string, cursor: ProgressToken): Promise<void> {
|
|
129
|
+
const expectedStreamId = await this.getStreamId(tenant);
|
|
130
|
+
|
|
131
|
+
let reason: ProgressGapReason;
|
|
132
|
+
if (cursor.streamId !== expectedStreamId) {
|
|
133
|
+
reason = 'stream_mismatch';
|
|
134
|
+
} else if (cursor.epoch !== this.epoch) {
|
|
135
|
+
reason = 'epoch_mismatch';
|
|
136
|
+
} else {
|
|
137
|
+
// Check if position is still within replay bounds.
|
|
138
|
+
const log = this.tenantLogs.get(tenant);
|
|
139
|
+
if (log !== undefined && log.size > 0) {
|
|
140
|
+
const firstSeq = log.keys().next().value as number;
|
|
141
|
+
const cursorSeq = EventEmitterEventLog.parsePosition(cursor.position);
|
|
142
|
+
if (cursorSeq < firstSeq - 1) {
|
|
143
|
+
// Cursor position has been evicted — events between cursor and firstSeq are lost.
|
|
144
|
+
reason = 'token_too_old';
|
|
145
|
+
} else {
|
|
146
|
+
return; // Cursor is valid.
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
return; // No events for tenant — cursor is vacuously valid (will get empty catch-up + EOSE).
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Build gap metadata.
|
|
154
|
+
const bounds = await this.getReplayBounds(tenant);
|
|
155
|
+
const gapInfo: ProgressGapInfo = {
|
|
156
|
+
requested : cursor,
|
|
157
|
+
oldestAvailable : bounds?.oldest ?? cursor,
|
|
158
|
+
latestAvailable : bounds?.latest ?? cursor,
|
|
159
|
+
reason,
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const error = new DwnError(
|
|
163
|
+
DwnErrorCode.EventLogProgressGap,
|
|
164
|
+
`progress token gap: ${reason}`
|
|
165
|
+
);
|
|
166
|
+
(error as any).gapInfo = gapInfo;
|
|
167
|
+
throw error;
|
|
168
|
+
}
|
|
169
|
+
|
|
88
170
|
public async open(): Promise<void> {
|
|
89
171
|
this.isOpen = true;
|
|
90
172
|
}
|
|
@@ -96,13 +178,13 @@ export class EventEmitterEventLog implements EventLog {
|
|
|
96
178
|
this.tenantSeqs.clear();
|
|
97
179
|
}
|
|
98
180
|
|
|
99
|
-
public async emit(tenant: string, event: MessageEvent, indexes: KeyValues): Promise<
|
|
181
|
+
public async emit(tenant: string, event: MessageEvent, indexes: KeyValues, messageCid: string): Promise<ProgressToken | undefined> {
|
|
100
182
|
if (!this.isOpen) {
|
|
101
183
|
this.errorHandler(new DwnError(
|
|
102
184
|
DwnErrorCode.EventLogNotOpenError,
|
|
103
185
|
'a message emitted when EventLog is closed'
|
|
104
186
|
));
|
|
105
|
-
return
|
|
187
|
+
return undefined;
|
|
106
188
|
}
|
|
107
189
|
|
|
108
190
|
// Assign a monotonic sequence number for this tenant.
|
|
@@ -116,7 +198,7 @@ export class EventEmitterEventLog implements EventLog {
|
|
|
116
198
|
log = new Map();
|
|
117
199
|
this.tenantLogs.set(tenant, log);
|
|
118
200
|
}
|
|
119
|
-
log.set(seq, { event, indexes });
|
|
201
|
+
log.set(seq, { event, indexes, messageCid });
|
|
120
202
|
|
|
121
203
|
// Evict oldest entries if the log exceeds the retention limit.
|
|
122
204
|
if (log.size > this.maxEventsPerTenant) {
|
|
@@ -131,14 +213,20 @@ export class EventEmitterEventLog implements EventLog {
|
|
|
131
213
|
|
|
132
214
|
// Notify in-process subscribers.
|
|
133
215
|
const channel = `${tenant}_${EVENTS_LISTENER_CHANNEL}`;
|
|
134
|
-
this.emitter.emit(channel, { tenant, event, indexes, seq });
|
|
216
|
+
this.emitter.emit(channel, { tenant, event, indexes, seq, messageCid });
|
|
135
217
|
|
|
136
|
-
return
|
|
218
|
+
return this.buildToken(tenant, seq, messageCid);
|
|
137
219
|
}
|
|
138
220
|
|
|
139
221
|
public async read(tenant: string, options: EventLogReadOptions = {}): Promise<EventLogReadResult> {
|
|
140
222
|
const { cursor, limit, filters } = options;
|
|
141
|
-
|
|
223
|
+
|
|
224
|
+
// Validate cursor before attempting to read.
|
|
225
|
+
if (cursor !== undefined) {
|
|
226
|
+
await this.validateCursor(tenant, cursor);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const cursorSeq = cursor !== undefined ? EventEmitterEventLog.parsePosition(cursor.position) : undefined;
|
|
142
230
|
const log = this.tenantLogs.get(tenant);
|
|
143
231
|
|
|
144
232
|
if (log === undefined || log.size === 0) {
|
|
@@ -159,26 +247,45 @@ export class EventEmitterEventLog implements EventLog {
|
|
|
159
247
|
|
|
160
248
|
results.push({
|
|
161
249
|
seq,
|
|
162
|
-
event
|
|
163
|
-
indexes
|
|
250
|
+
event : entry.event,
|
|
251
|
+
indexes : entry.indexes,
|
|
252
|
+
messageCid : entry.messageCid,
|
|
164
253
|
});
|
|
165
254
|
|
|
166
255
|
if (results.length >= maxResults) { break; }
|
|
167
256
|
}
|
|
168
257
|
|
|
169
|
-
|
|
170
|
-
|
|
258
|
+
if (results.length > 0) {
|
|
259
|
+
const lastEntry = results[results.length - 1];
|
|
260
|
+
// Use the messageCid captured during the synchronous iteration above —
|
|
261
|
+
// no re-lookup needed, so eviction during the await cannot lose it.
|
|
262
|
+
const lastToken = await this.buildToken(tenant, lastEntry.seq, lastEntry.messageCid!);
|
|
263
|
+
return { events: results, cursor: lastToken };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return { events: results, cursor };
|
|
171
267
|
}
|
|
172
268
|
|
|
173
269
|
/**
|
|
174
|
-
* Parse
|
|
270
|
+
* Parse a position string into an internal sequence number using BigInt
|
|
271
|
+
* for safe handling of values beyond `Number.MAX_SAFE_INTEGER`.
|
|
272
|
+
*
|
|
273
|
+
* The returned `number` is safe for the in-memory EventLog which uses
|
|
274
|
+
* `Map<number, StoredEntry>` keys — in-memory sequences will never
|
|
275
|
+
* exceed safe integer range. The BigInt parse validates correctness
|
|
276
|
+
* before the narrowing conversion.
|
|
175
277
|
*/
|
|
176
|
-
private static
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
278
|
+
private static parsePosition(position: string): number {
|
|
279
|
+
try {
|
|
280
|
+
const big = BigInt(position);
|
|
281
|
+
if (big < 0n) {
|
|
282
|
+
throw new DwnError(DwnErrorCode.EventLogNotOpenError, `invalid cursor position: '${position}'`);
|
|
283
|
+
}
|
|
284
|
+
return Number(big);
|
|
285
|
+
} catch (e) {
|
|
286
|
+
if (e instanceof DwnError) { throw e; }
|
|
287
|
+
throw new DwnError(DwnErrorCode.EventLogNotOpenError, `invalid cursor position: '${position}'`);
|
|
180
288
|
}
|
|
181
|
-
return seq;
|
|
182
289
|
}
|
|
183
290
|
|
|
184
291
|
public async subscribe(
|
|
@@ -190,12 +297,20 @@ export class EventEmitterEventLog implements EventLog {
|
|
|
190
297
|
const channel = `${tenant}_${EVENTS_LISTENER_CHANNEL}`;
|
|
191
298
|
const { cursor, filters } = options ?? {};
|
|
192
299
|
|
|
300
|
+
// Helper to build a token from a live emitter payload.
|
|
301
|
+
const tokenFromPayload = async (payload: EmitterPayload): Promise<ProgressToken> => {
|
|
302
|
+
return this.buildToken(tenant, payload.seq, payload.messageCid);
|
|
303
|
+
};
|
|
304
|
+
|
|
193
305
|
if (cursor !== undefined) {
|
|
194
306
|
// ---- Cursor mode: catch-up from stored events, then EOSE, then live ----
|
|
195
|
-
|
|
307
|
+
// Validate cursor before subscribing — throws DwnError(EventLogProgressGap) on gap.
|
|
308
|
+
await this.validateCursor(tenant, cursor);
|
|
309
|
+
|
|
310
|
+
const cursorSeq = EventEmitterEventLog.parsePosition(cursor.position);
|
|
196
311
|
|
|
197
312
|
// Buffer live events that arrive during catch-up to avoid losing them.
|
|
198
|
-
type BufferedEvent = { event: MessageEvent; seq: number };
|
|
313
|
+
type BufferedEvent = { event: MessageEvent; seq: number; messageCid: string };
|
|
199
314
|
const pendingLiveEvents: BufferedEvent[] = [];
|
|
200
315
|
let catchUpComplete = false;
|
|
201
316
|
|
|
@@ -205,9 +320,11 @@ export class EventEmitterEventLog implements EventLog {
|
|
|
205
320
|
if (!FilterUtility.matchAnyFilter(payload.indexes, filters)) { return; }
|
|
206
321
|
}
|
|
207
322
|
if (!catchUpComplete) {
|
|
208
|
-
pendingLiveEvents.push({ event: payload.event, seq: payload.seq });
|
|
323
|
+
pendingLiveEvents.push({ event: payload.event, seq: payload.seq, messageCid: payload.messageCid });
|
|
209
324
|
} else {
|
|
210
|
-
|
|
325
|
+
void tokenFromPayload(payload).then((token) => {
|
|
326
|
+
listener({ type: 'event', cursor: token, event: payload.event });
|
|
327
|
+
});
|
|
211
328
|
}
|
|
212
329
|
};
|
|
213
330
|
|
|
@@ -215,19 +332,27 @@ export class EventEmitterEventLog implements EventLog {
|
|
|
215
332
|
|
|
216
333
|
// Step 2: Read stored events from cursor and deliver them.
|
|
217
334
|
const readResult = await this.read(tenant, { cursor, filters });
|
|
218
|
-
// The read cursor is the last event
|
|
335
|
+
// The read cursor is the token of the last read event, or the input cursor if nothing new.
|
|
219
336
|
const eoseCursor = readResult.cursor ?? cursor;
|
|
220
|
-
const lastCatchUpSeq = readResult.cursor !== undefined
|
|
337
|
+
const lastCatchUpSeq = readResult.cursor !== undefined
|
|
338
|
+
? EventEmitterEventLog.parsePosition(readResult.cursor.position)
|
|
339
|
+
: cursorSeq;
|
|
221
340
|
|
|
341
|
+
// Use the messageCid captured by read() during its synchronous iteration.
|
|
342
|
+
// This eliminates re-lookup races: read() populates entry.messageCid before
|
|
343
|
+
// any async yield, so eviction during read()'s buildToken() cannot lose it.
|
|
222
344
|
for (const entry of readResult.events) {
|
|
223
|
-
|
|
345
|
+
const token = await this.buildToken(tenant, entry.seq, entry.messageCid ?? '');
|
|
346
|
+
listener({ type: 'event', cursor: token, event: entry.event });
|
|
224
347
|
}
|
|
225
348
|
|
|
226
349
|
// Step 3: Deliver any live events that arrived during catch-up (with seq > lastCatchUpSeq).
|
|
350
|
+
// messageCid was captured at buffer time from the EmitterPayload.
|
|
227
351
|
catchUpComplete = true;
|
|
228
352
|
for (const liveEvent of pendingLiveEvents) {
|
|
229
353
|
if (liveEvent.seq > lastCatchUpSeq) {
|
|
230
|
-
|
|
354
|
+
const token = await this.buildToken(tenant, liveEvent.seq, liveEvent.messageCid);
|
|
355
|
+
listener({ type: 'event', cursor: token, event: liveEvent.event });
|
|
231
356
|
}
|
|
232
357
|
}
|
|
233
358
|
|
|
@@ -246,7 +371,9 @@ export class EventEmitterEventLog implements EventLog {
|
|
|
246
371
|
if (filters !== undefined && filters.length > 0) {
|
|
247
372
|
if (!FilterUtility.matchAnyFilter(payload.indexes, filters)) { return; }
|
|
248
373
|
}
|
|
249
|
-
|
|
374
|
+
void tokenFromPayload(payload).then((token) => {
|
|
375
|
+
listener({ type: 'event', cursor: token, event: payload.event });
|
|
376
|
+
});
|
|
250
377
|
};
|
|
251
378
|
|
|
252
379
|
this.emitter.on(channel, handler);
|
|
@@ -257,6 +384,26 @@ export class EventEmitterEventLog implements EventLog {
|
|
|
257
384
|
};
|
|
258
385
|
}
|
|
259
386
|
|
|
387
|
+
public async getReplayBounds(tenant: string): Promise<{ oldest: ProgressToken; latest: ProgressToken } | undefined> {
|
|
388
|
+
const log = this.tenantLogs.get(tenant);
|
|
389
|
+
if (log === undefined || log.size === 0) {
|
|
390
|
+
return undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Map is ordered by insertion (ascending seq). First and last keys are min/max.
|
|
394
|
+
const keys = [...log.keys()];
|
|
395
|
+
const oldestSeq = keys[0];
|
|
396
|
+
const latestSeq = keys[keys.length - 1];
|
|
397
|
+
|
|
398
|
+
const oldestEntry = log.get(oldestSeq)!;
|
|
399
|
+
const latestEntry = log.get(latestSeq)!;
|
|
400
|
+
|
|
401
|
+
const oldest = await this.buildToken(tenant, oldestSeq, oldestEntry.messageCid);
|
|
402
|
+
const latest = await this.buildToken(tenant, latestSeq, latestEntry.messageCid);
|
|
403
|
+
|
|
404
|
+
return { oldest, latest };
|
|
405
|
+
}
|
|
406
|
+
|
|
260
407
|
public async trim(tenant: string, olderThan: number | string): Promise<void> {
|
|
261
408
|
const log = this.tenantLogs.get(tenant);
|
|
262
409
|
if (log === undefined) { return; }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MessageStore } from '../types/message-store.js';
|
|
2
|
-
import type { SubscriptionListener } from '../types/subscriptions.js';
|
|
3
2
|
import type { HandlerDependencies, MethodHandler } from '../types/method-handler.js';
|
|
4
3
|
import type { MessagesSubscribeMessage, MessagesSubscribeReply } from '../types/messages-types.js';
|
|
4
|
+
import type { ProgressGapInfo, SubscriptionListener } from '../types/subscriptions.js';
|
|
5
5
|
|
|
6
6
|
import { authenticate } from '../core/auth.js';
|
|
7
7
|
import { Message } from '../core/message.js';
|
|
@@ -61,6 +61,13 @@ export class MessagesSubscribeHandler implements MethodHandler {
|
|
|
61
61
|
subscription,
|
|
62
62
|
};
|
|
63
63
|
} catch (error) {
|
|
64
|
+
if (error instanceof DwnError && error.code === DwnErrorCode.EventLogProgressGap) {
|
|
65
|
+
const gapInfo = (error as any).gapInfo as ProgressGapInfo | undefined;
|
|
66
|
+
return {
|
|
67
|
+
status : { code: 410, detail: 'Progress token gap' },
|
|
68
|
+
error : gapInfo !== undefined ? { code: 'ProgressGap' as const, ...gapInfo } : undefined,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
64
71
|
return messageReplyFromError(error, 500);
|
|
65
72
|
}
|
|
66
73
|
}
|
|
@@ -83,7 +83,7 @@ export class ProtocolsConfigureHandler implements MethodHandler {
|
|
|
83
83
|
|
|
84
84
|
// only emit if the event log is set
|
|
85
85
|
if (this.deps.eventLog !== undefined) {
|
|
86
|
-
await this.deps.eventLog.emit(tenant, { message }, indexes);
|
|
86
|
+
await this.deps.eventLog.emit(tenant, { message }, indexes, messageCid);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
messageReply = {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { CoreProtocolRegistry } from '../core/core-protocol.js';
|
|
2
2
|
import type { MessageSort } from '../types/message-types.js';
|
|
3
3
|
import type { MessageStore } from '../types//message-store.js';
|
|
4
|
-
import type { SubscriptionListener } from '../types/subscriptions.js';
|
|
5
4
|
import type { Filter, PaginationCursor } from '../types/query-types.js';
|
|
6
5
|
import type { HandlerDependencies, MethodHandler } from '../types/method-handler.js';
|
|
6
|
+
import type { ProgressGapInfo, SubscriptionListener } from '../types/subscriptions.js';
|
|
7
7
|
import type { RecordsQueryReplyEntry, RecordsSubscribeMessage, RecordsSubscribeReply } from '../types/records-types.js';
|
|
8
8
|
|
|
9
9
|
import { authenticate } from '../core/auth.js';
|
|
@@ -93,6 +93,13 @@ export class RecordsSubscribeHandler implements MethodHandler {
|
|
|
93
93
|
subscription,
|
|
94
94
|
};
|
|
95
95
|
} catch (error) {
|
|
96
|
+
if (error instanceof DwnError && error.code === DwnErrorCode.EventLogProgressGap) {
|
|
97
|
+
const gapInfo = (error as any).gapInfo as ProgressGapInfo | undefined;
|
|
98
|
+
return {
|
|
99
|
+
status : { code: 410, detail: 'Progress token gap' },
|
|
100
|
+
error : gapInfo !== undefined ? { code: 'ProgressGap' as const, ...gapInfo } : undefined,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
96
103
|
return messageReplyFromError(error, 500);
|
|
97
104
|
}
|
|
98
105
|
}
|
|
@@ -165,7 +165,8 @@ export class RecordsWriteHandler implements MethodHandler {
|
|
|
165
165
|
|
|
166
166
|
const indexes = await recordsWrite.constructIndexes(isLatestBaseState);
|
|
167
167
|
await this.deps.messageStore.put(tenant, messageWithOptionalEncodedData, indexes);
|
|
168
|
-
|
|
168
|
+
const messageCid = await Message.getCid(message);
|
|
169
|
+
await this.deps.stateIndex!.insert(tenant, messageCid, indexes);
|
|
169
170
|
|
|
170
171
|
// NOTE: We only emit a `RecordsWrite` when the message is the latest base state.
|
|
171
172
|
// Because we allow a `RecordsWrite` which is not the latest state to be written, but not queried, we shouldn't emit it either.
|
|
@@ -176,7 +177,7 @@ export class RecordsWriteHandler implements MethodHandler {
|
|
|
176
177
|
// records (<= 30 KB). This allows live sync to store the record
|
|
177
178
|
// immediately without a separate MessagesRead round-trip.
|
|
178
179
|
if (this.deps.eventLog !== undefined && isLatestBaseState) {
|
|
179
|
-
await this.deps.eventLog.emit(tenant, { message: messageWithOptionalEncodedData, initialWrite }, indexes);
|
|
180
|
+
await this.deps.eventLog.emit(tenant, { message: messageWithOptionalEncodedData, initialWrite }, indexes, messageCid);
|
|
180
181
|
}
|
|
181
182
|
} catch (error) {
|
|
182
183
|
if (error instanceof DwnError) {
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// export everything that we want to be consumable
|
|
2
2
|
export type { DwnConfig } from './dwn.js';
|
|
3
|
-
export type { EventListener, EventLog, EventLogEntry, EventLogReadOptions, EventLogReadResult, EventLogSubscribeOptions, EventSubscription, MessageEvent, SubscriptionEose, SubscriptionEvent, SubscriptionListener, SubscriptionMessage, SubscriptionReply } from './types/subscriptions.js';
|
|
3
|
+
export type { EventListener, EventLog, EventLogEntry, EventLogReadOptions, EventLogReadResult, EventLogSubscribeOptions, EventSubscription, MessageEvent, ProgressGapInfo, ProgressGapReason, ProgressToken, SubscriptionEose, SubscriptionEvent, SubscriptionListener, SubscriptionMessage, SubscriptionReply } from './types/subscriptions.js';
|
|
4
4
|
export type { AuthorizationModel, Descriptor, DelegatedGrantRecordsWriteMessage, GenericMessage, GenericMessageReply, GenericSignaturePayload, MessageSort, MessageSubscription, Pagination, QueryResultEntry, Status } from './types/message-types.js';
|
|
5
5
|
export type { MessagesFilter, MessagesReadMessage as MessagesReadMessage, MessagesReadReply as MessagesReadReply, MessagesReadReplyEntry as MessagesReadReplyEntry, MessagesReadDescriptor, MessagesSubscribeDescriptor, MessagesSubscribeMessage, MessagesSubscribeReply, MessagesSubscribeMessageOptions, MessagesSyncAction, MessagesSyncDescriptor, MessagesSyncDiffEntry, MessagesSyncMessage, MessagesSyncReply } from './types/messages-types.js';
|
|
6
6
|
export type { GT, LT, Filter, FilterValue, KeyValues, EqualFilter, OneOfFilter, RangeFilter, RangeCriterion, PaginationCursor, QueryOptions, RangeValue, StartsWithFilter } from './types/query-types.js';
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { MessagesFilter } from '../types/messages-types.js';
|
|
2
2
|
import type { MessageSigner } from '../types/signer.js';
|
|
3
|
+
import type { ProgressToken } from '../types/subscriptions.js';
|
|
3
4
|
import type { MessagesSubscribeDescriptor, MessagesSubscribeMessage } from '../types/messages-types.js';
|
|
4
5
|
|
|
5
6
|
import { AbstractMessage } from '../core/abstract-message.js';
|
|
@@ -16,10 +17,10 @@ export type MessagesSubscribeOptions = {
|
|
|
16
17
|
filters?: MessagesFilter[];
|
|
17
18
|
permissionGrantId?: string;
|
|
18
19
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
20
|
+
* Progress token to resume from. When provided, catch-up events are replayed
|
|
21
|
+
* from the EventLog and an EOSE marker is delivered before live events.
|
|
21
22
|
*/
|
|
22
|
-
cursor?:
|
|
23
|
+
cursor?: ProgressToken;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
export class MessagesSubscribe extends AbstractMessage<MessagesSubscribeMessage> {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { MessageSigner } from '../types/signer.js';
|
|
2
2
|
import type { MessageStore } from '../types/message-store.js';
|
|
3
3
|
import type { Pagination } from '../types/message-types.js';
|
|
4
|
+
import type { ProgressToken } from '../types/subscriptions.js';
|
|
4
5
|
import type { DataEncodedRecordsWriteMessage, DateSort, RecordsFilter, RecordsSubscribeDescriptor, RecordsSubscribeMessage } from '../types/records-types.js';
|
|
5
6
|
|
|
6
7
|
import { AbstractMessage } from '../core/abstract-message.js';
|
|
@@ -23,10 +24,10 @@ export type RecordsSubscribeOptions = {
|
|
|
23
24
|
protocolRole?: string;
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
27
|
+
* Progress token to resume from. When provided, catch-up events are replayed
|
|
28
|
+
* from the EventLog and an EOSE marker is delivered before live events.
|
|
28
29
|
*/
|
|
29
|
-
cursor?:
|
|
30
|
+
cursor?: ProgressToken;
|
|
30
31
|
|
|
31
32
|
/**
|
|
32
33
|
* The delegated grant to sign on behalf of the logical author, which is the grantor (`grantedBy`) of the delegated grant.
|
|
@@ -75,7 +75,7 @@ export class StorageController {
|
|
|
75
75
|
|
|
76
76
|
// only emit if the event log is set
|
|
77
77
|
if (this.eventLog !== undefined) {
|
|
78
|
-
await this.eventLog.emit(tenant, { message, initialWrite }, indexes);
|
|
78
|
+
await this.eventLog.emit(tenant, { message, initialWrite }, indexes, messageCid);
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
if (message.descriptor.prune) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { RangeCriterion } from './query-types.js';
|
|
2
|
-
import type { SubscriptionListener } from './subscriptions.js';
|
|
3
2
|
import type { AuthorizationModel, GenericMessage, GenericMessageReply, MessageSubscription } from './message-types.js';
|
|
4
3
|
import type { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
|
|
4
|
+
import type { ProgressGapInfo, ProgressToken, SubscriptionListener } from './subscriptions.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* filters used when filtering for any type of Message across interfaces
|
|
@@ -10,6 +10,12 @@ export type MessagesFilter = {
|
|
|
10
10
|
interface?: string;
|
|
11
11
|
method?: string;
|
|
12
12
|
protocol?: string;
|
|
13
|
+
/** Prefix filter for protocolPath. Matches records whose protocolPath equals
|
|
14
|
+
* the prefix or starts with the prefix followed by '/'. */
|
|
15
|
+
protocolPathPrefix?: string;
|
|
16
|
+
/** Prefix filter for contextId. Matches records whose contextId equals
|
|
17
|
+
* the prefix or starts with the prefix followed by '/'. */
|
|
18
|
+
contextIdPrefix?: string;
|
|
13
19
|
messageTimestamp?: RangeCriterion;
|
|
14
20
|
};
|
|
15
21
|
|
|
@@ -100,6 +106,8 @@ export type MessagesSubscribeMessage = {
|
|
|
100
106
|
|
|
101
107
|
export type MessagesSubscribeReply = GenericMessageReply & {
|
|
102
108
|
subscription?: MessageSubscription;
|
|
109
|
+
/** Present when status.code is 410 — structured gap metadata. */
|
|
110
|
+
error?: { code: 'ProgressGap' } & ProgressGapInfo;
|
|
103
111
|
};
|
|
104
112
|
|
|
105
113
|
export type MessagesSubscribeDescriptor = {
|
|
@@ -109,9 +117,9 @@ export type MessagesSubscribeDescriptor = {
|
|
|
109
117
|
filters: MessagesFilter[];
|
|
110
118
|
permissionGrantId?: string;
|
|
111
119
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
120
|
+
* Progress token to resume from. When provided, the handler replays events
|
|
121
|
+
* from the EventLog starting after this position instead of returning no
|
|
114
122
|
* initial snapshot. An EOSE marker is sent after catch-up.
|
|
115
123
|
*/
|
|
116
|
-
cursor?:
|
|
124
|
+
cursor?: ProgressToken;
|
|
117
125
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { GeneralJws } from './jws-types.js';
|
|
2
2
|
import type { JweEncryption } from '../utils/encryption.js';
|
|
3
|
-
import type { SubscriptionListener } from './subscriptions.js';
|
|
4
3
|
import type { AuthorizationModel, GenericMessage, GenericMessageReply, GenericSignaturePayload, MessageSubscription, Pagination } from './message-types.js';
|
|
5
4
|
import type { DwnInterfaceName, DwnMethodName } from '../enums/dwn-interface-method.js';
|
|
6
5
|
import type { PaginationCursor, RangeCriterion, RangeFilter, StartsWithFilter } from './query-types.js';
|
|
6
|
+
import type { ProgressGapInfo, ProgressToken, SubscriptionListener } from './subscriptions.js';
|
|
7
7
|
|
|
8
8
|
export enum DateSort {
|
|
9
9
|
CreatedAscending = 'createdAscending',
|
|
@@ -132,11 +132,11 @@ export type RecordsSubscribeDescriptor = {
|
|
|
132
132
|
dateSort?: DateSort;
|
|
133
133
|
pagination?: Pagination;
|
|
134
134
|
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
135
|
+
* Progress token to resume from. When provided, the handler replays events
|
|
136
|
+
* from the EventLog starting after this position instead of querying the
|
|
137
137
|
* MessageStore for an initial snapshot. An EOSE marker is sent after catch-up.
|
|
138
138
|
*/
|
|
139
|
-
cursor?:
|
|
139
|
+
cursor?: ProgressToken;
|
|
140
140
|
};
|
|
141
141
|
|
|
142
142
|
export type RecordsFilter = {
|
|
@@ -203,6 +203,8 @@ export type RecordsSubscribeReply = GenericMessageReply & {
|
|
|
203
203
|
subscription?: MessageSubscription;
|
|
204
204
|
entries?: RecordsQueryReplyEntry[];
|
|
205
205
|
cursor?: PaginationCursor;
|
|
206
|
+
/** Present when status.code is 410 — structured gap metadata. */
|
|
207
|
+
error?: { code: 'ProgressGap' } & ProgressGapInfo;
|
|
206
208
|
};
|
|
207
209
|
|
|
208
210
|
export type RecordsReadMessage = {
|