@enbox/dwn-sdk-js 0.2.2 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/dist/browser.mjs +6 -6
  2. package/dist/browser.mjs.map +3 -3
  3. package/dist/esm/generated/precompiled-validators.js +704 -342
  4. package/dist/esm/generated/precompiled-validators.js.map +1 -1
  5. package/dist/esm/src/core/dwn-error.js +1 -0
  6. package/dist/esm/src/core/dwn-error.js.map +1 -1
  7. package/dist/esm/src/event-stream/event-emitter-event-log.js +151 -20
  8. package/dist/esm/src/event-stream/event-emitter-event-log.js.map +1 -1
  9. package/dist/esm/src/handlers/messages-subscribe.js +7 -0
  10. package/dist/esm/src/handlers/messages-subscribe.js.map +1 -1
  11. package/dist/esm/src/handlers/messages-sync.js +6 -3
  12. package/dist/esm/src/handlers/messages-sync.js.map +1 -1
  13. package/dist/esm/src/handlers/protocols-configure.js +1 -1
  14. package/dist/esm/src/handlers/protocols-configure.js.map +1 -1
  15. package/dist/esm/src/handlers/records-subscribe.js +7 -0
  16. package/dist/esm/src/handlers/records-subscribe.js.map +1 -1
  17. package/dist/esm/src/handlers/records-write.js +33 -5
  18. package/dist/esm/src/handlers/records-write.js.map +1 -1
  19. package/dist/esm/src/interfaces/messages-subscribe.js.map +1 -1
  20. package/dist/esm/src/interfaces/records-subscribe.js.map +1 -1
  21. package/dist/esm/src/store/storage-controller.js +1 -1
  22. package/dist/esm/src/store/storage-controller.js.map +1 -1
  23. package/dist/esm/src/utils/messages.js +41 -1
  24. package/dist/esm/src/utils/messages.js.map +1 -1
  25. package/dist/esm/tests/event-emitter-event-log.spec.js +214 -81
  26. package/dist/esm/tests/event-emitter-event-log.spec.js.map +1 -1
  27. package/dist/esm/tests/handlers/messages-subscribe.spec.js +288 -0
  28. package/dist/esm/tests/handlers/messages-subscribe.spec.js.map +1 -1
  29. package/dist/esm/tests/utils/test-data-generator.js.map +1 -1
  30. package/dist/types/generated/precompiled-validators.d.ts +50 -34
  31. package/dist/types/generated/precompiled-validators.d.ts.map +1 -1
  32. package/dist/types/src/core/dwn-error.d.ts +1 -0
  33. package/dist/types/src/core/dwn-error.d.ts.map +1 -1
  34. package/dist/types/src/event-stream/event-emitter-event-log.d.ts +34 -4
  35. package/dist/types/src/event-stream/event-emitter-event-log.d.ts.map +1 -1
  36. package/dist/types/src/handlers/messages-subscribe.d.ts +1 -1
  37. package/dist/types/src/handlers/messages-subscribe.d.ts.map +1 -1
  38. package/dist/types/src/handlers/messages-sync.d.ts.map +1 -1
  39. package/dist/types/src/handlers/records-subscribe.d.ts +1 -1
  40. package/dist/types/src/handlers/records-subscribe.d.ts.map +1 -1
  41. package/dist/types/src/handlers/records-write.d.ts.map +1 -1
  42. package/dist/types/src/index.d.ts +1 -1
  43. package/dist/types/src/index.d.ts.map +1 -1
  44. package/dist/types/src/interfaces/messages-subscribe.d.ts +4 -3
  45. package/dist/types/src/interfaces/messages-subscribe.d.ts.map +1 -1
  46. package/dist/types/src/interfaces/records-subscribe.d.ts +4 -3
  47. package/dist/types/src/interfaces/records-subscribe.d.ts.map +1 -1
  48. package/dist/types/src/types/messages-types.d.ts +14 -4
  49. package/dist/types/src/types/messages-types.d.ts.map +1 -1
  50. package/dist/types/src/types/records-types.d.ts +8 -4
  51. package/dist/types/src/types/records-types.d.ts.map +1 -1
  52. package/dist/types/src/types/subscriptions.d.ts +73 -20
  53. package/dist/types/src/types/subscriptions.d.ts.map +1 -1
  54. package/dist/types/src/utils/messages.d.ts.map +1 -1
  55. package/dist/types/tests/handlers/messages-subscribe.spec.d.ts.map +1 -1
  56. package/dist/types/tests/utils/test-data-generator.d.ts +5 -4
  57. package/dist/types/tests/utils/test-data-generator.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/src/core/dwn-error.ts +1 -0
  60. package/src/event-stream/event-emitter-event-log.ts +169 -21
  61. package/src/handlers/messages-subscribe.ts +8 -1
  62. package/src/handlers/messages-sync.ts +6 -3
  63. package/src/handlers/protocols-configure.ts +1 -1
  64. package/src/handlers/records-subscribe.ts +8 -1
  65. package/src/handlers/records-write.ts +34 -5
  66. package/src/index.ts +1 -1
  67. package/src/interfaces/messages-subscribe.ts +4 -3
  68. package/src/interfaces/records-subscribe.ts +4 -3
  69. package/src/store/storage-controller.ts +1 -1
  70. package/src/types/messages-types.ts +12 -4
  71. package/src/types/records-types.ts +6 -4
  72. package/src/types/subscriptions.ts +78 -20
  73. package/src/utils/messages.ts +47 -1
@@ -4,6 +4,7 @@ import type { HandlerDependencies, MethodHandler } from '../types/method-handler
4
4
  import type { MessagesSyncDiffEntry, MessagesSyncMessage, MessagesSyncReply } from '../types/messages-types.js';
5
5
 
6
6
  import { authenticate } from '../core/auth.js';
7
+ import { DwnConstant } from '../core/dwn-constant.js';
7
8
  import { Encoder } from '../utils/encoder.js';
8
9
  import { hashToHex } from '../smt/smt-utils.js';
9
10
  import { messageReplyFromError } from '../core/message-reply.js';
@@ -13,11 +14,13 @@ import { PermissionsProtocol } from '../protocols/permissions.js';
13
14
  import { DwnError, DwnErrorCode } from '../core/dwn-error.js';
14
15
 
15
16
  /**
16
- * Default maximum inline data size for diff responses (256 KB).
17
+ * Maximum inline data size for diff responses aligned with the
18
+ * {@link DwnConstant.maxDataSizeAllowedToBeEncoded} threshold (30 KB).
17
19
  * RecordsWrite data payloads smaller than this are base64url-encoded and
18
- * included directly in the diff reply, avoiding a separate MessagesRead round-trip.
20
+ * included directly in the diff reply. Larger payloads must be fetched
21
+ * separately via MessagesRead.
19
22
  */
20
- const DEFAULT_MAX_INLINE_DATA_SIZE = 262_144;
23
+ const DEFAULT_MAX_INLINE_DATA_SIZE = DwnConstant.maxDataSizeAllowedToBeEncoded;
21
24
 
22
25
 
23
26
  export class MessagesSyncHandler implements MethodHandler {
@@ -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
  }
@@ -91,9 +91,32 @@ export class RecordsWriteHandler implements MethodHandler {
91
91
  }
92
92
 
93
93
  if (!incomingMessageIsNewest) {
94
- return {
95
- status: { code: 409, detail: 'Conflict' }
96
- };
94
+ // Allow re-processing when the existing record was stored as an
95
+ // initial write without data (isLatestBaseState = false, status 204)
96
+ // and the incoming message now supplies data. This happens during
97
+ // sync when a live pull initially stores the message without data
98
+ // and a subsequent poll or retry delivers the same message with data.
99
+ //
100
+ // We detect the incomplete state by checking whether the existing
101
+ // message is an initial write that lacks both inline encodedData and
102
+ // DataStore data — indicating it was stored without data.
103
+ let existingLacksData = false;
104
+ if (newestExistingMessage !== undefined && dataStream !== undefined) {
105
+ const isInitial = await RecordsWrite.isInitialWrite(newestExistingMessage);
106
+ if (isInitial) {
107
+ const hasInlineData = !!(newestExistingMessage as any).encodedData;
108
+ const hasStoredData = this.deps.dataStore
109
+ ? !!(await this.deps.dataStore.get(tenant, recordsWrite.message.recordId, message.descriptor.dataCid!))
110
+ : false;
111
+ existingLacksData = !hasInlineData && !hasStoredData;
112
+ }
113
+ }
114
+
115
+ if (!existingLacksData) {
116
+ return {
117
+ status: { code: 409, detail: 'Conflict' }
118
+ };
119
+ }
97
120
  }
98
121
 
99
122
  // Look up the core protocol (if any) for the incoming message so that lifecycle hooks
@@ -142,13 +165,19 @@ export class RecordsWriteHandler implements MethodHandler {
142
165
 
143
166
  const indexes = await recordsWrite.constructIndexes(isLatestBaseState);
144
167
  await this.deps.messageStore.put(tenant, messageWithOptionalEncodedData, indexes);
145
- await this.deps.stateIndex!.insert(tenant, await Message.getCid(message), indexes);
168
+ const messageCid = await Message.getCid(message);
169
+ await this.deps.stateIndex!.insert(tenant, messageCid, indexes);
146
170
 
147
171
  // NOTE: We only emit a `RecordsWrite` when the message is the latest base state.
148
172
  // Because we allow a `RecordsWrite` which is not the latest state to be written, but not queried, we shouldn't emit it either.
149
173
  // It will be emitted as a part of a subsequent next write, if it is the latest base state.
174
+ //
175
+ // We emit `messageWithOptionalEncodedData` (not the raw `message`) so
176
+ // that WebSocket subscribers receive inline `encodedData` for small
177
+ // records (<= 30 KB). This allows live sync to store the record
178
+ // immediately without a separate MessagesRead round-trip.
150
179
  if (this.deps.eventLog !== undefined && isLatestBaseState) {
151
- await this.deps.eventLog.emit(tenant, { message, initialWrite }, indexes);
180
+ await this.deps.eventLog.emit(tenant, { message: messageWithOptionalEncodedData, initialWrite }, indexes, messageCid);
152
181
  }
153
182
  } catch (error) {
154
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
- * Opaque EventLog cursor string to resume from. When provided, catch-up events are
20
- * replayed from the EventLog and an EOSE marker is delivered before live events.
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?: string;
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
- * Opaque EventLog cursor string to resume from. When provided, catch-up events are
27
- * replayed from the EventLog and an EOSE marker is delivered before live events.
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?: string;
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
- * Opaque EventLog cursor string to resume from. When provided, the handler replays
113
- * events from the EventLog starting after this cursor instead of returning no
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?: string;
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
- * Opaque EventLog cursor string to resume from. When provided, the handler replays
136
- * events from the EventLog starting after this cursor instead of querying the
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?: string;
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 = {
@@ -2,6 +2,54 @@ import type { RecordsWriteMessage } from './records-types.js';
2
2
  import type { Filter, KeyValues } from './query-types.js';
3
3
  import type { GenericMessage, GenericMessageReply, MessageSubscription } from './message-types.js';
4
4
 
5
+ // ---------------------------------------------------------------------------
6
+ // ProgressToken — structured cursor for EventLog replay/resume
7
+ // ---------------------------------------------------------------------------
8
+
9
+ /**
10
+ * Structured cursor for EventLog replay and resume. Replaces the previous
11
+ * opaque `string` cursor to provide explicit ordering semantics required
12
+ * by causal frontier progression in multi-master sync.
13
+ *
14
+ * Comparisons are valid only when `streamId` and `epoch` are equal.
15
+ * `position` is compared numerically (BigInt-safe), never lexicographically.
16
+ * Progress tokens are source-local: they MUST NOT be reused across different
17
+ * remote providers or EventLog instances.
18
+ */
19
+ export type ProgressToken = {
20
+ /** Stable identity of the source event stream domain. */
21
+ streamId : string;
22
+ /** Stream generation/version; changes on non-compatible reset. */
23
+ epoch : string;
24
+ /** Monotonic decimal string within `(streamId, epoch)`. Compared numerically. */
25
+ position : string;
26
+ /** The CID of the message associated with this event. */
27
+ messageCid : string;
28
+ };
29
+
30
+ /**
31
+ * Reason code for a {@link ProgressGapInfo} — explains why the cursor
32
+ * cannot be resumed.
33
+ */
34
+ export type ProgressGapReason = 'token_too_old' | 'epoch_mismatch' | 'stream_mismatch';
35
+
36
+ /**
37
+ * Metadata attached to a `DwnError(DwnErrorCode.EventLogProgressGap, ...)`
38
+ * when an EventLog implementation cannot resume from a given cursor.
39
+ *
40
+ * Subscribe handlers translate this into a 410 response.
41
+ */
42
+ export type ProgressGapInfo = {
43
+ /** The cursor the consumer requested. */
44
+ requested : ProgressToken;
45
+ /** The oldest token still available for replay. */
46
+ oldestAvailable : ProgressToken;
47
+ /** The latest token available. */
48
+ latestAvailable : ProgressToken;
49
+ /** Why the cursor is no longer valid. */
50
+ reason : ProgressGapReason;
51
+ };
52
+
5
53
  /**
6
54
  * Internal listener type used by {@link EventLog.emit} to notify in-process
7
55
  * subscribers. Not intended for direct consumer use — consumers should use
@@ -32,17 +80,16 @@ export type SubscriptionReply = GenericMessageReply & {
32
80
  // ---------------------------------------------------------------------------
33
81
 
34
82
  /**
35
- * A regular subscription event carrying a message and its EventLog cursor.
83
+ * A regular subscription event carrying a message and its EventLog progress token.
36
84
  */
37
85
  export type SubscriptionEvent = {
38
86
  type : 'event';
39
87
  /**
40
- * Opaque cursor string assigned by the EventLog implementation. Clients should
41
- * persist this value and pass it back to `subscribe()` or `read()` to resume
42
- * from this point. The format is implementation-defined (e.g. numeric sequence
43
- * for in-memory, Redis stream ID, NATS stream sequence, etc.).
88
+ * Structured progress token assigned by the EventLog implementation. Clients
89
+ * should persist this value and pass it back to `subscribe()` or `read()` to
90
+ * resume from this point.
44
91
  */
45
- cursor : string;
92
+ cursor : ProgressToken;
46
93
  /** The event payload (message + optional initialWrite). */
47
94
  event : MessageEvent;
48
95
  };
@@ -57,10 +104,10 @@ export type SubscriptionEvent = {
57
104
  export type SubscriptionEose = {
58
105
  type : 'eose';
59
106
  /**
60
- * Opaque cursor string of the last stored event that was replayed.
107
+ * Progress token of the last stored event that was replayed.
61
108
  * Echoes the input cursor when no stored events matched (i.e. already caught up).
62
109
  */
63
- cursor : string;
110
+ cursor : ProgressToken;
64
111
  };
65
112
 
66
113
  /**
@@ -80,15 +127,15 @@ export type SubscriptionListener = (message: SubscriptionMessage) => void;
80
127
  */
81
128
  export type EventLogSubscribeOptions = {
82
129
  /**
83
- * Opaque cursor string to resume from (exclusive — events after this cursor
130
+ * Progress token to resume from (exclusive — events after this position
84
131
  * are replayed). When provided, stored events are replayed first, followed by
85
132
  * an EOSE marker, then live events. When omitted, only live events are delivered.
86
133
  *
87
- * Cursor values are implementation-defined and must be obtained from a prior
88
- * interaction with the same EventLog instance (e.g. `SubscriptionEvent.cursor`,
89
- * `EventLogReadResult.cursor`, or the return value of `emit()`).
134
+ * Tokens must be obtained from a prior interaction with the same EventLog
135
+ * instance (e.g. `SubscriptionEvent.cursor`, `EventLogReadResult.cursor`,
136
+ * or the return value of `emit()`).
90
137
  */
91
- cursor? : string;
138
+ cursor? : ProgressToken;
92
139
 
93
140
  /**
94
141
  * Filters evaluated against event indexes. Events must match at least one
@@ -119,8 +166,8 @@ export type EventLogEntry = {
119
166
  * Options accepted by {@link EventLog.read}.
120
167
  */
121
168
  export type EventLogReadOptions = {
122
- /** Opaque cursor string to resume from (exclusive — returns events after this cursor). */
123
- cursor? : string;
169
+ /** Progress token to resume from (exclusive — returns events after this position). */
170
+ cursor? : ProgressToken;
124
171
 
125
172
  /** Maximum number of events to return. */
126
173
  limit? : number;
@@ -137,14 +184,14 @@ export type EventLogReadResult = {
137
184
  events : EventLogEntry[];
138
185
 
139
186
  /**
140
- * Opaque cursor string for resuming subsequent reads or subscriptions.
187
+ * Progress token for resuming subsequent reads or subscriptions.
141
188
  *
142
- * - When events are returned: cursor of the last event.
189
+ * - When events are returned: token of the last event.
143
190
  * - When no events are returned but a cursor was provided: the input cursor
144
191
  * (meaning "you are caught up, nothing new since this point").
145
192
  * - When no events exist and no cursor was provided: `undefined`.
146
193
  */
147
- cursor? : string;
194
+ cursor? : ProgressToken;
148
195
  };
149
196
 
150
197
  /**
@@ -162,9 +209,13 @@ export type EventLogReadResult = {
162
209
  export interface EventLog {
163
210
  /**
164
211
  * Persist an event and notify in-process subscribers.
165
- * @returns The opaque cursor string assigned to the event, or empty string on failure.
212
+ * @param tenant The tenant DID.
213
+ * @param event The event payload.
214
+ * @param indexes Index values for the event.
215
+ * @param messageCid The CID of the message being emitted — embedded in the returned token.
216
+ * @returns A {@link ProgressToken} assigned to the event, or `undefined` on failure.
166
217
  */
167
- emit(tenant: string, event: MessageEvent, indexes: KeyValues): Promise<string>;
218
+ emit(tenant: string, event: MessageEvent, indexes: KeyValues, messageCid: string): Promise<ProgressToken | undefined>;
168
219
 
169
220
  /**
170
221
  * Read events from the log starting after `cursor`, optionally filtered.
@@ -188,6 +239,13 @@ export interface EventLog {
188
239
  */
189
240
  subscribe(tenant: string, id: string, listener: SubscriptionListener, options?: EventLogSubscribeOptions): Promise<EventSubscription>;
190
241
 
242
+ /**
243
+ * Returns the oldest and latest available progress tokens for a tenant,
244
+ * or `undefined` if the tenant has no events. Used to construct
245
+ * `ProgressGap` metadata when a consumer's cursor is no longer replayable.
246
+ */
247
+ getReplayBounds(tenant: string): Promise<{ oldest: ProgressToken; latest: ProgressToken } | undefined>;
248
+
191
249
  /**
192
250
  * Delete events older than the given sequence number or ISO-8601 timestamp.
193
251
  */
@@ -67,6 +67,29 @@ export class Messages {
67
67
  }
68
68
 
69
69
  messagesQueryFilters.push(this.convertFilter(filter));
70
+
71
+ // When protocolPathPrefix is used with a protocol, inject a shadow filter
72
+ // for ProtocolsConfigure events. Without this, protocol metadata updates
73
+ // would be excluded (ProtocolsConfigure indexes have no protocolPath).
74
+ // This mirrors the existing core-protocol additional-filter pattern above.
75
+ // The messageTimestamp constraint is carried over so time-bounded queries
76
+ // (including cursor-based subscriptions) also apply to the shadow filter.
77
+ if ((filter.protocolPathPrefix !== undefined || filter.contextIdPrefix !== undefined) && filter.protocol !== undefined) {
78
+ const metadataFilter: Filter = {
79
+ interface : 'Protocols',
80
+ method : 'Configure',
81
+ protocol : filter.protocol,
82
+ };
83
+
84
+ if (filter.messageTimestamp !== undefined) {
85
+ const timestampFilter = FilterUtility.convertRangeCriterion(filter.messageTimestamp);
86
+ if (timestampFilter) {
87
+ metadataFilter.messageTimestamp = timestampFilter;
88
+ }
89
+ }
90
+
91
+ messagesQueryFilters.push(metadataFilter);
92
+ }
70
93
  }
71
94
 
72
95
  return messagesQueryFilters;
@@ -78,12 +101,35 @@ export class Messages {
78
101
  private static convertFilter(filter: MessagesFilter): Filter {
79
102
  const filterCopy = { ...filter } as Filter;
80
103
 
81
- const { messageTimestamp } = filter;
104
+ const { messageTimestamp, protocolPathPrefix, contextIdPrefix } = filter;
82
105
  const messageTimestampFilter = messageTimestamp ? FilterUtility.convertRangeCriterion(messageTimestamp) : undefined;
83
106
  if (messageTimestampFilter) {
84
107
  filterCopy.messageTimestamp = messageTimestampFilter;
85
108
  delete filterCopy.dateUpdated;
86
109
  }
110
+
111
+ // Convert protocolPathPrefix into a protocolPath range filter.
112
+ // The range gte: prefix, lt: prefix + '/\uffff' matches:
113
+ // - exact: 'post' (prefix itself)
114
+ // - children: 'post/attachment', 'post/comment', etc.
115
+ // - NOT siblings: 'poster', 'postfix' (excluded because '/' < any alphanumeric)
116
+ if (protocolPathPrefix !== undefined) {
117
+ delete (filterCopy as any).protocolPathPrefix;
118
+ filterCopy.protocolPath = {
119
+ gte : protocolPathPrefix,
120
+ lt : protocolPathPrefix + '/\uffff',
121
+ };
122
+ }
123
+
124
+ // Convert contextIdPrefix into a contextId range filter (same pattern).
125
+ if (contextIdPrefix !== undefined) {
126
+ delete (filterCopy as any).contextIdPrefix;
127
+ filterCopy.contextId = {
128
+ gte : contextIdPrefix,
129
+ lt : contextIdPrefix + '/\uffff',
130
+ };
131
+ }
132
+
87
133
  return filterCopy as Filter;
88
134
  }
89
135
  }