Skip to content

Commit a2f0dbe

Browse files
LiranCohenandorsk
andauthored
EventsSubscribe (#658)
This PR introduces an `EventStream` interface and an implementation based on `EventEmitter` which emits events for any interfaces which are saved as a part of the DWN's message store. (ie. not Query/Read/Subscribe messages). We also introduce an `EventsSubscribe` interface that follows the same authorization model as `EventsGet` and `EventsQuery`, which only allows access to the tenant owner. In a subsequent PR we will introduce `RecordsSubscribe`, as well as some additional enhancements. Co-authored-by: Liran Cohen <[email protected]> Co-authored-by: Andor Kesselman <[email protected]>
1 parent 30ba7d5 commit a2f0dbe

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+2195
-283
lines changed

build/compile-validators.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import Definitions from '../json-schemas/definitions.json' assert { type: 'json'
2424
import EventsFilter from '../json-schemas/interface-methods/events-filter.json' assert { type: 'json' };
2525
import EventsGet from '../json-schemas/interface-methods/events-get.json' assert { type: 'json' };
2626
import EventsQuery from '../json-schemas/interface-methods/events-query.json' assert { type: 'json' };
27+
import EventsSubscribe from '../json-schemas/interface-methods/events-subscribe.json' assert { type: 'json' };
2728
import GeneralJwk from '../json-schemas/jwk/general-jwk.json' assert { type: 'json' };
2829
import GeneralJws from '../json-schemas/general-jws.json' assert { type: 'json' };
2930
import GenericSignaturePayload from '../json-schemas/signature-payloads/generic-signature-payload.json' assert { type: 'json' };
@@ -60,6 +61,7 @@ const schemas = {
6061
EventsFilter,
6162
EventsGet,
6263
EventsQuery,
64+
EventsSubscribe,
6365
Definitions,
6466
GeneralJwk,
6567
GeneralJws,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"$id": "https://identity.foundation/dwn/json-schemas/events-subscribe.json",
4+
"type": "object",
5+
"additionalProperties": false,
6+
"required": [
7+
"descriptor",
8+
"authorization"
9+
],
10+
"properties": {
11+
"authorization": {
12+
"$ref": "https://identity.foundation/dwn/json-schemas/authorization.json"
13+
},
14+
"descriptor": {
15+
"type": "object",
16+
"additionalProperties": false,
17+
"required": [
18+
"interface",
19+
"method",
20+
"messageTimestamp",
21+
"filters"
22+
],
23+
"properties": {
24+
"interface": {
25+
"enum": [
26+
"Events"
27+
],
28+
"type": "string"
29+
},
30+
"method": {
31+
"enum": [
32+
"Subscribe"
33+
],
34+
"type": "string"
35+
},
36+
"messageTimestamp": {
37+
"type": "string"
38+
},
39+
"filters": {
40+
"type": "array",
41+
"items": {
42+
"$ref": "https://identity.foundation/dwn/json-schemas/events-filter.json"
43+
}
44+
}
45+
}
46+
}
47+
}
48+
}

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@tbd54566975/dwn-sdk-js",
3-
"version": "0.2.12",
3+
"version": "0.2.13",
44
"description": "A reference implementation of https://identity.foundation/decentralized-web-node/spec/",
55
"repository": {
66
"type": "git",

src/core/dwn-error.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export enum DwnErrorCode {
2525
DidNotValid = 'DidNotValid',
2626
DidResolutionFailed = 'DidResolutionFailed',
2727
Ed25519InvalidJwk = 'Ed25519InvalidJwk',
28+
EventsSubscribeEventStreamUnimplemented = 'EventsSubscribeEventStreamUnimplemented',
2829
GeneralJwsVerifierGetPublicKeyNotFound = 'GeneralJwsVerifierGetPublicKeyNotFound',
2930
GeneralJwsVerifierInvalidSignature = 'GeneralJwsVerifierInvalidSignature',
3031
GrantAuthorizationGrantExpired = 'GrantAuthorizationGrantExpired',

src/core/message-reply.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { PaginationCursor } from '../types/query-types.js';
33
import type { ProtocolsConfigureMessage } from '../types/protocols-types.js';
44
import type { Readable } from 'readable-stream';
55
import type { RecordsWriteMessage } from '../types/records-types.js';
6-
import type { GenericMessageReply, QueryResultEntry } from '../types/message-types.js';
6+
import type { GenericMessageReply, MessageSubscription, QueryResultEntry } from '../types/message-types.js';
77

88
export function messageReplyFromError(e: unknown, code: number): GenericMessageReply {
99

@@ -40,4 +40,9 @@ export type UnionMessageReply = GenericMessageReply & {
4040
* Mutually exclusive with `record`.
4141
*/
4242
cursor?: PaginationCursor;
43+
44+
/**
45+
* A subscription object if a subscription was requested.
46+
*/
47+
subscription?: MessageSubscription;
4348
};

src/dwn.ts

Lines changed: 88 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type { DataStore } from './types/data-store.js';
22
import type { EventLog } from './types/event-log.js';
3+
import type { EventStream } from './types/subscriptions.js';
34
import type { MessageStore } from './types/message-store.js';
45
import type { MethodHandler } from './types/method-handler.js';
56
import type { TenantGate } from './core/tenant-gate.js';
67
import type { UnionMessageReply } from './core/message-reply.js';
7-
import type { EventsGetMessage, EventsGetReply, EventsQueryMessage, EventsQueryReply } from './types/event-types.js';
8+
import type { EventsGetMessage, EventsGetReply, EventsQueryMessage, EventsQueryReply, EventsSubscribeMessage, EventsSubscribeMessageOptions, EventsSubscribeReply } from './types/events-types.js';
89
import type { GenericMessage, GenericMessageReply, MessageOptions } from './types/message-types.js';
910
import type { MessagesGetMessage, MessagesGetReply } from './types/messages-types.js';
1011
import type { PermissionsGrantMessage, PermissionsRequestMessage, PermissionsRevokeMessage } from './types/permissions-types.js';
@@ -15,6 +16,7 @@ import { AllowAllTenantGate } from './core/tenant-gate.js';
1516
import { DidResolver } from './did/did-resolver.js';
1617
import { EventsGetHandler } from './handlers/events-get.js';
1718
import { EventsQueryHandler } from './handlers/events-query.js';
19+
import { EventsSubscribeHandler } from './handlers/events-subscribe.js';
1820
import { Message } from './core/message.js';
1921
import { messageReplyFromError } from './core/message-reply.js';
2022
import { MessagesGetHandler } from './handlers/messages-get.js';
@@ -36,32 +38,87 @@ export class Dwn {
3638
private dataStore: DataStore;
3739
private eventLog: EventLog;
3840
private tenantGate: TenantGate;
41+
private eventStream?: EventStream;
3942

4043
private constructor(config: DwnConfig) {
4144
this.didResolver = config.didResolver!;
4245
this.tenantGate = config.tenantGate!;
4346
this.messageStore = config.messageStore;
4447
this.dataStore = config.dataStore;
4548
this.eventLog = config.eventLog;
49+
this.eventStream = config.eventStream;
4650

4751
this.methodHandlers = {
48-
[DwnInterfaceName.Events + DwnMethodName.Get] : new EventsGetHandler(this.didResolver, this.eventLog),
49-
[DwnInterfaceName.Events + DwnMethodName.Query] : new EventsQueryHandler(this.didResolver, this.eventLog),
50-
[DwnInterfaceName.Messages + DwnMethodName.Get] : new MessagesGetHandler(this.didResolver, this.messageStore, this.dataStore),
51-
[DwnInterfaceName.Permissions + DwnMethodName.Grant] : new PermissionsGrantHandler(
52-
this.didResolver, this.messageStore, this.eventLog),
52+
[DwnInterfaceName.Events + DwnMethodName.Get]: new EventsGetHandler(
53+
this.didResolver,
54+
this.eventLog,
55+
),
56+
[DwnInterfaceName.Events + DwnMethodName.Query]: new EventsQueryHandler(
57+
this.didResolver,
58+
this.eventLog,
59+
),
60+
[DwnInterfaceName.Events+ DwnMethodName.Subscribe]: new EventsSubscribeHandler(
61+
this.didResolver,
62+
this.eventStream,
63+
),
64+
[DwnInterfaceName.Messages + DwnMethodName.Get]: new MessagesGetHandler(
65+
this.didResolver,
66+
this.messageStore,
67+
this.dataStore,
68+
),
69+
[DwnInterfaceName.Permissions + DwnMethodName.Grant]: new PermissionsGrantHandler(
70+
this.didResolver,
71+
this.messageStore,
72+
this.eventLog,
73+
this.eventStream
74+
),
5375
[DwnInterfaceName.Permissions + DwnMethodName.Request]: new PermissionsRequestHandler(
54-
this.didResolver, this.messageStore, this.eventLog),
76+
this.didResolver,
77+
this.messageStore,
78+
this.eventLog,
79+
this.eventStream
80+
),
5581
[DwnInterfaceName.Permissions + DwnMethodName.Revoke]: new PermissionsRevokeHandler(
56-
this.didResolver, this.messageStore, this.eventLog),
82+
this.didResolver,
83+
this.messageStore,
84+
this.eventLog,
85+
this.eventStream
86+
),
5787
[DwnInterfaceName.Protocols + DwnMethodName.Configure]: new ProtocolsConfigureHandler(
58-
this.didResolver, this.messageStore, this.dataStore, this.eventLog),
59-
[DwnInterfaceName.Protocols + DwnMethodName.Query] : new ProtocolsQueryHandler(this.didResolver, this.messageStore, this.dataStore),
60-
[DwnInterfaceName.Records + DwnMethodName.Delete] : new RecordsDeleteHandler(
61-
this.didResolver, this.messageStore, this.dataStore, this.eventLog),
62-
[DwnInterfaceName.Records + DwnMethodName.Query] : new RecordsQueryHandler(this.didResolver, this.messageStore, this.dataStore),
63-
[DwnInterfaceName.Records + DwnMethodName.Read] : new RecordsReadHandler(this.didResolver, this.messageStore, this.dataStore),
64-
[DwnInterfaceName.Records + DwnMethodName.Write] : new RecordsWriteHandler(this.didResolver, this.messageStore, this.dataStore, this.eventLog),
88+
this.didResolver,
89+
this.messageStore,
90+
this.eventLog,
91+
this.eventStream
92+
),
93+
[DwnInterfaceName.Protocols + DwnMethodName.Query]: new ProtocolsQueryHandler(
94+
this.didResolver,
95+
this.messageStore,
96+
this.dataStore
97+
),
98+
[DwnInterfaceName.Records + DwnMethodName.Delete]: new RecordsDeleteHandler(
99+
this.didResolver,
100+
this.messageStore,
101+
this.dataStore,
102+
this.eventLog,
103+
this.eventStream
104+
),
105+
[DwnInterfaceName.Records + DwnMethodName.Query]: new RecordsQueryHandler(
106+
this.didResolver,
107+
this.messageStore,
108+
this.dataStore
109+
),
110+
[DwnInterfaceName.Records + DwnMethodName.Read]: new RecordsReadHandler(
111+
this.didResolver,
112+
this.messageStore,
113+
this.dataStore
114+
),
115+
[DwnInterfaceName.Records + DwnMethodName.Write]: new RecordsWriteHandler(
116+
this.didResolver,
117+
this.messageStore,
118+
this.dataStore,
119+
this.eventLog,
120+
this.eventStream
121+
)
65122
};
66123
}
67124

@@ -82,12 +139,14 @@ export class Dwn {
82139
await this.messageStore.open();
83140
await this.dataStore.open();
84141
await this.eventLog.open();
142+
await this.eventStream?.open();
85143
}
86144

87145
public async close(): Promise<void> {
88-
this.messageStore.close();
89-
this.dataStore.close();
90-
this.eventLog.close();
146+
await this.eventStream?.close();
147+
await this.messageStore.close();
148+
await this.dataStore.close();
149+
await this.eventLog.close();
91150
}
92151

93152
/**
@@ -96,6 +155,8 @@ export class Dwn {
96155
*/
97156
public async processMessage(tenant: string, rawMessage: EventsGetMessage): Promise<EventsGetReply>;
98157
public async processMessage(tenant: string, rawMessage: EventsQueryMessage): Promise<EventsQueryReply>;
158+
public async processMessage(
159+
tenant: string, rawMessage: EventsSubscribeMessage, options?: EventsSubscribeMessageOptions): Promise<EventsSubscribeReply>;
99160
public async processMessage(tenant: string, rawMessage: MessagesGetMessage): Promise<MessagesGetReply>;
100161
public async processMessage(tenant: string, rawMessage: ProtocolsConfigureMessage): Promise<GenericMessageReply>;
101162
public async processMessage(tenant: string, rawMessage: ProtocolsQueryMessage): Promise<ProtocolsQueryReply>;
@@ -113,13 +174,14 @@ export class Dwn {
113174
return errorMessageReply;
114175
}
115176

116-
const { dataStream } = options;
177+
const { dataStream, subscriptionHandler } = options;
117178

118179
const handlerKey = rawMessage.descriptor.interface + rawMessage.descriptor.method;
119180
const methodHandlerReply = await this.methodHandlers[handlerKey].handle({
120181
tenant,
121182
message: rawMessage as GenericMessage,
122-
dataStream
183+
dataStream,
184+
subscriptionHandler
123185
});
124186

125187
return methodHandlerReply;
@@ -154,6 +216,7 @@ export class Dwn {
154216
// Verify interface and method
155217
const dwnInterface = rawMessage?.descriptor?.interface;
156218
const dwnMethod = rawMessage?.descriptor?.method;
219+
157220
if (dwnInterface === undefined || dwnMethod === undefined) {
158221
return {
159222
status: { code: 400, detail: `Both interface and method must be present, interface: ${dwnInterface}, method: ${dwnMethod}` }
@@ -174,10 +237,13 @@ export class Dwn {
174237
* DWN configuration.
175238
*/
176239
export type DwnConfig = {
177-
didResolver?: DidResolver,
240+
didResolver?: DidResolver;
178241
tenantGate?: TenantGate;
179242

243+
// event stream is optional if a DWN does not wish to provide subscription services.
244+
eventStream?: EventStream;
245+
180246
messageStore: MessageStore;
181247
dataStore: DataStore;
182-
eventLog: EventLog
248+
eventLog: EventLog;
183249
};

src/enums/dwn-interface-method.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ export enum DwnMethodName {
1616
Request = 'Request',
1717
Revoke = 'Revoke',
1818
Write = 'Write',
19-
Delete = 'Delete'
19+
Delete = 'Delete',
20+
Subscribe = 'Subscribe'
2021
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import type { GenericMessage } from '../types/message-types.js';
2+
import type { KeyValues } from '../types/query-types.js';
3+
import type { EventListener, EventStream, EventSubscription } from '../types/subscriptions.js';
4+
5+
import { EventEmitter } from 'events';
6+
7+
const EVENTS_LISTENER_CHANNEL = 'events';
8+
9+
export class EventEmitterStream implements EventStream {
10+
private eventEmitter: EventEmitter;
11+
private isOpen: boolean = false;
12+
13+
constructor() {
14+
// we capture the rejections and currently just log the errors that are produced
15+
this.eventEmitter = new EventEmitter({ captureRejections: true });
16+
this.eventEmitter.on('error', this.eventError);
17+
}
18+
19+
// we subscribe to the general `EventEmitter` error events with this handler.
20+
// this handler is also called when there is a caught error upon emitting an event from a handler.
21+
private eventError(error: any): void {
22+
console.error('event emitter error', error);
23+
};
24+
25+
async subscribe(id: string, listener: EventListener): Promise<EventSubscription> {
26+
this.eventEmitter.on(EVENTS_LISTENER_CHANNEL, listener);
27+
return {
28+
id,
29+
close: async (): Promise<void> => { this.eventEmitter.off(EVENTS_LISTENER_CHANNEL, listener); }
30+
};
31+
}
32+
33+
async open(): Promise<void> {
34+
this.isOpen = true;
35+
}
36+
37+
async close(): Promise<void> {
38+
this.isOpen = false;
39+
this.eventEmitter.removeAllListeners();
40+
}
41+
42+
emit(tenant: string, message: GenericMessage, indexes: KeyValues): void {
43+
if (!this.isOpen) {
44+
console.error('message emitted when EventEmitterStream is closed', tenant, message, indexes);
45+
return;
46+
}
47+
this.eventEmitter.emit(EVENTS_LISTENER_CHANNEL, tenant, message, indexes);
48+
}
49+
}

src/event-log/event-log-level.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { EventLog } from '../types/event-log.js';
2+
import type { EventStream } from '../types/subscriptions.js';
23
import type { ULIDFactory } from 'ulidx';
34
import type { Filter, KeyValues, PaginationCursor } from '../types/query-types.js';
45

@@ -14,6 +15,7 @@ type EventLogLevelConfig = {
1415
*/
1516
location?: string,
1617
createLevelDatabase?: typeof createLevelDatabase,
18+
eventStream?: EventStream,
1719
};
1820

1921
export class EventLogLevel implements EventLog {

0 commit comments

Comments
 (0)