@enbox/api 0.0.8 → 0.1.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.
- package/dist/browser.mjs +46 -53
- package/dist/browser.mjs.map +4 -4
- package/dist/esm/define-protocol.js +37 -0
- package/dist/esm/define-protocol.js.map +1 -0
- package/dist/esm/dwn-api.js +54 -26
- package/dist/esm/dwn-api.js.map +1 -1
- package/dist/esm/index.js +4 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/live-query.js +153 -0
- package/dist/esm/live-query.js.map +1 -0
- package/dist/esm/protocol-types.js +8 -0
- package/dist/esm/protocol-types.js.map +1 -0
- package/dist/esm/record.js.map +1 -1
- package/dist/esm/typed-dwn-api.js +182 -0
- package/dist/esm/typed-dwn-api.js.map +1 -0
- package/dist/esm/web5.js +3 -2
- package/dist/esm/web5.js.map +1 -1
- package/dist/types/define-protocol.d.ts +37 -0
- package/dist/types/define-protocol.d.ts.map +1 -0
- package/dist/types/dwn-api.d.ts +32 -17
- package/dist/types/dwn-api.d.ts.map +1 -1
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/live-query.d.ts +136 -0
- package/dist/types/live-query.d.ts.map +1 -0
- package/dist/types/protocol-types.d.ts +95 -0
- package/dist/types/protocol-types.d.ts.map +1 -0
- package/dist/types/record.d.ts +1 -1
- package/dist/types/record.d.ts.map +1 -1
- package/dist/types/typed-dwn-api.d.ts +193 -0
- package/dist/types/typed-dwn-api.d.ts.map +1 -0
- package/dist/types/web5.d.ts.map +1 -1
- package/package.json +14 -17
- package/src/define-protocol.ts +48 -0
- package/src/dwn-api.ts +87 -49
- package/src/index.ts +4 -0
- package/src/live-query.ts +245 -0
- package/src/protocol-types.ts +171 -0
- package/src/record.ts +3 -3
- package/src/typed-dwn-api.ts +370 -0
- package/src/web5.ts +3 -2
- package/dist/browser.js +0 -2224
- package/dist/browser.js.map +0 -7
- package/dist/esm/subscription-util.js +0 -35
- package/dist/esm/subscription-util.js.map +0 -1
- package/dist/types/subscription-util.d.ts +0 -19
- package/dist/types/subscription-util.d.ts.map +0 -1
- package/src/subscription-util.ts +0 -44
package/src/dwn-api.ts
CHANGED
|
@@ -8,9 +8,7 @@ import type {
|
|
|
8
8
|
CreateGrantParams,
|
|
9
9
|
CreateRequestParams,
|
|
10
10
|
DwnMessage,
|
|
11
|
-
DwnMessageParams
|
|
12
|
-
,
|
|
13
|
-
DwnMessageSubscription,
|
|
11
|
+
DwnMessageParams,
|
|
14
12
|
DwnPaginationCursor,
|
|
15
13
|
DwnResponse,
|
|
16
14
|
DwnResponseStatus,
|
|
@@ -26,12 +24,17 @@ import {
|
|
|
26
24
|
import { isEmptyObject } from '@enbox/common';
|
|
27
25
|
import { DwnInterface, getRecordAuthor } from '@enbox/agent';
|
|
28
26
|
|
|
27
|
+
import type { ProtocolDefinition } from '@enbox/dwn-sdk-js';
|
|
28
|
+
|
|
29
|
+
import type { SchemaMap, TypedProtocol } from './protocol-types.js';
|
|
30
|
+
|
|
29
31
|
import { dataToBlob } from './utils.js';
|
|
32
|
+
import { LiveQuery } from './live-query.js';
|
|
30
33
|
import { PermissionGrant } from './permission-grant.js';
|
|
31
34
|
import { PermissionRequest } from './permission-request.js';
|
|
32
35
|
import { Protocol } from './protocol.js';
|
|
33
36
|
import { Record } from './record.js';
|
|
34
|
-
import {
|
|
37
|
+
import { TypedDwnApi } from './typed-dwn-api.js';
|
|
35
38
|
|
|
36
39
|
/**
|
|
37
40
|
* Represents the request payload for fetching permission requests from a Decentralized Web Node (DWN).
|
|
@@ -221,39 +224,28 @@ export type RecordsReadResponse = DwnResponseStatus & {
|
|
|
221
224
|
record: Record;
|
|
222
225
|
};
|
|
223
226
|
|
|
224
|
-
/** Subscription handler for Records */
|
|
225
|
-
export type RecordsSubscriptionHandler = (record: Record) => void;
|
|
226
|
-
|
|
227
227
|
/**
|
|
228
228
|
* Represents a request to subscribe to records from a Decentralized Web Node (DWN).
|
|
229
229
|
*
|
|
230
|
-
*
|
|
231
|
-
*
|
|
230
|
+
* Returns a {@link LiveQuery} that atomically provides an initial snapshot of
|
|
231
|
+
* matching records alongside a real-time stream of deduplicated, semantically-
|
|
232
|
+
* typed change events (`create`, `update`, `delete`).
|
|
232
233
|
*/
|
|
233
234
|
export type RecordsSubscribeRequest = {
|
|
234
235
|
/** Optional DID specifying the remote target DWN tenant to subscribe from. */
|
|
235
236
|
from?: string;
|
|
236
237
|
|
|
237
|
-
/** Records must be scoped to a specific protocol */
|
|
238
|
+
/** Records must be scoped to a specific protocol. */
|
|
238
239
|
protocol?: string;
|
|
239
240
|
|
|
240
|
-
/** The parameters for the subscription operation, detailing the criteria for the subscription filter */
|
|
241
|
+
/** The parameters for the subscription operation, detailing the criteria for the subscription filter. */
|
|
241
242
|
message: Omit<DwnMessageParams[DwnInterface.RecordsSubscribe], 'signer'>;
|
|
242
|
-
|
|
243
|
-
/** The handler to process the subscription events */
|
|
244
|
-
subscriptionHandler: RecordsSubscriptionHandler;
|
|
245
|
-
|
|
246
|
-
/** When true, indicates encryption is active (decryption happens on subsequent reads). */
|
|
247
|
-
encryption?: boolean;
|
|
248
243
|
};
|
|
249
244
|
|
|
250
|
-
/** Encapsulates the response from a DWN
|
|
245
|
+
/** Encapsulates the response from a DWN RecordsSubscribeRequest. */
|
|
251
246
|
export type RecordsSubscribeResponse = DwnResponseStatus & {
|
|
252
|
-
/**
|
|
253
|
-
|
|
254
|
-
*
|
|
255
|
-
* */
|
|
256
|
-
subscription?: DwnMessageSubscription;
|
|
247
|
+
/** The live query instance, or `undefined` if the request failed. */
|
|
248
|
+
liveQuery?: LiveQuery;
|
|
257
249
|
};
|
|
258
250
|
|
|
259
251
|
/**
|
|
@@ -331,6 +323,30 @@ export class DwnApi {
|
|
|
331
323
|
this.permissionsApi = new AgentPermissionsApi({ agent: this.agent });
|
|
332
324
|
}
|
|
333
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Returns a {@link TypedDwnApi} scoped to the given typed protocol.
|
|
328
|
+
*
|
|
329
|
+
* The returned API provides path autocompletion, typed data payloads,
|
|
330
|
+
* and automatically injects the protocol URI and protocolPath into every
|
|
331
|
+
* DWN operation.
|
|
332
|
+
*
|
|
333
|
+
* @param protocol - A typed protocol created via `defineProtocol()`.
|
|
334
|
+
* @returns A `TypedDwnApi` instance bound to the given protocol.
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```ts
|
|
338
|
+
* const social = dwn.using(SocialGraphProtocol);
|
|
339
|
+
* const { record } = await social.write('friend', {
|
|
340
|
+
* data: { did: 'did:example:alice' },
|
|
341
|
+
* });
|
|
342
|
+
* ```
|
|
343
|
+
*/
|
|
344
|
+
public using<D extends ProtocolDefinition, M extends SchemaMap>(
|
|
345
|
+
protocol: TypedProtocol<D, M>,
|
|
346
|
+
): TypedDwnApi<D, M> {
|
|
347
|
+
return new TypedDwnApi<D, M>(this, protocol);
|
|
348
|
+
}
|
|
349
|
+
|
|
334
350
|
/**
|
|
335
351
|
* API to interact with Grants
|
|
336
352
|
*
|
|
@@ -660,6 +676,7 @@ export class DwnApi {
|
|
|
660
676
|
|
|
661
677
|
return { status };
|
|
662
678
|
},
|
|
679
|
+
|
|
663
680
|
/**
|
|
664
681
|
* Query a single or multiple records based on the given filter
|
|
665
682
|
*/
|
|
@@ -846,38 +863,46 @@ export class DwnApi {
|
|
|
846
863
|
},
|
|
847
864
|
|
|
848
865
|
/**
|
|
849
|
-
*
|
|
866
|
+
* Subscribe to records matching the given filter.
|
|
850
867
|
*
|
|
851
|
-
*
|
|
852
|
-
*
|
|
868
|
+
* Returns a {@link LiveQuery} that atomically provides an initial snapshot
|
|
869
|
+
* of matching records and a real-time stream of deduplicated, semantically-
|
|
870
|
+
* typed change events (`create`, `update`, `delete`).
|
|
853
871
|
*/
|
|
854
872
|
subscribe: async (request: RecordsSubscribeRequest): Promise<RecordsSubscribeResponse> => {
|
|
873
|
+
// Build a DWN-level subscription handler that wraps raw RecordEvents
|
|
874
|
+
// into Record objects and feeds them into the LiveQuery.
|
|
875
|
+
let liveQuery: LiveQuery | undefined;
|
|
876
|
+
|
|
877
|
+
const remoteOrigin = request.from;
|
|
878
|
+
const protocolRole = request.message.protocolRole;
|
|
879
|
+
|
|
880
|
+
type RecordEvent = {
|
|
881
|
+
message: DwnMessage[DwnInterface.RecordsWrite];
|
|
882
|
+
initialWrite?: DwnMessage[DwnInterface.RecordsWrite];
|
|
883
|
+
};
|
|
884
|
+
|
|
885
|
+
const subscriptionHandler = async (event: RecordEvent): Promise<void> => {
|
|
886
|
+
const { message, initialWrite } = event;
|
|
887
|
+
const record = new Record(this.agent, {
|
|
888
|
+
...message,
|
|
889
|
+
author : getRecordAuthor(message),
|
|
890
|
+
connectedDid : this.connectedDid,
|
|
891
|
+
remoteOrigin,
|
|
892
|
+
initialWrite,
|
|
893
|
+
protocolRole,
|
|
894
|
+
delegateDid : this.delegateDid,
|
|
895
|
+
}, this.permissionsApi);
|
|
896
|
+
|
|
897
|
+
liveQuery?.handleEvent(record);
|
|
898
|
+
};
|
|
899
|
+
|
|
855
900
|
const agentRequest: ProcessDwnRequest<DwnInterface.RecordsSubscribe> = {
|
|
856
|
-
/**
|
|
857
|
-
* The `author` is the DID that will sign the message and must be the DID the Web5 app is
|
|
858
|
-
* connected with and is authorized to access the signing private key of.
|
|
859
|
-
*/
|
|
860
901
|
author : this.connectedDid,
|
|
861
902
|
messageParams : request.message,
|
|
862
903
|
messageType : DwnInterface.RecordsSubscribe,
|
|
863
|
-
/**
|
|
864
|
-
* The `target` is the DID of the DWN tenant under which the subscribe operation will be executed.
|
|
865
|
-
* If `from` is provided, the subscribe operation will be executed on a remote DWN.
|
|
866
|
-
* Otherwise, the local DWN will execute the subscribe operation.
|
|
867
|
-
*/
|
|
868
904
|
target : request.from || this.connectedDid,
|
|
869
|
-
|
|
870
|
-
/**
|
|
871
|
-
* The handler to process the subscription events.
|
|
872
|
-
*/
|
|
873
|
-
subscriptionHandler: SubscriptionUtil.recordSubscriptionHandler({
|
|
874
|
-
agent : this.agent,
|
|
875
|
-
connectedDid : this.connectedDid,
|
|
876
|
-
delegateDid : this.delegateDid,
|
|
877
|
-
permissionsApi : this.permissionsApi,
|
|
878
|
-
protocolRole : request.message.protocolRole,
|
|
879
|
-
request
|
|
880
|
-
})
|
|
905
|
+
subscriptionHandler,
|
|
881
906
|
};
|
|
882
907
|
|
|
883
908
|
if (this.delegateDid) {
|
|
@@ -917,9 +942,22 @@ export class DwnApi {
|
|
|
917
942
|
}
|
|
918
943
|
|
|
919
944
|
const reply = agentResponse.reply;
|
|
920
|
-
const { status, subscription } = reply;
|
|
945
|
+
const { status, subscription, entries = [] } = reply;
|
|
946
|
+
|
|
947
|
+
if (subscription) {
|
|
948
|
+
liveQuery = new LiveQuery({
|
|
949
|
+
agent : this.agent,
|
|
950
|
+
connectedDid : this.connectedDid,
|
|
951
|
+
delegateDid : this.delegateDid,
|
|
952
|
+
protocolRole,
|
|
953
|
+
remoteOrigin,
|
|
954
|
+
permissionsApi : this.permissionsApi,
|
|
955
|
+
initialEntries : entries,
|
|
956
|
+
subscription,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
921
959
|
|
|
922
|
-
return { status,
|
|
960
|
+
return { status, liveQuery };
|
|
923
961
|
},
|
|
924
962
|
|
|
925
963
|
/**
|
package/src/index.ts
CHANGED
|
@@ -21,13 +21,17 @@
|
|
|
21
21
|
* @packageDocumentation
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
|
+
export * from './define-protocol.js';
|
|
24
25
|
export * from './did-api.js';
|
|
25
26
|
export * from './dwn-api.js';
|
|
26
27
|
export * from './grant-revocation.js';
|
|
28
|
+
export * from './live-query.js';
|
|
27
29
|
export * from './permission-grant.js';
|
|
28
30
|
export * from './permission-request.js';
|
|
29
31
|
export * from './protocol.js';
|
|
32
|
+
export * from './protocol-types.js';
|
|
30
33
|
export * from './record.js';
|
|
34
|
+
export * from './typed-dwn-api.js';
|
|
31
35
|
export * from './vc-api.js';
|
|
32
36
|
export * from './web5.js';
|
|
33
37
|
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import type { RecordsQueryReplyEntry } from '@enbox/dwn-sdk-js';
|
|
2
|
+
import type { DwnMessageSubscription, PermissionsApi, Web5Agent } from '@enbox/agent';
|
|
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
|
+
/** The underlying DWN subscription handle. */
|
|
64
|
+
subscription: DwnMessageSubscription;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* A live query that combines an initial snapshot of matching records with a
|
|
69
|
+
* real-time stream of deduplicated, semantically-typed change events.
|
|
70
|
+
*
|
|
71
|
+
* `LiveQuery` extends `EventTarget` so that standard `addEventListener` /
|
|
72
|
+
* `removeEventListener` work out of the box. For convenience, the typed
|
|
73
|
+
* {@link LiveQuery.on | `.on()`} method provides a cleaner API that returns an
|
|
74
|
+
* unsubscribe function.
|
|
75
|
+
*
|
|
76
|
+
* ### Events
|
|
77
|
+
*
|
|
78
|
+
* | Event name | `detail` type | Description |
|
|
79
|
+
* |---|---|---|
|
|
80
|
+
* | `create` | {@link RecordChange} | A new record was written |
|
|
81
|
+
* | `update` | {@link RecordChange} | An existing record was updated |
|
|
82
|
+
* | `delete` | {@link RecordChange} | A record was deleted |
|
|
83
|
+
* | `change` | {@link RecordChange} | Catch-all for any of the above |
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```ts
|
|
87
|
+
* const { liveQuery } = await dwn.records.subscribe({
|
|
88
|
+
* message: {
|
|
89
|
+
* filter: {
|
|
90
|
+
* protocol : chatProtocol.protocol,
|
|
91
|
+
* protocolPath : 'thread/message',
|
|
92
|
+
* }
|
|
93
|
+
* }
|
|
94
|
+
* });
|
|
95
|
+
*
|
|
96
|
+
* // Initial state
|
|
97
|
+
* for (const record of liveQuery.records) {
|
|
98
|
+
* renderMessage(record);
|
|
99
|
+
* }
|
|
100
|
+
*
|
|
101
|
+
* // Real-time changes
|
|
102
|
+
* liveQuery.on('create', (record) => appendMessage(record));
|
|
103
|
+
* liveQuery.on('update', (record) => refreshMessage(record));
|
|
104
|
+
* liveQuery.on('delete', (record) => removeMessage(record));
|
|
105
|
+
*
|
|
106
|
+
* // Or use the catch-all
|
|
107
|
+
* liveQuery.on('change', ({ type, record }) => {
|
|
108
|
+
* console.log(`${type}: ${record.id}`);
|
|
109
|
+
* });
|
|
110
|
+
*
|
|
111
|
+
* // Cleanup
|
|
112
|
+
* await liveQuery.close();
|
|
113
|
+
* ```
|
|
114
|
+
*/
|
|
115
|
+
export class LiveQuery extends EventTarget {
|
|
116
|
+
/** The initial snapshot of matching records. */
|
|
117
|
+
readonly records: Record[];
|
|
118
|
+
|
|
119
|
+
/** The underlying DWN subscription handle. */
|
|
120
|
+
private _subscription: DwnMessageSubscription;
|
|
121
|
+
|
|
122
|
+
/** Tracks known record states for dedup and change-type classification. */
|
|
123
|
+
private _knownRecords: Map<string, string>;
|
|
124
|
+
|
|
125
|
+
/** Whether the live query has been closed. */
|
|
126
|
+
private _closed = false;
|
|
127
|
+
|
|
128
|
+
constructor(options: LiveQueryOptions) {
|
|
129
|
+
super();
|
|
130
|
+
|
|
131
|
+
const {
|
|
132
|
+
agent,
|
|
133
|
+
connectedDid,
|
|
134
|
+
delegateDid,
|
|
135
|
+
protocolRole,
|
|
136
|
+
remoteOrigin,
|
|
137
|
+
permissionsApi,
|
|
138
|
+
initialEntries,
|
|
139
|
+
subscription,
|
|
140
|
+
} = options;
|
|
141
|
+
|
|
142
|
+
this._subscription = subscription;
|
|
143
|
+
|
|
144
|
+
// Build Record objects from the initial snapshot entries (same logic as records.query()).
|
|
145
|
+
this.records = initialEntries.map((entry) => new Record(agent, {
|
|
146
|
+
author: getRecordAuthor(entry),
|
|
147
|
+
connectedDid,
|
|
148
|
+
remoteOrigin,
|
|
149
|
+
delegateDid,
|
|
150
|
+
protocolRole,
|
|
151
|
+
...entry,
|
|
152
|
+
}, permissionsApi));
|
|
153
|
+
|
|
154
|
+
// Seed the known-records map with recordId -> messageTimestamp for dedup.
|
|
155
|
+
this._knownRecords = new Map();
|
|
156
|
+
for (const record of this.records) {
|
|
157
|
+
this._knownRecords.set(record.id, record.dateModified);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Process an incoming live event from the DWN subscription.
|
|
163
|
+
* Deduplicates against the initial snapshot and classifies the change type.
|
|
164
|
+
*
|
|
165
|
+
* @internal — Called by `DwnApi.records.subscribe()` when wiring up the subscription handler.
|
|
166
|
+
*/
|
|
167
|
+
public handleEvent(record: Record): void {
|
|
168
|
+
if (this._closed) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let changeType: RecordChangeType;
|
|
173
|
+
|
|
174
|
+
if (record.deleted) {
|
|
175
|
+
changeType = 'delete';
|
|
176
|
+
this._knownRecords.delete(record.id);
|
|
177
|
+
} else {
|
|
178
|
+
const knownTimestamp = this._knownRecords.get(record.id);
|
|
179
|
+
|
|
180
|
+
if (knownTimestamp !== undefined) {
|
|
181
|
+
// We've seen this recordId before (either from snapshot or a prior event).
|
|
182
|
+
if (record.dateModified <= knownTimestamp) {
|
|
183
|
+
// Duplicate or stale event from the overlap window — skip.
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
changeType = 'update';
|
|
187
|
+
} else {
|
|
188
|
+
changeType = 'create';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Update the known state.
|
|
192
|
+
this._knownRecords.set(record.id, record.dateModified);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const change: RecordChange = { type: changeType, record };
|
|
196
|
+
|
|
197
|
+
// Dispatch the specific event (create/update/delete).
|
|
198
|
+
this.dispatchEvent(new RecordChangeEvent(change));
|
|
199
|
+
|
|
200
|
+
// Dispatch the catch-all change event.
|
|
201
|
+
this.dispatchEvent(new CustomEvent('change', { detail: change }));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Register a typed event handler. Returns an unsubscribe function.
|
|
206
|
+
*
|
|
207
|
+
* @param event - The event type to listen for.
|
|
208
|
+
* @param handler - The handler function.
|
|
209
|
+
* @returns A function that removes the handler when called.
|
|
210
|
+
*
|
|
211
|
+
* @example
|
|
212
|
+
* ```ts
|
|
213
|
+
* const off = live.on('create', (record) => console.log(record.id));
|
|
214
|
+
* off(); // stop listening
|
|
215
|
+
* ```
|
|
216
|
+
*/
|
|
217
|
+
on(event: 'change', handler: (change: RecordChange) => void): () => void;
|
|
218
|
+
on(event: 'create', handler: (record: Record) => void): () => void;
|
|
219
|
+
on(event: 'update', handler: (record: Record) => void): () => void;
|
|
220
|
+
on(event: 'delete', handler: (record: Record) => void): () => void;
|
|
221
|
+
on(event: 'change' | 'create' | 'update' | 'delete', handler: ((change: RecordChange) => void) | ((record: Record) => void)): () => void {
|
|
222
|
+
const wrapper = (e: Event): void => {
|
|
223
|
+
const detail = (e as CustomEvent<RecordChange>).detail;
|
|
224
|
+
if (event === 'change') {
|
|
225
|
+
(handler as (change: RecordChange) => void)(detail);
|
|
226
|
+
} else {
|
|
227
|
+
(handler as (record: Record) => void)(detail.record);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
this.addEventListener(event, wrapper);
|
|
232
|
+
return (): void => { this.removeEventListener(event, wrapper); };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Close the underlying subscription and stop dispatching events.
|
|
237
|
+
*/
|
|
238
|
+
async close(): Promise<void> {
|
|
239
|
+
if (this._closed) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
this._closed = true;
|
|
243
|
+
await this._subscription.close();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-level utilities for extracting typed paths, type names, schemas,
|
|
3
|
+
* data formats, and tag shapes from a {@link ProtocolDefinition}.
|
|
4
|
+
*
|
|
5
|
+
* These types are purely compile-time — they produce no runtime code.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ProtocolDefinition, ProtocolRuleSet, ProtocolTagsDefinition, ProtocolType } from '@enbox/dwn-sdk-js';
|
|
9
|
+
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Path extraction
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Recursively extracts all valid protocol path strings from a `ProtocolRuleSet`.
|
|
16
|
+
*
|
|
17
|
+
* Given a structure like `{ foo: { bar: { ... } } }`, this produces
|
|
18
|
+
* `'foo' | 'foo/bar'`. Directive keys (starting with `$`) are excluded.
|
|
19
|
+
*/
|
|
20
|
+
export type RuleSetPaths<R, Prefix extends string = ''> = {
|
|
21
|
+
[K in Extract<keyof R, string>]: K extends `$${string}`
|
|
22
|
+
? never
|
|
23
|
+
: R[K] extends ProtocolRuleSet
|
|
24
|
+
? `${Prefix}${K}` | RuleSetPaths<R[K], `${Prefix}${K}/`>
|
|
25
|
+
: never;
|
|
26
|
+
}[Extract<keyof R, string>];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* All valid protocol path strings for a given `ProtocolDefinition`.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* type Paths = ProtocolPaths<typeof myDef>;
|
|
34
|
+
* // 'friend' | 'friend/message' | 'group' | 'group/member'
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
export type ProtocolPaths<D extends ProtocolDefinition> = RuleSetPaths<D['structure']>;
|
|
38
|
+
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
// Type-name extraction
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Extracts the last segment (type name) from a protocol path string.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* type T = TypeNameAtPath<'group/member'>; // 'member'
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export type TypeNameAtPath<Path extends string> =
|
|
52
|
+
Path extends `${string}/${infer Rest}`
|
|
53
|
+
? TypeNameAtPath<Rest>
|
|
54
|
+
: Path;
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Schema & data-format lookup
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The type names declared in the `types` map of a `ProtocolDefinition`.
|
|
62
|
+
*/
|
|
63
|
+
export type TypeNames<D extends ProtocolDefinition> = Extract<keyof D['types'], string>;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Looks up the `schema` URI for a given type name in the protocol definition.
|
|
67
|
+
* Returns `undefined` if the type does not declare a schema.
|
|
68
|
+
*/
|
|
69
|
+
export type SchemaForType<D extends ProtocolDefinition, TypeName extends string> =
|
|
70
|
+
TypeName extends keyof D['types']
|
|
71
|
+
? D['types'][TypeName] extends ProtocolType
|
|
72
|
+
? D['types'][TypeName]['schema']
|
|
73
|
+
: undefined
|
|
74
|
+
: undefined;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Looks up the `dataFormats` array for a given type name in the protocol definition.
|
|
78
|
+
* Returns `undefined` if the type does not declare dataFormats.
|
|
79
|
+
*/
|
|
80
|
+
export type DataFormatsForType<D extends ProtocolDefinition, TypeName extends string> =
|
|
81
|
+
TypeName extends keyof D['types']
|
|
82
|
+
? D['types'][TypeName] extends ProtocolType
|
|
83
|
+
? D['types'][TypeName]['dataFormats']
|
|
84
|
+
: undefined
|
|
85
|
+
: undefined;
|
|
86
|
+
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
// Tag extraction helpers
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Navigates a `ProtocolRuleSet` tree to the node at the given slash-delimited path.
|
|
93
|
+
*/
|
|
94
|
+
type RuleSetAtPath<R, Path extends string> =
|
|
95
|
+
Path extends `${infer Head}/${infer Tail}`
|
|
96
|
+
? Head extends keyof R
|
|
97
|
+
? R[Head] extends ProtocolRuleSet
|
|
98
|
+
? RuleSetAtPath<R[Head], Tail>
|
|
99
|
+
: never
|
|
100
|
+
: never
|
|
101
|
+
: Path extends keyof R
|
|
102
|
+
? R[Path] extends ProtocolRuleSet
|
|
103
|
+
? R[Path]
|
|
104
|
+
: never
|
|
105
|
+
: never;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Extracts the `$tags` definition for a given protocol path.
|
|
109
|
+
* Returns `never` if the path does not declare `$tags`.
|
|
110
|
+
*/
|
|
111
|
+
export type TagsAtPath<D extends ProtocolDefinition, Path extends string> =
|
|
112
|
+
RuleSetAtPath<D['structure'], Path> extends infer RS
|
|
113
|
+
? RS extends ProtocolRuleSet
|
|
114
|
+
? RS['$tags'] extends ProtocolTagsDefinition
|
|
115
|
+
? RS['$tags']
|
|
116
|
+
: never
|
|
117
|
+
: never
|
|
118
|
+
: never;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Extracts the user-defined tag keys (excluding `$`-prefixed meta-keys)
|
|
122
|
+
* from a `ProtocolTagsDefinition`.
|
|
123
|
+
*/
|
|
124
|
+
export type TagKeys<Tags extends ProtocolTagsDefinition> = Exclude<
|
|
125
|
+
Extract<keyof Tags, string>,
|
|
126
|
+
`$${string}`
|
|
127
|
+
>;
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// Schema map — associates TypeScript data types with protocol type names
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* A mapping from protocol type names to their TypeScript data shapes.
|
|
135
|
+
*
|
|
136
|
+
* Used as a type parameter to `defineProtocol()` and `TypedDwnApi` so that
|
|
137
|
+
* the protocol definition JSON stays JSON-compatible while TypeScript types
|
|
138
|
+
* are tracked separately.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```ts
|
|
142
|
+
* type MySchemaMap = {
|
|
143
|
+
* profile : { displayName: string; bio?: string };
|
|
144
|
+
* avatar : Blob;
|
|
145
|
+
* };
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export type SchemaMap = Record<string, unknown>;
|
|
149
|
+
|
|
150
|
+
// ---------------------------------------------------------------------------
|
|
151
|
+
// Typed protocol — output of defineProtocol()
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The return type of `defineProtocol()`. Bundles the raw protocol definition
|
|
156
|
+
* with its inferred path strings and schema type map for downstream use
|
|
157
|
+
* by `TypedDwnApi`.
|
|
158
|
+
*/
|
|
159
|
+
export type TypedProtocol<
|
|
160
|
+
D extends ProtocolDefinition = ProtocolDefinition,
|
|
161
|
+
M extends SchemaMap = SchemaMap,
|
|
162
|
+
> = {
|
|
163
|
+
/** The raw DWN protocol definition (JSON-compatible). */
|
|
164
|
+
readonly definition: D;
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Phantom property carrying the schema map type. Not present at runtime;
|
|
168
|
+
* used only by TypeScript to thread the generic through.
|
|
169
|
+
*/
|
|
170
|
+
readonly _schemaMap?: M;
|
|
171
|
+
};
|
package/src/record.ts
CHANGED
|
@@ -445,7 +445,7 @@ export class Record implements RecordModel {
|
|
|
445
445
|
get data(): {
|
|
446
446
|
blob: () => Promise<Blob>;
|
|
447
447
|
bytes: () => Promise<Uint8Array>;
|
|
448
|
-
json: () => Promise<
|
|
448
|
+
json: <T = unknown>() => Promise<T>;
|
|
449
449
|
text: () => Promise<string>;
|
|
450
450
|
stream: () => Promise<ReadableStream>;
|
|
451
451
|
then: (
|
|
@@ -489,8 +489,8 @@ export class Record implements RecordModel {
|
|
|
489
489
|
*
|
|
490
490
|
* @beta
|
|
491
491
|
*/
|
|
492
|
-
async json(): Promise<
|
|
493
|
-
return await Stream.consumeToJson({ readableStream: await this.stream() });
|
|
492
|
+
async json<T = unknown>(): Promise<T> {
|
|
493
|
+
return await Stream.consumeToJson({ readableStream: await this.stream() }) as T;
|
|
494
494
|
},
|
|
495
495
|
|
|
496
496
|
/**
|