@enbox/api 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +140 -159
- package/dist/browser.mjs +23 -13
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/advanced.js +11 -0
- package/dist/esm/advanced.js.map +1 -0
- package/dist/esm/define-protocol.js +3 -3
- package/dist/esm/dwn-api.js +84 -130
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/dwn-reader-api.js +128 -0
- package/dist/esm/dwn-reader-api.js.map +1 -0
- package/dist/esm/index.js +4 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/live-query.js +154 -0
- package/dist/esm/live-query.js.map +1 -0
- package/dist/esm/read-only-record.js +255 -0
- package/dist/esm/read-only-record.js.map +1 -0
- package/dist/esm/record.js +46 -57
- package/dist/esm/record.js.map +1 -1
- package/dist/esm/typed-web5.js +242 -0
- package/dist/esm/typed-web5.js.map +1 -0
- package/dist/esm/web5.js +71 -3
- package/dist/esm/web5.js.map +1 -1
- package/dist/types/advanced.d.ts +12 -0
- package/dist/types/advanced.d.ts.map +1 -0
- package/dist/types/define-protocol.d.ts +3 -3
- package/dist/types/dwn-api.d.ts +20 -104
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/dwn-reader-api.d.ts +138 -0
- package/dist/types/dwn-reader-api.d.ts.map +1 -0
- package/dist/types/index.d.ts +4 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/live-query.d.ts +148 -0
- package/dist/types/live-query.d.ts.map +1 -0
- package/dist/types/protocol-types.d.ts +2 -2
- package/dist/types/read-only-record.d.ts +133 -0
- package/dist/types/read-only-record.d.ts.map +1 -0
- package/dist/types/record.d.ts +37 -17
- package/dist/types/record.d.ts.map +1 -1
- package/dist/types/{typed-dwn-api.d.ts → typed-web5.d.ts} +75 -76
- package/dist/types/typed-web5.d.ts.map +1 -0
- package/dist/types/web5.d.ts +79 -3
- package/dist/types/web5.d.ts.map +1 -1
- package/package.json +10 -6
- package/src/advanced.ts +29 -0
- package/src/define-protocol.ts +3 -3
- package/src/dwn-api.ts +141 -266
- package/src/dwn-reader-api.ts +255 -0
- package/src/index.ts +4 -2
- package/src/live-query.ts +261 -0
- package/src/protocol-types.ts +2 -2
- package/src/read-only-record.ts +328 -0
- package/src/record.ts +116 -86
- package/src/typed-web5.ts +445 -0
- package/src/web5.ts +104 -5
- package/dist/esm/subscription-util.js +0 -35
- package/dist/esm/subscription-util.js.map +0 -1
- package/dist/esm/typed-dwn-api.js +0 -181
- package/dist/esm/typed-dwn-api.js.map +0 -1
- package/dist/types/subscription-util.d.ts +0 -19
- package/dist/types/subscription-util.d.ts.map +0 -1
- package/dist/types/typed-dwn-api.d.ts.map +0 -1
- package/src/subscription-util.ts +0 -44
- package/src/typed-dwn-api.ts +0 -370
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NOTE: Added reference types here to avoid a `pnpm` bug during build.
|
|
3
|
+
* https://github.com/enboxorg/enbox/pull/507
|
|
4
|
+
*/
|
|
5
|
+
/// <reference types="@enbox/dwn-sdk-js" />
|
|
6
|
+
|
|
7
|
+
import type { AnonymousDwnApi, DwnPaginationCursor, DwnResponseStatus } from '@enbox/agent';
|
|
8
|
+
import type {
|
|
9
|
+
DateSort,
|
|
10
|
+
Pagination,
|
|
11
|
+
ProtocolDefinition,
|
|
12
|
+
ProtocolsQueryFilter,
|
|
13
|
+
RecordsFilter,
|
|
14
|
+
} from '@enbox/dwn-sdk-js';
|
|
15
|
+
|
|
16
|
+
import { ReadOnlyRecord } from './read-only-record.js';
|
|
17
|
+
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
// Request / Response types for records
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Request to query public records from a remote DWN.
|
|
24
|
+
*
|
|
25
|
+
* @beta
|
|
26
|
+
*/
|
|
27
|
+
export type ReaderRecordsQueryRequest = {
|
|
28
|
+
/** The DID of the remote DWN to query (required — reader is remote-only). */
|
|
29
|
+
from: string;
|
|
30
|
+
/** Filter criteria for the query. */
|
|
31
|
+
filter: RecordsFilter;
|
|
32
|
+
/** Sort order for results. */
|
|
33
|
+
dateSort?: DateSort;
|
|
34
|
+
/** Pagination options. */
|
|
35
|
+
pagination?: Pagination;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Response from a reader records query.
|
|
40
|
+
*
|
|
41
|
+
* @beta
|
|
42
|
+
*/
|
|
43
|
+
export type ReaderRecordsQueryResponse = DwnResponseStatus & {
|
|
44
|
+
/** Array of read-only records matching the query. */
|
|
45
|
+
records: ReadOnlyRecord[];
|
|
46
|
+
/** Pagination cursor for fetching the next page. */
|
|
47
|
+
cursor?: DwnPaginationCursor;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Request to read a specific public record from a remote DWN.
|
|
52
|
+
*
|
|
53
|
+
* @beta
|
|
54
|
+
*/
|
|
55
|
+
export type ReaderRecordsReadRequest = {
|
|
56
|
+
/** The DID of the remote DWN to read from (required — reader is remote-only). */
|
|
57
|
+
from: string;
|
|
58
|
+
/** Filter to identify the record (typically `{ recordId: '...' }`). */
|
|
59
|
+
filter: RecordsFilter;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Response from a reader records read.
|
|
64
|
+
*
|
|
65
|
+
* @beta
|
|
66
|
+
*/
|
|
67
|
+
export type ReaderRecordsReadResponse = DwnResponseStatus & {
|
|
68
|
+
/** The read-only record, if found. */
|
|
69
|
+
record?: ReadOnlyRecord;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Request to count public records on a remote DWN.
|
|
74
|
+
*
|
|
75
|
+
* @beta
|
|
76
|
+
*/
|
|
77
|
+
export type ReaderRecordsCountRequest = {
|
|
78
|
+
/** The DID of the remote DWN to count records in (required). */
|
|
79
|
+
from: string;
|
|
80
|
+
/** Filter criteria for counting. */
|
|
81
|
+
filter: RecordsFilter;
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Response from a reader records count.
|
|
86
|
+
*
|
|
87
|
+
* @beta
|
|
88
|
+
*/
|
|
89
|
+
export type ReaderRecordsCountResponse = DwnResponseStatus & {
|
|
90
|
+
/** The number of matching public records. */
|
|
91
|
+
count?: number;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Request / Response types for protocols
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Request to query published protocols from a remote DWN.
|
|
100
|
+
*
|
|
101
|
+
* @beta
|
|
102
|
+
*/
|
|
103
|
+
export type ReaderProtocolsQueryRequest = {
|
|
104
|
+
/** The DID of the remote DWN to query protocols from (required). */
|
|
105
|
+
from: string;
|
|
106
|
+
/** Optional filter for the protocol query. */
|
|
107
|
+
filter?: ProtocolsQueryFilter;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Response from a reader protocols query.
|
|
112
|
+
*
|
|
113
|
+
* @beta
|
|
114
|
+
*/
|
|
115
|
+
export type ReaderProtocolsQueryResponse = DwnResponseStatus & {
|
|
116
|
+
/** Array of published protocol definitions. */
|
|
117
|
+
protocols: ProtocolDefinition[];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// DwnReaderApi
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A read-only API for querying public data on remote DWNs without any identity or signing keys.
|
|
126
|
+
*
|
|
127
|
+
* This class mirrors the shape of {@link DwnApi}'s `records` and `protocols`
|
|
128
|
+
* namespaces, but restricts to read-path operations and requires a `from` DID
|
|
129
|
+
* on every call (remote-only). All messages are unsigned, so only published
|
|
130
|
+
* records and protocols are accessible.
|
|
131
|
+
*
|
|
132
|
+
* Obtain an instance via {@link Web5.anonymous | `Web5.anonymous()`}.
|
|
133
|
+
*
|
|
134
|
+
* @example
|
|
135
|
+
* ```ts
|
|
136
|
+
* const { dwn } = Web5.anonymous();
|
|
137
|
+
*
|
|
138
|
+
* const { records } = await dwn.records.query({
|
|
139
|
+
* from: 'did:dht:alice...',
|
|
140
|
+
* filter: { protocol: 'https://social.example/posts', protocolPath: 'post' },
|
|
141
|
+
* });
|
|
142
|
+
*
|
|
143
|
+
* for (const record of records) {
|
|
144
|
+
* console.log(record.id, await record.data.text());
|
|
145
|
+
* }
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* @beta
|
|
149
|
+
*/
|
|
150
|
+
export class DwnReaderApi {
|
|
151
|
+
private _anonymousDwn: AnonymousDwnApi;
|
|
152
|
+
|
|
153
|
+
constructor(anonymousDwn: AnonymousDwnApi) {
|
|
154
|
+
this._anonymousDwn = anonymousDwn;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* API to interact with public DWN records (query, read, count).
|
|
159
|
+
*/
|
|
160
|
+
get records(): {
|
|
161
|
+
query: (request: ReaderRecordsQueryRequest) => Promise<ReaderRecordsQueryResponse>;
|
|
162
|
+
read: (request: ReaderRecordsReadRequest) => Promise<ReaderRecordsReadResponse>;
|
|
163
|
+
count: (request: ReaderRecordsCountRequest) => Promise<ReaderRecordsCountResponse>;
|
|
164
|
+
} {
|
|
165
|
+
return {
|
|
166
|
+
/**
|
|
167
|
+
* Query public records from a remote DWN.
|
|
168
|
+
* Only published records are returned.
|
|
169
|
+
*/
|
|
170
|
+
query: async (request: ReaderRecordsQueryRequest): Promise<ReaderRecordsQueryResponse> => {
|
|
171
|
+
const reply = await this._anonymousDwn.recordsQuery(request.from, {
|
|
172
|
+
filter : request.filter,
|
|
173
|
+
dateSort : request.dateSort,
|
|
174
|
+
pagination : request.pagination,
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const { entries = [], status, cursor } = reply;
|
|
178
|
+
|
|
179
|
+
const records = entries.map((entry) => new ReadOnlyRecord({
|
|
180
|
+
rawMessage : entry,
|
|
181
|
+
initialWrite : entry.initialWrite,
|
|
182
|
+
encodedData : entry.encodedData,
|
|
183
|
+
remoteOrigin : request.from,
|
|
184
|
+
anonymousDwn : this._anonymousDwn,
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
return { records, status, cursor };
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Read a specific public record from a remote DWN.
|
|
192
|
+
* Succeeds for published records and protocol records with `{ who: 'anyone', can: ['read'] }`.
|
|
193
|
+
*/
|
|
194
|
+
read: async (request: ReaderRecordsReadRequest): Promise<ReaderRecordsReadResponse> => {
|
|
195
|
+
const reply = await this._anonymousDwn.recordsRead(request.from, {
|
|
196
|
+
filter: request.filter,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const { entry, status } = reply;
|
|
200
|
+
|
|
201
|
+
let record: ReadOnlyRecord | undefined;
|
|
202
|
+
if (200 <= status.code && status.code <= 299 && entry?.recordsWrite) {
|
|
203
|
+
record = new ReadOnlyRecord({
|
|
204
|
+
rawMessage : entry.recordsWrite,
|
|
205
|
+
initialWrite : entry.initialWrite,
|
|
206
|
+
data : entry.data,
|
|
207
|
+
remoteOrigin : request.from,
|
|
208
|
+
anonymousDwn : this._anonymousDwn,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { record, status };
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Count public records on a remote DWN.
|
|
217
|
+
* Only published records are counted.
|
|
218
|
+
*/
|
|
219
|
+
count: async (request: ReaderRecordsCountRequest): Promise<ReaderRecordsCountResponse> => {
|
|
220
|
+
const reply = await this._anonymousDwn.recordsCount(request.from, {
|
|
221
|
+
filter: request.filter,
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const { count, status } = reply;
|
|
225
|
+
|
|
226
|
+
return { count, status };
|
|
227
|
+
},
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* API to query published protocol definitions from remote DWNs.
|
|
233
|
+
*/
|
|
234
|
+
get protocols(): {
|
|
235
|
+
query: (request: ReaderProtocolsQueryRequest) => Promise<ReaderProtocolsQueryResponse>;
|
|
236
|
+
} {
|
|
237
|
+
return {
|
|
238
|
+
/**
|
|
239
|
+
* Query published protocols from a remote DWN.
|
|
240
|
+
* Only protocol definitions with `published: true` are returned.
|
|
241
|
+
*/
|
|
242
|
+
query: async (request: ReaderProtocolsQueryRequest): Promise<ReaderProtocolsQueryResponse> => {
|
|
243
|
+
const reply = await this._anonymousDwn.protocolsQuery(request.from, {
|
|
244
|
+
filter: request.filter,
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
const { entries = [], status } = reply;
|
|
248
|
+
|
|
249
|
+
const protocols = entries.map((entry) => entry.descriptor.definition);
|
|
250
|
+
|
|
251
|
+
return { protocols, status };
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -23,14 +23,16 @@
|
|
|
23
23
|
|
|
24
24
|
export * from './define-protocol.js';
|
|
25
25
|
export * from './did-api.js';
|
|
26
|
-
export * from './dwn-api.js';
|
|
26
|
+
export * from './dwn-reader-api.js';
|
|
27
27
|
export * from './grant-revocation.js';
|
|
28
|
+
export * from './live-query.js';
|
|
28
29
|
export * from './permission-grant.js';
|
|
29
30
|
export * from './permission-request.js';
|
|
30
31
|
export * from './protocol.js';
|
|
31
32
|
export * from './protocol-types.js';
|
|
33
|
+
export * from './read-only-record.js';
|
|
32
34
|
export * from './record.js';
|
|
33
|
-
export * from './typed-
|
|
35
|
+
export * from './typed-web5.js';
|
|
34
36
|
export * from './vc-api.js';
|
|
35
37
|
export * from './web5.js';
|
|
36
38
|
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import type { DwnMessageSubscription, PermissionsApi, Web5Agent } from '@enbox/agent';
|
|
2
|
+
import type { PaginationCursor, RecordsQueryReplyEntry } from '@enbox/dwn-sdk-js';
|
|
3
|
+
|
|
4
|
+
import { getRecordAuthor } from '@enbox/agent';
|
|
5
|
+
|
|
6
|
+
import { Record } from './record.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The type of change that occurred to a record.
|
|
10
|
+
*/
|
|
11
|
+
export type RecordChangeType = 'create' | 'update' | 'delete';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Describes a change to a record in a {@link LiveQuery}.
|
|
15
|
+
*/
|
|
16
|
+
export type RecordChange = {
|
|
17
|
+
/** Whether the record was created, updated, or deleted. */
|
|
18
|
+
type: RecordChangeType;
|
|
19
|
+
|
|
20
|
+
/** The record affected by the change. */
|
|
21
|
+
record: Record;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A `CustomEvent` subclass carrying a {@link RecordChange} as its `detail`.
|
|
26
|
+
*
|
|
27
|
+
* Dispatched on the {@link LiveQuery} `EventTarget` for both the specific
|
|
28
|
+
* change-type event (`create`, `update`, `delete`) and the catch-all `change`
|
|
29
|
+
* event.
|
|
30
|
+
*/
|
|
31
|
+
export class RecordChangeEvent extends CustomEvent<RecordChange> {
|
|
32
|
+
constructor(change: RecordChange) {
|
|
33
|
+
super(change.type, { detail: change });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Options for creating a {@link LiveQuery}.
|
|
39
|
+
* @internal — Constructed by `DwnApi.records.subscribe()`, not by end users.
|
|
40
|
+
*/
|
|
41
|
+
export type LiveQueryOptions = {
|
|
42
|
+
/** The agent instance used to construct Record objects. */
|
|
43
|
+
agent: Web5Agent;
|
|
44
|
+
|
|
45
|
+
/** The DID of the connected user. */
|
|
46
|
+
connectedDid: string;
|
|
47
|
+
|
|
48
|
+
/** Optional delegate DID for permission-delegated access. */
|
|
49
|
+
delegateDid?: string;
|
|
50
|
+
|
|
51
|
+
/** Optional protocol role for role-authorized access. */
|
|
52
|
+
protocolRole?: string;
|
|
53
|
+
|
|
54
|
+
/** Optional remote DWN origin if subscribing to a remote DWN. */
|
|
55
|
+
remoteOrigin?: string;
|
|
56
|
+
|
|
57
|
+
/** The permissions API instance for constructing Record objects. */
|
|
58
|
+
permissionsApi?: PermissionsApi;
|
|
59
|
+
|
|
60
|
+
/** The initial snapshot entries from the subscribe reply. */
|
|
61
|
+
initialEntries: RecordsQueryReplyEntry[];
|
|
62
|
+
|
|
63
|
+
/** Pagination cursor for fetching the next page of initial results. */
|
|
64
|
+
cursor?: PaginationCursor;
|
|
65
|
+
|
|
66
|
+
/** The underlying DWN subscription handle. */
|
|
67
|
+
subscription: DwnMessageSubscription;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* A live query that combines an initial snapshot of matching records with a
|
|
72
|
+
* real-time stream of deduplicated, semantically-typed change events.
|
|
73
|
+
*
|
|
74
|
+
* `LiveQuery` extends `EventTarget` so that standard `addEventListener` /
|
|
75
|
+
* `removeEventListener` work out of the box. For convenience, the typed
|
|
76
|
+
* {@link LiveQuery.on | `.on()`} method provides a cleaner API that returns an
|
|
77
|
+
* unsubscribe function.
|
|
78
|
+
*
|
|
79
|
+
* ### Events
|
|
80
|
+
*
|
|
81
|
+
* | Event name | `detail` type | Description |
|
|
82
|
+
* |---|---|---|
|
|
83
|
+
* | `create` | {@link RecordChange} | A new record was written |
|
|
84
|
+
* | `update` | {@link RecordChange} | An existing record was updated |
|
|
85
|
+
* | `delete` | {@link RecordChange} | A record was deleted |
|
|
86
|
+
* | `change` | {@link RecordChange} | Catch-all for any of the above |
|
|
87
|
+
*
|
|
88
|
+
* @example
|
|
89
|
+
* ```ts
|
|
90
|
+
* const { liveQuery } = await dwn.records.subscribe({
|
|
91
|
+
* message: {
|
|
92
|
+
* filter: {
|
|
93
|
+
* protocol : chatProtocol.protocol,
|
|
94
|
+
* protocolPath : 'thread/message',
|
|
95
|
+
* }
|
|
96
|
+
* }
|
|
97
|
+
* });
|
|
98
|
+
*
|
|
99
|
+
* // Initial state
|
|
100
|
+
* for (const record of liveQuery.records) {
|
|
101
|
+
* renderMessage(record);
|
|
102
|
+
* }
|
|
103
|
+
*
|
|
104
|
+
* // Real-time changes
|
|
105
|
+
* liveQuery.on('create', (record) => appendMessage(record));
|
|
106
|
+
* liveQuery.on('update', (record) => refreshMessage(record));
|
|
107
|
+
* liveQuery.on('delete', (record) => removeMessage(record));
|
|
108
|
+
*
|
|
109
|
+
* // Or use the catch-all
|
|
110
|
+
* liveQuery.on('change', ({ type, record }) => {
|
|
111
|
+
* console.log(`${type}: ${record.id}`);
|
|
112
|
+
* });
|
|
113
|
+
*
|
|
114
|
+
* // Cleanup
|
|
115
|
+
* await liveQuery.close();
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export class LiveQuery extends EventTarget {
|
|
119
|
+
/** The initial snapshot of matching records. */
|
|
120
|
+
readonly records: Record[];
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Pagination cursor for fetching the next page of initial results.
|
|
124
|
+
*
|
|
125
|
+
* When the initial snapshot was limited (via `pagination.limit`), this
|
|
126
|
+
* cursor can be passed in a subsequent `records.subscribe()` call to
|
|
127
|
+
* continue from where the previous snapshot left off.
|
|
128
|
+
*
|
|
129
|
+
* `undefined` when there are no more results.
|
|
130
|
+
*/
|
|
131
|
+
readonly cursor?: PaginationCursor;
|
|
132
|
+
|
|
133
|
+
/** The underlying DWN subscription handle. */
|
|
134
|
+
private _subscription: DwnMessageSubscription;
|
|
135
|
+
|
|
136
|
+
/** Tracks known record states for dedup and change-type classification. */
|
|
137
|
+
private _knownRecords: Map<string, string>;
|
|
138
|
+
|
|
139
|
+
/** Whether the live query has been closed. */
|
|
140
|
+
private _closed = false;
|
|
141
|
+
|
|
142
|
+
constructor(options: LiveQueryOptions) {
|
|
143
|
+
super();
|
|
144
|
+
|
|
145
|
+
const {
|
|
146
|
+
agent,
|
|
147
|
+
connectedDid,
|
|
148
|
+
cursor,
|
|
149
|
+
delegateDid,
|
|
150
|
+
protocolRole,
|
|
151
|
+
remoteOrigin,
|
|
152
|
+
permissionsApi,
|
|
153
|
+
initialEntries,
|
|
154
|
+
subscription,
|
|
155
|
+
} = options;
|
|
156
|
+
|
|
157
|
+
this._subscription = subscription;
|
|
158
|
+
this.cursor = cursor;
|
|
159
|
+
|
|
160
|
+
// Build Record objects from the initial snapshot entries (same logic as records.query()).
|
|
161
|
+
this.records = initialEntries.map((entry) => new Record(agent, {
|
|
162
|
+
author: getRecordAuthor(entry),
|
|
163
|
+
connectedDid,
|
|
164
|
+
remoteOrigin,
|
|
165
|
+
delegateDid,
|
|
166
|
+
protocolRole,
|
|
167
|
+
...entry,
|
|
168
|
+
}, permissionsApi));
|
|
169
|
+
|
|
170
|
+
// Seed the known-records map with recordId -> messageTimestamp for dedup.
|
|
171
|
+
this._knownRecords = new Map();
|
|
172
|
+
for (const record of this.records) {
|
|
173
|
+
this._knownRecords.set(record.id, record.timestamp);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Process an incoming live event from the DWN subscription.
|
|
179
|
+
* Deduplicates against the initial snapshot and classifies the change type.
|
|
180
|
+
*
|
|
181
|
+
* @internal — Called by `DwnApi.records.subscribe()` when wiring up the subscription handler.
|
|
182
|
+
*/
|
|
183
|
+
public handleEvent(record: Record): void {
|
|
184
|
+
if (this._closed) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let changeType: RecordChangeType;
|
|
189
|
+
|
|
190
|
+
if (record.deleted) {
|
|
191
|
+
changeType = 'delete';
|
|
192
|
+
this._knownRecords.delete(record.id);
|
|
193
|
+
} else {
|
|
194
|
+
const knownTimestamp = this._knownRecords.get(record.id);
|
|
195
|
+
|
|
196
|
+
if (knownTimestamp !== undefined) {
|
|
197
|
+
// We've seen this recordId before (either from snapshot or a prior event).
|
|
198
|
+
if (record.timestamp <= knownTimestamp) {
|
|
199
|
+
// Duplicate or stale event from the overlap window — skip.
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
changeType = 'update';
|
|
203
|
+
} else {
|
|
204
|
+
changeType = 'create';
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Update the known state.
|
|
208
|
+
this._knownRecords.set(record.id, record.timestamp);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const change: RecordChange = { type: changeType, record };
|
|
212
|
+
|
|
213
|
+
// Dispatch the specific event (create/update/delete).
|
|
214
|
+
this.dispatchEvent(new RecordChangeEvent(change));
|
|
215
|
+
|
|
216
|
+
// Dispatch the catch-all change event.
|
|
217
|
+
this.dispatchEvent(new CustomEvent('change', { detail: change }));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Register a typed event handler. Returns an unsubscribe function.
|
|
222
|
+
*
|
|
223
|
+
* @param event - The event type to listen for.
|
|
224
|
+
* @param handler - The handler function.
|
|
225
|
+
* @returns A function that removes the handler when called.
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```ts
|
|
229
|
+
* const off = live.on('create', (record) => console.log(record.id));
|
|
230
|
+
* off(); // stop listening
|
|
231
|
+
* ```
|
|
232
|
+
*/
|
|
233
|
+
on(event: 'change', handler: (change: RecordChange) => void): () => void;
|
|
234
|
+
on(event: 'create', handler: (record: Record) => void): () => void;
|
|
235
|
+
on(event: 'update', handler: (record: Record) => void): () => void;
|
|
236
|
+
on(event: 'delete', handler: (record: Record) => void): () => void;
|
|
237
|
+
on(event: 'change' | 'create' | 'update' | 'delete', handler: ((change: RecordChange) => void) | ((record: Record) => void)): () => void {
|
|
238
|
+
const wrapper = (e: Event): void => {
|
|
239
|
+
const detail = (e as CustomEvent<RecordChange>).detail;
|
|
240
|
+
if (event === 'change') {
|
|
241
|
+
(handler as (change: RecordChange) => void)(detail);
|
|
242
|
+
} else {
|
|
243
|
+
(handler as (record: Record) => void)(detail.record);
|
|
244
|
+
}
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
this.addEventListener(event, wrapper);
|
|
248
|
+
return (): void => { this.removeEventListener(event, wrapper); };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Close the underlying subscription and stop dispatching events.
|
|
253
|
+
*/
|
|
254
|
+
async close(): Promise<void> {
|
|
255
|
+
if (this._closed) {
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
this._closed = true;
|
|
259
|
+
await this._subscription.close();
|
|
260
|
+
}
|
|
261
|
+
}
|
package/src/protocol-types.ts
CHANGED
|
@@ -133,7 +133,7 @@ export type TagKeys<Tags extends ProtocolTagsDefinition> = Exclude<
|
|
|
133
133
|
/**
|
|
134
134
|
* A mapping from protocol type names to their TypeScript data shapes.
|
|
135
135
|
*
|
|
136
|
-
* Used as a type parameter to `defineProtocol()` and `
|
|
136
|
+
* Used as a type parameter to `defineProtocol()` and `TypedWeb5` so that
|
|
137
137
|
* the protocol definition JSON stays JSON-compatible while TypeScript types
|
|
138
138
|
* are tracked separately.
|
|
139
139
|
*
|
|
@@ -154,7 +154,7 @@ export type SchemaMap = Record<string, unknown>;
|
|
|
154
154
|
/**
|
|
155
155
|
* The return type of `defineProtocol()`. Bundles the raw protocol definition
|
|
156
156
|
* with its inferred path strings and schema type map for downstream use
|
|
157
|
-
* by `
|
|
157
|
+
* by `TypedWeb5`.
|
|
158
158
|
*/
|
|
159
159
|
export type TypedProtocol<
|
|
160
160
|
D extends ProtocolDefinition = ProtocolDefinition,
|