@enbox/api 0.2.3 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +235 -35
- package/dist/browser.mjs +13 -13
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/dwn-api.js +24 -10
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/live-query.js +34 -5
- package/dist/esm/live-query.js.map +1 -1
- package/dist/esm/permission-grant.js +3 -6
- package/dist/esm/permission-grant.js.map +1 -1
- package/dist/esm/permission-request.js +4 -7
- package/dist/esm/permission-request.js.map +1 -1
- package/dist/esm/record-data.js +131 -0
- package/dist/esm/record-data.js.map +1 -0
- package/dist/esm/record-types.js +9 -0
- package/dist/esm/record-types.js.map +1 -0
- package/dist/esm/record.js +58 -184
- package/dist/esm/record.js.map +1 -1
- package/dist/esm/repository-types.js +13 -0
- package/dist/esm/repository-types.js.map +1 -0
- package/dist/esm/repository.js +347 -0
- package/dist/esm/repository.js.map +1 -0
- package/dist/esm/typed-live-query.js +129 -0
- package/dist/esm/typed-live-query.js.map +1 -0
- package/dist/esm/typed-record.js +227 -0
- package/dist/esm/typed-record.js.map +1 -0
- package/dist/esm/typed-web5.js +134 -23
- package/dist/esm/typed-web5.js.map +1 -1
- package/dist/esm/web5.js +83 -22
- package/dist/esm/web5.js.map +1 -1
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/live-query.d.ts +43 -4
- package/dist/types/live-query.d.ts.map +1 -1
- package/dist/types/permission-grant.d.ts +1 -1
- package/dist/types/permission-grant.d.ts.map +1 -1
- package/dist/types/permission-request.d.ts +1 -1
- package/dist/types/permission-request.d.ts.map +1 -1
- package/dist/types/record-data.d.ts +49 -0
- package/dist/types/record-data.d.ts.map +1 -0
- package/dist/types/record-types.d.ts +145 -0
- package/dist/types/record-types.d.ts.map +1 -0
- package/dist/types/record.d.ts +13 -144
- package/dist/types/record.d.ts.map +1 -1
- package/dist/types/repository-types.d.ts +137 -0
- package/dist/types/repository-types.d.ts.map +1 -0
- package/dist/types/repository.d.ts +59 -0
- package/dist/types/repository.d.ts.map +1 -0
- package/dist/types/typed-live-query.d.ts +111 -0
- package/dist/types/typed-live-query.d.ts.map +1 -0
- package/dist/types/typed-record.d.ts +179 -0
- package/dist/types/typed-record.d.ts.map +1 -0
- package/dist/types/typed-web5.d.ts +55 -24
- package/dist/types/typed-web5.d.ts.map +1 -1
- package/dist/types/web5.d.ts +54 -4
- package/dist/types/web5.d.ts.map +1 -1
- package/package.json +8 -7
- package/src/dwn-api.ts +30 -13
- package/src/index.ts +6 -0
- package/src/live-query.ts +71 -7
- package/src/permission-grant.ts +2 -3
- package/src/permission-request.ts +3 -4
- package/src/record-data.ts +155 -0
- package/src/record-types.ts +188 -0
- package/src/record.ts +86 -389
- package/src/repository-types.ts +249 -0
- package/src/repository.ts +391 -0
- package/src/typed-live-query.ts +200 -0
- package/src/typed-record.ts +309 -0
- package/src/typed-web5.ts +202 -49
- package/src/web5.ts +162 -27
package/src/typed-web5.ts
CHANGED
|
@@ -6,6 +6,10 @@
|
|
|
6
6
|
* and schema into every operation, and provides compile-time path
|
|
7
7
|
* autocompletion plus typed data payloads via the schema map.
|
|
8
8
|
*
|
|
9
|
+
* All record-returning methods wrap the underlying `Record` instances in
|
|
10
|
+
* {@link TypedRecord} so that type information flows through reads, queries,
|
|
11
|
+
* updates, and subscriptions without manual casts.
|
|
12
|
+
*
|
|
9
13
|
* @example
|
|
10
14
|
* ```ts
|
|
11
15
|
* const social = web5.using(SocialProtocol);
|
|
@@ -13,29 +17,36 @@
|
|
|
13
17
|
* // Install the protocol
|
|
14
18
|
* await social.configure();
|
|
15
19
|
*
|
|
16
|
-
* //
|
|
17
|
-
* const { record } = await social.records.
|
|
20
|
+
* // Create — path and data type are checked at compile time
|
|
21
|
+
* const { record } = await social.records.create('thread', {
|
|
18
22
|
* data: { title: 'Hello World', body: '...' },
|
|
19
23
|
* });
|
|
24
|
+
* // record is TypedRecord<ThreadData>
|
|
25
|
+
*
|
|
26
|
+
* const data = await record.data.json(); // ThreadData — no cast needed
|
|
20
27
|
*
|
|
21
28
|
* // Query — protocol and protocolPath are auto-injected
|
|
22
29
|
* const { records } = await social.records.query('thread');
|
|
30
|
+
* // records is TypedRecord<ThreadData>[]
|
|
23
31
|
*
|
|
24
|
-
* // Subscribe — real-time changes via
|
|
32
|
+
* // Subscribe — real-time changes via TypedLiveQuery
|
|
25
33
|
* const { liveQuery } = await social.records.subscribe('thread/reply');
|
|
26
|
-
* liveQuery.on('create', (record) => {
|
|
34
|
+
* liveQuery.on('create', (record) => {
|
|
35
|
+
* // record is TypedRecord<ReplyData>
|
|
36
|
+
* });
|
|
27
37
|
* ```
|
|
28
38
|
*/
|
|
29
39
|
|
|
30
40
|
import type { DwnApi } from './dwn-api.js';
|
|
31
|
-
import type { LiveQuery } from './live-query.js';
|
|
32
41
|
import type { Protocol } from './protocol.js';
|
|
33
|
-
import type { Record } from './record.js';
|
|
34
42
|
|
|
35
43
|
import type { DateSort, ProtocolDefinition, ProtocolType, RecordsFilter } from '@enbox/dwn-sdk-js';
|
|
36
44
|
import type { DwnPaginationCursor, DwnResponseStatus } from '@enbox/agent';
|
|
37
45
|
import type { ProtocolPaths, SchemaMap, TypedProtocol, TypeNameAtPath } from './protocol-types.js';
|
|
38
46
|
|
|
47
|
+
import { TypedLiveQuery } from './typed-live-query.js';
|
|
48
|
+
import { TypedRecord } from './typed-record.js';
|
|
49
|
+
|
|
39
50
|
// ---------------------------------------------------------------------------
|
|
40
51
|
// Helper types
|
|
41
52
|
// ---------------------------------------------------------------------------
|
|
@@ -46,7 +57,7 @@ import type { ProtocolPaths, SchemaMap, TypedProtocol, TypeNameAtPath } from './
|
|
|
46
57
|
* If the schema map contains a mapping for the type name at the given path,
|
|
47
58
|
* that type is returned. Otherwise falls back to `unknown`.
|
|
48
59
|
*/
|
|
49
|
-
type DataForPath<
|
|
60
|
+
export type DataForPath<
|
|
50
61
|
_D extends ProtocolDefinition,
|
|
51
62
|
M extends SchemaMap,
|
|
52
63
|
Path extends string,
|
|
@@ -80,8 +91,8 @@ type DataFormatForPath<
|
|
|
80
91
|
// Request / response types
|
|
81
92
|
// ---------------------------------------------------------------------------
|
|
82
93
|
|
|
83
|
-
/** Options for {@link TypedWeb5} `records.
|
|
84
|
-
export type
|
|
94
|
+
/** Options for {@link TypedWeb5} `records.create()`. */
|
|
95
|
+
export type TypedCreateRequest<
|
|
85
96
|
D extends ProtocolDefinition,
|
|
86
97
|
M extends SchemaMap,
|
|
87
98
|
Path extends string,
|
|
@@ -104,9 +115,9 @@ export type TypedWriteRequest<
|
|
|
104
115
|
encryption?: boolean;
|
|
105
116
|
};
|
|
106
117
|
|
|
107
|
-
/** Response from {@link TypedWeb5} `records.
|
|
108
|
-
export type
|
|
109
|
-
record:
|
|
118
|
+
/** Response from {@link TypedWeb5} `records.create()`. */
|
|
119
|
+
export type TypedCreateResponse<T = unknown> = DwnResponseStatus & {
|
|
120
|
+
record: TypedRecord<T>;
|
|
110
121
|
};
|
|
111
122
|
|
|
112
123
|
/** Filter options for {@link TypedWeb5} `records.query()`. */
|
|
@@ -130,8 +141,8 @@ export type TypedQueryRequest = {
|
|
|
130
141
|
};
|
|
131
142
|
|
|
132
143
|
/** Response from {@link TypedWeb5} `records.query()`. */
|
|
133
|
-
export type TypedQueryResponse = DwnResponseStatus & {
|
|
134
|
-
records:
|
|
144
|
+
export type TypedQueryResponse<T = unknown> = DwnResponseStatus & {
|
|
145
|
+
records: TypedRecord<T>[];
|
|
135
146
|
cursor?: DwnPaginationCursor;
|
|
136
147
|
};
|
|
137
148
|
|
|
@@ -148,8 +159,8 @@ export type TypedReadRequest = {
|
|
|
148
159
|
};
|
|
149
160
|
|
|
150
161
|
/** Response from {@link TypedWeb5} `records.read()`. */
|
|
151
|
-
export type TypedReadResponse = DwnResponseStatus & {
|
|
152
|
-
record:
|
|
162
|
+
export type TypedReadResponse<T = unknown> = DwnResponseStatus & {
|
|
163
|
+
record: TypedRecord<T>;
|
|
153
164
|
};
|
|
154
165
|
|
|
155
166
|
/** Options for {@link TypedWeb5} `records.delete()`. */
|
|
@@ -172,9 +183,9 @@ export type TypedSubscribeRequest = {
|
|
|
172
183
|
};
|
|
173
184
|
|
|
174
185
|
/** Response from {@link TypedWeb5} `records.subscribe()`. */
|
|
175
|
-
export type TypedSubscribeResponse = DwnResponseStatus & {
|
|
176
|
-
/** The live query instance, or `undefined` if the request failed. */
|
|
177
|
-
liveQuery?:
|
|
186
|
+
export type TypedSubscribeResponse<T = unknown> = DwnResponseStatus & {
|
|
187
|
+
/** The typed live query instance, or `undefined` if the request failed. */
|
|
188
|
+
liveQuery?: TypedLiveQuery<T>;
|
|
178
189
|
};
|
|
179
190
|
|
|
180
191
|
// ---------------------------------------------------------------------------
|
|
@@ -185,6 +196,10 @@ export type TypedSubscribeResponse = DwnResponseStatus & {
|
|
|
185
196
|
* A protocol-scoped API that auto-injects `protocol`, `protocolPath`, and
|
|
186
197
|
* `schema` into every DWN operation.
|
|
187
198
|
*
|
|
199
|
+
* All record-returning methods wrap results in {@link TypedRecord} so that
|
|
200
|
+
* the data type `T` (resolved from the schema map) flows end-to-end — from
|
|
201
|
+
* write through read, query, update, and subscribe — without manual casts.
|
|
202
|
+
*
|
|
188
203
|
* Obtain an instance via `web5.using(typedProtocol)`.
|
|
189
204
|
*
|
|
190
205
|
* @example
|
|
@@ -193,13 +208,17 @@ export type TypedSubscribeResponse = DwnResponseStatus & {
|
|
|
193
208
|
*
|
|
194
209
|
* await social.configure();
|
|
195
210
|
*
|
|
196
|
-
* const { record } = await social.records.
|
|
211
|
+
* const { record } = await social.records.create('friend', {
|
|
197
212
|
* data: { did: 'did:example:alice', alias: 'Alice' },
|
|
198
213
|
* });
|
|
214
|
+
* const data = await record.data.json(); // FriendData — no cast
|
|
199
215
|
*
|
|
200
216
|
* const { records } = await social.records.query('friend', {
|
|
201
217
|
* filter: { tags: { did: 'did:example:alice' } },
|
|
202
218
|
* });
|
|
219
|
+
* for (const r of records) {
|
|
220
|
+
* const d = await r.data.json(); // FriendData
|
|
221
|
+
* }
|
|
203
222
|
* ```
|
|
204
223
|
*/
|
|
205
224
|
export class TypedWeb5<
|
|
@@ -208,10 +227,14 @@ export class TypedWeb5<
|
|
|
208
227
|
> {
|
|
209
228
|
private _dwn: DwnApi;
|
|
210
229
|
private _definition: D;
|
|
230
|
+
private _configured: boolean = false;
|
|
231
|
+
private _validPaths: Set<string>;
|
|
232
|
+
private _records?: TypedWeb5<D, M>['records'];
|
|
211
233
|
|
|
212
234
|
constructor(dwn: DwnApi, protocol: TypedProtocol<D, M>) {
|
|
213
235
|
this._dwn = dwn;
|
|
214
236
|
this._definition = protocol.definition;
|
|
237
|
+
this._validPaths = collectPaths(this._definition.structure);
|
|
215
238
|
}
|
|
216
239
|
|
|
217
240
|
/** The protocol URI. */
|
|
@@ -244,15 +267,47 @@ export class TypedWeb5<
|
|
|
244
267
|
if (protocols.length > 0) {
|
|
245
268
|
const existing = protocols[0];
|
|
246
269
|
if (definitionsEqual(existing.definition, this._definition)) {
|
|
270
|
+
this._configured = true;
|
|
247
271
|
return { status: { code: 200, detail: 'OK' }, protocol: existing };
|
|
248
272
|
}
|
|
249
273
|
}
|
|
250
274
|
|
|
251
275
|
// Not installed or definition has changed — configure the new version.
|
|
252
|
-
|
|
276
|
+
const result = await this._dwn.protocols.configure({
|
|
253
277
|
definition : this._definition,
|
|
254
278
|
encryption : options?.encryption,
|
|
255
279
|
});
|
|
280
|
+
|
|
281
|
+
if (result.status.code === 202) {
|
|
282
|
+
this._configured = true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return result;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Whether the protocol has been configured (installed) on the local DWN. */
|
|
289
|
+
public get isConfigured(): boolean {
|
|
290
|
+
return this._configured;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Validates that the protocol has been configured and that the path is
|
|
295
|
+
* recognized. Throws a descriptive error if either check fails.
|
|
296
|
+
*/
|
|
297
|
+
private _assertReady(path: string): void {
|
|
298
|
+
if (!this._configured) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`TypedWeb5: protocol '${this._definition.protocol}' has not been configured. ` +
|
|
301
|
+
'Call configure() before performing record operations.',
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!this._validPaths.has(path)) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`TypedWeb5: invalid protocol path '${path}'. ` +
|
|
308
|
+
`Valid paths are: ${[...this._validPaths].join(', ')}.`,
|
|
309
|
+
);
|
|
310
|
+
}
|
|
256
311
|
}
|
|
257
312
|
|
|
258
313
|
/**
|
|
@@ -261,29 +316,57 @@ export class TypedWeb5<
|
|
|
261
316
|
* Every method auto-injects the protocol URI, protocolPath, and schema
|
|
262
317
|
* from the protocol definition. Path parameters provide compile-time
|
|
263
318
|
* autocompletion via `ProtocolPaths<D>`.
|
|
319
|
+
*
|
|
320
|
+
* All methods return {@link TypedRecord} or {@link TypedLiveQuery} instances
|
|
321
|
+
* that carry the resolved data type from the schema map.
|
|
264
322
|
*/
|
|
265
323
|
public get records(): {
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
324
|
+
create: <Path extends ProtocolPaths<D> & string>(
|
|
325
|
+
path: Path,
|
|
326
|
+
request: TypedCreateRequest<D, M, Path>,
|
|
327
|
+
) => Promise<TypedCreateResponse<DataForPath<D, M, Path>>>;
|
|
328
|
+
|
|
329
|
+
query: <Path extends ProtocolPaths<D> & string>(
|
|
330
|
+
path: Path,
|
|
331
|
+
request?: TypedQueryRequest,
|
|
332
|
+
) => Promise<TypedQueryResponse<DataForPath<D, M, Path>>>;
|
|
333
|
+
|
|
334
|
+
read: <Path extends ProtocolPaths<D> & string>(
|
|
335
|
+
path: Path,
|
|
336
|
+
request: TypedReadRequest,
|
|
337
|
+
) => Promise<TypedReadResponse<DataForPath<D, M, Path>>>;
|
|
338
|
+
|
|
339
|
+
delete: <Path extends ProtocolPaths<D> & string>(
|
|
340
|
+
path: Path,
|
|
341
|
+
request: TypedDeleteRequest,
|
|
342
|
+
) => Promise<DwnResponseStatus>;
|
|
343
|
+
|
|
344
|
+
subscribe: <Path extends ProtocolPaths<D> & string>(
|
|
345
|
+
path: Path,
|
|
346
|
+
request?: TypedSubscribeRequest,
|
|
347
|
+
) => Promise<TypedSubscribeResponse<DataForPath<D, M, Path>>>;
|
|
271
348
|
} {
|
|
272
|
-
|
|
349
|
+
if (this._records !== undefined) {
|
|
350
|
+
return this._records;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const cached = {
|
|
273
354
|
/**
|
|
274
|
-
*
|
|
355
|
+
* Create a new record at the given protocol path.
|
|
275
356
|
*
|
|
276
357
|
* @param path - The protocol path (e.g. `'friend'`, `'group/member'`).
|
|
277
|
-
* @param request -
|
|
358
|
+
* @param request - Create options including typed `data`.
|
|
278
359
|
*/
|
|
279
|
-
|
|
360
|
+
create: async <Path extends ProtocolPaths<D> & string>(
|
|
280
361
|
path: Path,
|
|
281
|
-
request:
|
|
282
|
-
): Promise<
|
|
283
|
-
const
|
|
362
|
+
request: TypedCreateRequest<D, M, Path>,
|
|
363
|
+
): Promise<TypedCreateResponse<DataForPath<D, M, Path>>> => {
|
|
364
|
+
const normalizedPath = normalizePath(path);
|
|
365
|
+
this._assertReady(normalizedPath);
|
|
366
|
+
const typeName = lastSegment(normalizedPath);
|
|
284
367
|
const typeEntry = this._definition.types[typeName] as ProtocolType | undefined;
|
|
285
368
|
|
|
286
|
-
|
|
369
|
+
const { status, record } = await this._dwn.records.write({
|
|
287
370
|
data : request.data,
|
|
288
371
|
store : request.store,
|
|
289
372
|
encryption : request.encryption,
|
|
@@ -294,10 +377,15 @@ export class TypedWeb5<
|
|
|
294
377
|
protocolRole : request.protocolRole,
|
|
295
378
|
tags : request.tags,
|
|
296
379
|
protocol : this._definition.protocol,
|
|
297
|
-
protocolPath :
|
|
380
|
+
protocolPath : normalizedPath,
|
|
298
381
|
...(typeEntry?.schema !== undefined ? { schema: typeEntry.schema } : {}),
|
|
299
382
|
dataFormat : request.dataFormat ?? typeEntry?.dataFormats?.[0],
|
|
300
383
|
});
|
|
384
|
+
|
|
385
|
+
return {
|
|
386
|
+
status,
|
|
387
|
+
record: new TypedRecord<DataForPath<D, M, Path>>(record),
|
|
388
|
+
};
|
|
301
389
|
},
|
|
302
390
|
|
|
303
391
|
/**
|
|
@@ -309,23 +397,31 @@ export class TypedWeb5<
|
|
|
309
397
|
query: async <Path extends ProtocolPaths<D> & string>(
|
|
310
398
|
path: Path,
|
|
311
399
|
request?: TypedQueryRequest,
|
|
312
|
-
): Promise<TypedQueryResponse
|
|
313
|
-
const
|
|
400
|
+
): Promise<TypedQueryResponse<DataForPath<D, M, Path>>> => {
|
|
401
|
+
const normalizedPath = normalizePath(path);
|
|
402
|
+
this._assertReady(normalizedPath);
|
|
403
|
+
const typeName = lastSegment(normalizedPath);
|
|
314
404
|
const typeEntry = this._definition.types[typeName] as ProtocolType | undefined;
|
|
315
405
|
|
|
316
|
-
|
|
406
|
+
const { status, records, cursor } = await this._dwn.records.query({
|
|
317
407
|
from : request?.from,
|
|
318
408
|
encryption : request?.encryption,
|
|
319
409
|
filter : {
|
|
320
410
|
...request?.filter,
|
|
321
411
|
protocol : this._definition.protocol,
|
|
322
|
-
protocolPath :
|
|
412
|
+
protocolPath : normalizedPath,
|
|
323
413
|
...(typeEntry?.schema !== undefined ? { schema: typeEntry.schema } : {}),
|
|
324
414
|
},
|
|
325
415
|
dateSort : request?.dateSort,
|
|
326
416
|
pagination : request?.pagination,
|
|
327
417
|
protocolRole : request?.protocolRole,
|
|
328
418
|
});
|
|
419
|
+
|
|
420
|
+
return {
|
|
421
|
+
status,
|
|
422
|
+
records: records.map((r) => new TypedRecord<DataForPath<D, M, Path>>(r)),
|
|
423
|
+
cursor,
|
|
424
|
+
};
|
|
329
425
|
},
|
|
330
426
|
|
|
331
427
|
/**
|
|
@@ -337,21 +433,28 @@ export class TypedWeb5<
|
|
|
337
433
|
read: async <Path extends ProtocolPaths<D> & string>(
|
|
338
434
|
path: Path,
|
|
339
435
|
request: TypedReadRequest,
|
|
340
|
-
): Promise<TypedReadResponse
|
|
341
|
-
const
|
|
436
|
+
): Promise<TypedReadResponse<DataForPath<D, M, Path>>> => {
|
|
437
|
+
const normalizedPath = normalizePath(path);
|
|
438
|
+
this._assertReady(normalizedPath);
|
|
439
|
+
const typeName = lastSegment(normalizedPath);
|
|
342
440
|
const typeEntry = this._definition.types[typeName] as ProtocolType | undefined;
|
|
343
441
|
|
|
344
|
-
|
|
442
|
+
const { status, record } = await this._dwn.records.read({
|
|
345
443
|
from : request.from,
|
|
346
444
|
encryption : request.encryption,
|
|
347
445
|
protocol : this._definition.protocol,
|
|
348
446
|
filter : {
|
|
349
447
|
...request.filter,
|
|
350
448
|
protocol : this._definition.protocol,
|
|
351
|
-
protocolPath :
|
|
449
|
+
protocolPath : normalizedPath,
|
|
352
450
|
...(typeEntry?.schema !== undefined ? { schema: typeEntry.schema } : {}),
|
|
353
451
|
},
|
|
354
452
|
});
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
status,
|
|
456
|
+
record: new TypedRecord<DataForPath<D, M, Path>>(record),
|
|
457
|
+
};
|
|
355
458
|
},
|
|
356
459
|
|
|
357
460
|
/**
|
|
@@ -364,6 +467,7 @@ export class TypedWeb5<
|
|
|
364
467
|
_path: Path,
|
|
365
468
|
request: TypedDeleteRequest,
|
|
366
469
|
): Promise<DwnResponseStatus> => {
|
|
470
|
+
this._assertReady(normalizePath(_path));
|
|
367
471
|
return this._dwn.records.delete({
|
|
368
472
|
from : request.from,
|
|
369
473
|
protocol : this._definition.protocol,
|
|
@@ -374,8 +478,9 @@ export class TypedWeb5<
|
|
|
374
478
|
/**
|
|
375
479
|
* Subscribe to records at the given protocol path.
|
|
376
480
|
*
|
|
377
|
-
* Returns a {@link
|
|
378
|
-
* and a real-time stream of deduplicated change events
|
|
481
|
+
* Returns a {@link TypedLiveQuery} that atomically provides an initial
|
|
482
|
+
* snapshot and a real-time stream of deduplicated change events, with
|
|
483
|
+
* all records typed as `TypedRecord<T>`.
|
|
379
484
|
*
|
|
380
485
|
* @param path - The protocol path to subscribe to.
|
|
381
486
|
* @param request - Optional filter and role.
|
|
@@ -383,22 +488,32 @@ export class TypedWeb5<
|
|
|
383
488
|
subscribe: async <Path extends ProtocolPaths<D> & string>(
|
|
384
489
|
path: Path,
|
|
385
490
|
request?: TypedSubscribeRequest,
|
|
386
|
-
): Promise<TypedSubscribeResponse
|
|
387
|
-
const
|
|
491
|
+
): Promise<TypedSubscribeResponse<DataForPath<D, M, Path>>> => {
|
|
492
|
+
const normalizedPath = normalizePath(path);
|
|
493
|
+
this._assertReady(normalizedPath);
|
|
494
|
+
const typeName = lastSegment(normalizedPath);
|
|
388
495
|
const typeEntry = this._definition.types[typeName] as ProtocolType | undefined;
|
|
389
496
|
|
|
390
|
-
|
|
497
|
+
const { status, liveQuery } = await this._dwn.records.subscribe({
|
|
391
498
|
from : request?.from,
|
|
392
499
|
filter : {
|
|
393
500
|
...request?.filter,
|
|
394
501
|
protocol : this._definition.protocol,
|
|
395
|
-
protocolPath :
|
|
502
|
+
protocolPath : normalizedPath,
|
|
396
503
|
...(typeEntry?.schema !== undefined ? { schema: typeEntry.schema } : {}),
|
|
397
504
|
},
|
|
398
505
|
protocolRole: request?.protocolRole,
|
|
399
506
|
});
|
|
507
|
+
|
|
508
|
+
return {
|
|
509
|
+
status,
|
|
510
|
+
liveQuery: liveQuery ? new TypedLiveQuery<DataForPath<D, M, Path>>(liveQuery) : undefined,
|
|
511
|
+
};
|
|
400
512
|
},
|
|
401
513
|
};
|
|
514
|
+
|
|
515
|
+
this._records = cached;
|
|
516
|
+
return cached;
|
|
402
517
|
}
|
|
403
518
|
}
|
|
404
519
|
|
|
@@ -417,6 +532,15 @@ function definitionsEqual(a: unknown, b: unknown): boolean {
|
|
|
417
532
|
return stableStringify(a) === stableStringify(b);
|
|
418
533
|
}
|
|
419
534
|
|
|
535
|
+
/**
|
|
536
|
+
* Strips leading and trailing slashes from a path.
|
|
537
|
+
*
|
|
538
|
+
* `'friend/'` → `'friend'`, `'/group/member/'` → `'group/member'`.
|
|
539
|
+
*/
|
|
540
|
+
function normalizePath(path: string): string {
|
|
541
|
+
return path.replace(/^\/+|\/+$/g, '');
|
|
542
|
+
}
|
|
543
|
+
|
|
420
544
|
/**
|
|
421
545
|
* Returns the last segment of a slash-delimited path.
|
|
422
546
|
*/
|
|
@@ -425,6 +549,35 @@ function lastSegment(path: string): string {
|
|
|
425
549
|
return parts[parts.length - 1];
|
|
426
550
|
}
|
|
427
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Recursively collects all valid protocol path strings from a structure object.
|
|
554
|
+
*
|
|
555
|
+
* Given `{ foo: { bar: { $actions: [...] } } }`, returns `Set(['foo', 'foo/bar'])`.
|
|
556
|
+
* Keys starting with `$` are skipped.
|
|
557
|
+
*/
|
|
558
|
+
function collectPaths(
|
|
559
|
+
structure: Record<string, unknown>,
|
|
560
|
+
prefix: string = '',
|
|
561
|
+
): Set<string> {
|
|
562
|
+
const paths = new Set<string>();
|
|
563
|
+
|
|
564
|
+
for (const key of Object.keys(structure)) {
|
|
565
|
+
if (key.startsWith('$')) { continue; }
|
|
566
|
+
|
|
567
|
+
const fullPath = prefix ? `${prefix}/${key}` : key;
|
|
568
|
+
paths.add(fullPath);
|
|
569
|
+
|
|
570
|
+
const child = structure[key];
|
|
571
|
+
if (child !== null && typeof child === 'object') {
|
|
572
|
+
for (const nested of collectPaths(child as Record<string, unknown>, fullPath)) {
|
|
573
|
+
paths.add(nested);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
return paths;
|
|
579
|
+
}
|
|
580
|
+
|
|
428
581
|
/**
|
|
429
582
|
* Deterministic JSON serialization with sorted keys.
|
|
430
583
|
*/
|