@design.estate/dees-comms 1.0.31 → 1.1.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.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-comms',
6
- version: '1.0.31',
6
+ version: '1.1.0',
7
7
  description: 'A communications module for enabling DOM-based messaging and synchronization across browser tabs and workers.'
8
8
  };
9
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSwyQkFBMkI7SUFDakMsT0FBTyxFQUFFLFFBQVE7SUFDakIsV0FBVyxFQUFFLCtHQUErRztDQUM3SCxDQUFBIn0=
9
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiMDBfY29tbWl0aW5mb19kYXRhLmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vdHMvMDBfY29tbWl0aW5mb19kYXRhLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOztHQUVHO0FBQ0gsTUFBTSxDQUFDLE1BQU0sVUFBVSxHQUFHO0lBQ3hCLElBQUksRUFBRSwyQkFBMkI7SUFDakMsT0FBTyxFQUFFLE9BQU87SUFDaEIsV0FBVyxFQUFFLCtHQUErRztDQUM3SCxDQUFBIn0=
@@ -1,12 +1,42 @@
1
1
  import * as plugins from './dees-comms.plugins.js';
2
+ export interface IDeesCommsOptions {
3
+ /**
4
+ * The channel this instance talks on. Instances hear each other only when
5
+ * they use the same name, in any tab, frame or worker of the same origin.
6
+ * Defaults to `DeesComms.defaultChannelName`, the channel every instance
7
+ * used before names existed.
8
+ */
9
+ channelName?: string;
10
+ }
11
+ /**
12
+ * Receives the payload of one typed event, and the event itself.
13
+ */
14
+ export type TDeesCommsEventHandler<T extends plugins.typedrequestInterfaces.ITypedEvent<unknown>> = (payloadArg: T['payload'], eventArg: T) => void | Promise<void>;
15
+ export interface IDeesCommsSubscription {
16
+ /**
17
+ * Stops this subscription. An event that is being delivered right now does
18
+ * not reach it any more; calling it again does nothing.
19
+ */
20
+ unsubscribe(): void;
21
+ }
2
22
  /**
3
23
  * a comm class for client side communication between workers and tabs.
4
24
  */
5
25
  export declare class DeesComms {
6
- private broadcastChannel;
26
+ /**
27
+ * The channel of every instance that is not given a name. Code that talks
28
+ * to other code on it, like a service worker and its pages, keeps working
29
+ * unchanged; an application gives its own messages a name of its own.
30
+ */
31
+ static readonly defaultChannelName = "dees-comms";
32
+ readonly channelName: string;
33
+ private readonly channel;
34
+ private readonly eventHandlers;
35
+ private closed;
36
+ private closing;
7
37
  typedrouter: plugins.typedrequest.TypedRouter<plugins.typedrequestInterfaces.ITypedRequest>;
8
38
  typedtarget: plugins.typedrequest.TypedTarget;
9
- constructor();
39
+ constructor(optionsArg?: IDeesCommsOptions);
10
40
  /**
11
41
  * creates a typedrequest with this classes postMessage as postMethod
12
42
  */
@@ -19,4 +49,27 @@ export declare class DeesComms {
19
49
  * subscribe to messages
20
50
  */
21
51
  createTypedHandler<T extends plugins.typedrequestInterfaces.ITypedRequest>(methodArg: T['method'], handlerFunction: plugins.typedrequest.THandlerFunction<T>): Promise<void>;
52
+ /**
53
+ * Sends a typed event to every other instance on this channel and expects
54
+ * no answer. The instance that sends it does not receive it; any other
55
+ * instance on the same channel does, in this context or another one.
56
+ */
57
+ broadcast<T extends plugins.typedrequestInterfaces.ITypedEvent<unknown>>(nameArg: T['name'], payloadArg: T['payload']): Promise<void>;
58
+ /**
59
+ * Calls the handler for every typed event of that name another instance
60
+ * broadcasts on this channel. Handlers run in the order they subscribed; one
61
+ * that throws or rejects is reported and does not keep the others from
62
+ * running.
63
+ */
64
+ subscribe<T extends plugins.typedrequestInterfaces.ITypedEvent<unknown>>(nameArg: T['name'], handlerArg: TDeesCommsEventHandler<T>): IDeesCommsSubscription;
65
+ /**
66
+ * Closes the channel. Nothing is received from then on, every subscription
67
+ * ends, and sending, broadcasting or subscribing throws. A typed request
68
+ * still waiting for its response when the channel closes cannot receive it
69
+ * and ends with its own timeout. Closing again does nothing.
70
+ */
71
+ close(): Promise<void>;
72
+ private assertOpen;
73
+ private receive;
74
+ private dispatch;
22
75
  }
@@ -1,34 +1,84 @@
1
1
  import * as plugins from './dees-comms.plugins.js';
2
- let BroadcastChannel = globalThis.BroadcastChannel;
3
- if (!BroadcastChannel) {
4
- BroadcastChannel = plugins.BroadCastChannelPolyfill;
5
- }
2
+ /**
3
+ * Opens a channel that hands every message it receives to `receiveArg` as the
4
+ * message itself: the platform's BroadcastChannel delivers a MessageEvent, the
5
+ * polyfill delivers the message.
6
+ */
7
+ const openChannel = (nameArg, receiveArg) => {
8
+ if (typeof globalThis.BroadcastChannel === 'function') {
9
+ const channel = new globalThis.BroadcastChannel(nameArg);
10
+ channel.onmessage = (eventArg) => {
11
+ receiveArg(eventArg.data);
12
+ };
13
+ return {
14
+ post: async (messageArg) => {
15
+ channel.postMessage(messageArg);
16
+ },
17
+ close: async () => {
18
+ channel.close();
19
+ },
20
+ };
21
+ }
22
+ const channel = new plugins.BroadCastChannelPolyfill(nameArg);
23
+ channel.onmessage = (messageArg) => {
24
+ receiveArg(messageArg);
25
+ };
26
+ return {
27
+ post: (messageArg) => channel.postMessage(messageArg),
28
+ close: () => channel.close(),
29
+ };
30
+ };
31
+ const isObject = (valueArg) => typeof valueArg === 'object' && valueArg !== null;
32
+ // `Object.hasOwn` is younger than some of the browsers the polyfill serves
33
+ const hasOwn = (objectArg, keyArg) => Object.prototype.hasOwnProperty.call(objectArg, keyArg);
34
+ /**
35
+ * The event a received message carries: `null` for a message that is no
36
+ * event, `'malformed'` for an envelope without a well-formed event in it.
37
+ */
38
+ const eventOf = (messageArg) => {
39
+ if (!isObject(messageArg) || !hasOwn(messageArg, 'deesCommsEvent')) {
40
+ return null;
41
+ }
42
+ const event = messageArg.deesCommsEvent;
43
+ if (!isObject(event) ||
44
+ typeof event.name !== 'string' ||
45
+ typeof event.uniqueEventId !== 'string' ||
46
+ !hasOwn(event, 'payload')) {
47
+ return 'malformed';
48
+ }
49
+ return { name: event.name, uniqueEventId: event.uniqueEventId, payload: event.payload };
50
+ };
51
+ const createEventId = () => {
52
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
53
+ return Array.from(bytes, (byteArg) => byteArg.toString(16).padStart(2, '0')).join('');
54
+ };
6
55
  /**
7
56
  * a comm class for client side communication between workers and tabs.
8
57
  */
9
58
  export class DeesComms {
59
+ /**
60
+ * The channel of every instance that is not given a name. Code that talks
61
+ * to other code on it, like a service worker and its pages, keeps working
62
+ * unchanged; an application gives its own messages a name of its own.
63
+ */
64
+ static { this.defaultChannelName = 'dees-comms'; }
10
65
  // receiving messages
11
- constructor() {
12
- this.broadcastChannel = new BroadcastChannel('dees-comms');
66
+ constructor(optionsArg = {}) {
67
+ this.eventHandlers = new Map();
68
+ this.closed = false;
69
+ this.closing = null;
13
70
  // sending messages
14
71
  this.typedrouter = new plugins.typedrequest.TypedRouter();
15
72
  this.typedtarget = new plugins.typedrequest.TypedTarget({
16
73
  postMethodWithTypedRouter: async (messageArg) => {
17
- this.postMessage(messageArg);
74
+ await this.postMessage(messageArg);
18
75
  },
19
76
  typedRouterRef: this.typedrouter,
20
77
  });
21
- this.broadcastChannel.onmessage = async (eventArg) => {
22
- const message = eventArg.method ? eventArg : eventArg.data;
23
- // Skip hooks for broadcast-received messages to prevent infinite loops
24
- // when traffic logging hooks are set up globally
25
- const response = await this.typedrouter.routeAndAddResponse(message, {
26
- skipHooks: true,
27
- });
28
- if (response && !response.error) {
29
- this.postMessage(response);
30
- }
31
- };
78
+ this.channelName = optionsArg.channelName ?? DeesComms.defaultChannelName;
79
+ this.channel = openChannel(this.channelName, (messageArg) => {
80
+ void this.receive(messageArg);
81
+ });
32
82
  }
33
83
  /**
34
84
  * creates a typedrequest with this classes postMessage as postMethod
@@ -41,7 +91,8 @@ export class DeesComms {
41
91
  * posts a typedrequestmessage
42
92
  */
43
93
  async postMessage(messageArg) {
44
- this.broadcastChannel.postMessage(messageArg);
94
+ this.assertOpen();
95
+ await this.channel.post(messageArg);
45
96
  }
46
97
  /**
47
98
  * subscribe to messages
@@ -49,5 +100,110 @@ export class DeesComms {
49
100
  async createTypedHandler(methodArg, handlerFunction) {
50
101
  this.typedrouter.addTypedHandler(new plugins.typedrequest.TypedHandler(methodArg, handlerFunction));
51
102
  }
103
+ /**
104
+ * Sends a typed event to every other instance on this channel and expects
105
+ * no answer. The instance that sends it does not receive it; any other
106
+ * instance on the same channel does, in this context or another one.
107
+ */
108
+ async broadcast(nameArg, payloadArg) {
109
+ const message = {
110
+ deesCommsEvent: {
111
+ name: nameArg,
112
+ uniqueEventId: createEventId(),
113
+ payload: payloadArg,
114
+ },
115
+ };
116
+ await this.postMessage(message);
117
+ }
118
+ /**
119
+ * Calls the handler for every typed event of that name another instance
120
+ * broadcasts on this channel. Handlers run in the order they subscribed; one
121
+ * that throws or rejects is reported and does not keep the others from
122
+ * running.
123
+ */
124
+ subscribe(nameArg, handlerArg) {
125
+ this.assertOpen();
126
+ // the name is the contract: what arrives under it is taken as its type,
127
+ // as a typed handler takes the request of its method
128
+ const handler = (payloadArg, eventArg) => handlerArg(payloadArg, eventArg);
129
+ let handlers = this.eventHandlers.get(nameArg);
130
+ if (!handlers) {
131
+ handlers = new Set();
132
+ this.eventHandlers.set(nameArg, handlers);
133
+ }
134
+ handlers.add(handler);
135
+ return {
136
+ unsubscribe: () => {
137
+ const current = this.eventHandlers.get(nameArg);
138
+ if (!current) {
139
+ return;
140
+ }
141
+ current.delete(handler);
142
+ if (current.size === 0) {
143
+ this.eventHandlers.delete(nameArg);
144
+ }
145
+ },
146
+ };
147
+ }
148
+ /**
149
+ * Closes the channel. Nothing is received from then on, every subscription
150
+ * ends, and sending, broadcasting or subscribing throws. A typed request
151
+ * still waiting for its response when the channel closes cannot receive it
152
+ * and ends with its own timeout. Closing again does nothing.
153
+ */
154
+ async close() {
155
+ if (!this.closing) {
156
+ this.closed = true;
157
+ this.eventHandlers.clear();
158
+ this.closing = this.channel.close();
159
+ }
160
+ await this.closing;
161
+ }
162
+ assertOpen() {
163
+ if (this.closed) {
164
+ throw new Error(`DeesComms channel "${this.channelName}" is closed`);
165
+ }
166
+ }
167
+ async receive(messageArg) {
168
+ if (this.closed) {
169
+ return;
170
+ }
171
+ const event = eventOf(messageArg);
172
+ if (event === 'malformed') {
173
+ console.log('DeesComms received a malformed event');
174
+ return;
175
+ }
176
+ if (event) {
177
+ this.dispatch(event);
178
+ return;
179
+ }
180
+ // Skip hooks for broadcast-received messages to prevent infinite loops
181
+ // when traffic logging hooks are set up globally. The router validates
182
+ // the shape of what it is given.
183
+ const response = await this.typedrouter.routeAndAddResponse(messageArg, { skipHooks: true });
184
+ if (response && !response.error && !this.closed) {
185
+ await this.postMessage(response);
186
+ }
187
+ }
188
+ dispatch(eventArg) {
189
+ const handlers = this.eventHandlers.get(eventArg.name);
190
+ if (!handlers) {
191
+ return;
192
+ }
193
+ for (const handler of [...handlers]) {
194
+ // a handler that closed the channel ends the delivery, and one an
195
+ // earlier handler unsubscribed is not called any more
196
+ if (this.closed) {
197
+ return;
198
+ }
199
+ if (!handlers.has(handler)) {
200
+ continue;
201
+ }
202
+ void (async () => handler(eventArg.payload, eventArg))().catch((errorArg) => {
203
+ // the event's name and the failure, never its payload
204
+ console.error(`DeesComms: a handler of the event "${eventArg.name}" failed`, errorArg);
205
+ });
206
+ }
207
+ }
52
208
  }
53
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVlcy1jb21tcy5jbGFzc2VzLmRlZXNjb21tcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2RlZXMtY29tbXMuY2xhc3Nlcy5kZWVzY29tbXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSx5QkFBeUIsQ0FBQztBQUVuRCxJQUFJLGdCQUFnQixHQUFHLFVBQVUsQ0FBQyxnQkFBZ0IsQ0FBQztBQUNuRCxJQUFJLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQztJQUN0QixnQkFBZ0IsR0FBRyxPQUFPLENBQUMsd0JBQStCLENBQUM7QUFDN0QsQ0FBQztBQUVEOztHQUVHO0FBQ0gsTUFBTSxPQUFPLFNBQVM7SUFZcEIscUJBQXFCO0lBQ3JCO1FBWlEscUJBQWdCLEdBQUcsSUFBSSxnQkFBZ0IsQ0FBQyxZQUFZLENBQUMsQ0FBQztRQUU5RCxtQkFBbUI7UUFDWixnQkFBVyxHQUFHLElBQUksT0FBTyxDQUFDLFlBQVksQ0FBQyxXQUFXLEVBQUUsQ0FBQztRQUNyRCxnQkFBVyxHQUFHLElBQUksT0FBTyxDQUFDLFlBQVksQ0FBQyxXQUFXLENBQUM7WUFDeEQseUJBQXlCLEVBQUUsS0FBSyxFQUFFLFVBQVUsRUFBRSxFQUFFO2dCQUM5QyxJQUFJLENBQUMsV0FBVyxDQUFDLFVBQVUsQ0FBQyxDQUFDO1lBQy9CLENBQUM7WUFDRCxjQUFjLEVBQUUsSUFBSSxDQUFDLFdBQVc7U0FDakMsQ0FBQyxDQUFDO1FBSUQsSUFBSSxDQUFDLGdCQUFnQixDQUFDLFNBQVMsR0FBRyxLQUFLLEVBQUUsUUFBUSxFQUFFLEVBQUU7WUFDbkQsTUFBTSxPQUFPLEdBQUksUUFBZ0IsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQztZQUNwRSx1RUFBdUU7WUFDdkUsaURBQWlEO1lBQ2pELE1BQU0sUUFBUSxHQUFHLE1BQU0sSUFBSSxDQUFDLFdBQVcsQ0FBQyxtQkFBbUIsQ0FBQyxPQUFPLEVBQUU7Z0JBQ25FLFNBQVMsRUFBRSxJQUFJO2FBQ2hCLENBQUMsQ0FBQztZQUNILElBQUksUUFBUSxJQUFJLENBQUMsUUFBUSxDQUFDLEtBQUssRUFBRSxDQUFDO2dCQUNoQyxJQUFJLENBQUMsV0FBVyxDQUFDLFFBQVEsQ0FBQyxDQUFDO1lBQzdCLENBQUM7UUFDSCxDQUFDLENBQUM7SUFDSixDQUFDO0lBRUQ7O09BRUc7SUFDSSxrQkFBa0IsQ0FDdkIsVUFBdUI7UUFFdkIsTUFBTSxZQUFZLEdBQUcsSUFBSSxPQUFPLENBQUMsWUFBWSxDQUFDLFlBQVksQ0FBQyxJQUFJLENBQUMsV0FBVyxFQUFFLFVBQVUsQ0FBQyxDQUFDO1FBQ3pGLE9BQU8sWUFBWSxDQUFDO0lBQ3RCLENBQUM7SUFFRDs7T0FFRztJQUNJLEtBQUssQ0FBQyxXQUFXLENBQ3RCLFVBQWE7UUFFYixJQUFJLENBQUMsZ0JBQWdCLENBQUMsV0FBVyxDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQ2hELENBQUM7SUFFRDs7T0FFRztJQUNJLEtBQUssQ0FBQyxrQkFBa0IsQ0FDN0IsU0FBc0IsRUFDdEIsZUFBeUQ7UUFFekQsSUFBSSxDQUFDLFdBQVcsQ0FBQyxlQUFlLENBQzlCLElBQUksT0FBTyxDQUFDLFlBQVksQ0FBQyxZQUFZLENBQUksU0FBUyxFQUFFLGVBQWUsQ0FBQyxDQUNyRSxDQUFDO0lBQ0osQ0FBQztDQUNGIn0=
209
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVlcy1jb21tcy5jbGFzc2VzLmRlZXNjb21tcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2RlZXMtY29tbXMuY2xhc3Nlcy5kZWVzY29tbXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSx5QkFBeUIsQ0FBQztBQWtEbkQ7Ozs7R0FJRztBQUNILE1BQU0sV0FBVyxHQUFHLENBQ2xCLE9BQWUsRUFDZixVQUF5QyxFQUN0QixFQUFFO0lBQ3JCLElBQUksT0FBTyxVQUFVLENBQUMsZ0JBQWdCLEtBQUssVUFBVSxFQUFFLENBQUM7UUFDdEQsTUFBTSxPQUFPLEdBQUcsSUFBSSxVQUFVLENBQUMsZ0JBQWdCLENBQUMsT0FBTyxDQUFDLENBQUM7UUFDekQsT0FBTyxDQUFDLFNBQVMsR0FBRyxDQUFDLFFBQStCLEVBQUUsRUFBRTtZQUN0RCxVQUFVLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQyxDQUFDO1FBQzVCLENBQUMsQ0FBQztRQUNGLE9BQU87WUFDTCxJQUFJLEVBQUUsS0FBSyxFQUFFLFVBQVUsRUFBRSxFQUFFO2dCQUN6QixPQUFPLENBQUMsV0FBVyxDQUFDLFVBQVUsQ0FBQyxDQUFDO1lBQ2xDLENBQUM7WUFDRCxLQUFLLEVBQUUsS0FBSyxJQUFJLEVBQUU7Z0JBQ2hCLE9BQU8sQ0FBQyxLQUFLLEVBQUUsQ0FBQztZQUNsQixDQUFDO1NBQ0YsQ0FBQztJQUNKLENBQUM7SUFDRCxNQUFNLE9BQU8sR0FBRyxJQUFJLE9BQU8sQ0FBQyx3QkFBd0IsQ0FBVSxPQUFPLENBQUMsQ0FBQztJQUN2RSxPQUFPLENBQUMsU0FBUyxHQUFHLENBQUMsVUFBbUIsRUFBRSxFQUFFO1FBQzFDLFVBQVUsQ0FBQyxVQUFVLENBQUMsQ0FBQztJQUN6QixDQUFDLENBQUM7SUFDRixPQUFPO1FBQ0wsSUFBSSxFQUFFLENBQUMsVUFBVSxFQUFFLEVBQUUsQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLFVBQVUsQ0FB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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design.estate/dees-comms",
3
- "version": "1.0.31",
3
+ "version": "1.1.0",
4
4
  "private": false,
5
5
  "description": "A communications module for enabling DOM-based messaging and synchronization across browser tabs and workers.",
6
6
  "main": "dist_ts/index.js",
@@ -9,15 +9,15 @@
9
9
  "author": "Lossless GmbH",
10
10
  "license": "MIT",
11
11
  "devDependencies": {
12
- "@git.zone/tsbuild": "^4.4.2",
13
- "@git.zone/tsbundle": "^2.11.3",
14
- "@git.zone/tsrun": "^2.0.6",
15
- "@git.zone/tstest": "^3.6.7",
16
- "@types/node": "^26.1.1"
12
+ "@git.zone/tsbuild": "^4.5.2",
13
+ "@git.zone/tsbundle": "^2.14.0",
14
+ "@git.zone/tsrun": "^3.0.0",
15
+ "@git.zone/tstest": "^6.2.0",
16
+ "@types/node": "^26.6.1"
17
17
  },
18
18
  "dependencies": {
19
- "@api.global/typedrequest": "^3.8.0",
20
- "@api.global/typedrequest-interfaces": "^4.0.0",
19
+ "@api.global/typedrequest": "^8.0.3",
20
+ "@api.global/typedrequest-interfaces": "^7.1.0",
21
21
  "@push.rocks/smartdelay": "^3.0.5",
22
22
  "broadcast-channel": "^7.2.0"
23
23
  },
package/readme.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @design.estate/dees-comms
2
2
 
3
- a comms module for communicating within the DOM
3
+ Typed messaging between the tabs, frames and workers of one origin over a BroadcastChannel: typed requests that expect an answer, and typed events that do not.
4
4
 
5
5
  ## Issue Reporting and Security
6
6
 
@@ -8,113 +8,112 @@ For reporting bugs, issues, or security vulnerabilities, please visit [community
8
8
 
9
9
  ## Install
10
10
 
11
- To integrate `@design.estate/dees-comms` into your project, run the following command in your terminal:
12
-
13
11
  ```bash
14
12
  pnpm add @design.estate/dees-comms
15
13
  ```
16
14
 
17
- This will add the package to your project dependencies.
18
-
19
15
  ## Usage
20
16
 
21
- `@design.estate/dees-comms` facilitates communication within the DOM, especially between various workers and tabs in client-side JavaScript. Using TypeScript and the provided ESM syntax will enhance your development experience with better intellisense support and type safety.
22
-
23
- ### Setting Up
17
+ ### Channels
24
18
 
25
- First, make sure to import the module in your TypeScript file:
19
+ Every `DeesComms` instance talks on one BroadcastChannel. Instances hear each other only when they use the same channel name, in any tab, frame or worker of the same origin.
26
20
 
27
21
  ```typescript
28
22
  import { DeesComms } from '@design.estate/dees-comms';
29
23
 
30
- interface IMyMessage {
31
- method: 'myMethod';
32
- request: {
33
- someData: string;
34
- };
35
- response: {
36
- reply: string;
37
- };
38
- }
39
- ```
40
-
41
- ### Initializing the Communication Module
24
+ // the default channel, 'dees-comms'
25
+ const comms = new DeesComms();
42
26
 
43
- Create an instance of the `DeesComms` class to start using the module. This instance will act as the central communication hub in your application.
27
+ // a channel of this application's own
28
+ const appComms = new DeesComms({ channelName: 'my-app' });
44
29
 
45
- ```typescript
46
- const myDeesComms = new DeesComms();
30
+ console.log(DeesComms.defaultChannelName); // 'dees-comms'
31
+ console.log(appComms.channelName); // 'my-app'
47
32
  ```
48
33
 
49
- ### Sending Messages
34
+ An instance created without a name uses `DeesComms.defaultChannelName`, the channel every instance used before names existed. Other packages talk on it: `DomTools` of `@design.estate/dees-domtools` opens an instance on it, and the service worker of `@api.global/typedserver` exchanges its messages with its pages there. Give your application's messages a channel of their own, so that they neither reach nor disturb that traffic.
50
35
 
51
- To send messages, you'll need to create a typed request. Let's assume we are sending a message that requires a response:
36
+ The platform's `BroadcastChannel` is used wherever there is one (browsers, workers, Node.js); elsewhere the [`broadcast-channel`](https://www.npmjs.com/package/broadcast-channel) package stands in for it.
52
37
 
53
- ```typescript
54
- // Creating a typed request
55
- const myTypedRequest = myDeesComms.createTypedRequest<IMyMessage>('myMethod');
56
-
57
- // Firing the request
58
- const response = await myTypedRequest.fire({
59
- someData: 'Hello, World!',
60
- });
61
- console.log('Received response:', response.reply);
62
- ```
63
-
64
- ### Receiving and Handling Messages
38
+ ### Typed requests
65
39
 
66
- To handle incoming messages, you need to define handlers for the types of messages you expect to receive.
40
+ A typed request goes to every other instance on the channel; an instance with a handler for its method answers it.
67
41
 
68
42
  ```typescript
69
- // Define the handler for the 'myMethod' message
70
- myDeesComms.createTypedHandler<IMyMessage>('myMethod', async (request) => {
71
- console.log('Message received:', request.someData);
72
-
73
- // Process the incoming message...
74
-
75
- // Return a response (if needed)
76
- return {
77
- reply: 'Message received loud and clear!'
43
+ import { DeesComms } from '@design.estate/dees-comms';
44
+ import type { ITypedRequest } from '@api.global/typedrequest-interfaces';
45
+
46
+ interface IGreetRequest extends ITypedRequest {
47
+ method: 'greet';
48
+ request: {
49
+ name: string;
50
+ };
51
+ response: {
52
+ reply: string;
78
53
  };
54
+ }
55
+
56
+ // in one tab or worker
57
+ const answering = new DeesComms({ channelName: 'my-app' });
58
+ answering.createTypedHandler<IGreetRequest>('greet', async (request) => {
59
+ return { reply: `Hello ${request.name}, nice to meet you!` };
79
60
  });
80
- ```
81
61
 
82
- ### Communication Between Tabs or Workers
62
+ // in another one
63
+ const asking = new DeesComms({ channelName: 'my-app' });
64
+ const response = await asking.createTypedRequest<IGreetRequest>('greet').fire({ name: 'Alice' });
65
+ console.log(response.reply); // 'Hello Alice, nice to meet you!'
66
+ ```
83
67
 
84
- `@design.estate/dees-comms` uses the Broadcast Channel API and a polyfill for environments where it's not available, enabling seamless communication between different contexts like tabs, iframes, or web workers.
68
+ `postMessage()` posts a typed request object as it is; `typedrouter` and `typedtarget` are the `@api.global/typedrequest` router and target the instance routes with.
85
69
 
86
- ### Complete Example
70
+ ### Typed events
87
71
 
88
- Putting it all together, let's create a small application that sends a message and listens for a response:
72
+ A typed event is sent to every other instance on the channel and expects no answer. It is declared as an `ITypedEvent` of `@api.global/typedrequest-interfaces`: its `name` is the contract, its `payload` is what travels.
89
73
 
90
74
  ```typescript
91
75
  import { DeesComms } from '@design.estate/dees-comms';
76
+ import type { ITypedEvent } from '@api.global/typedrequest-interfaces';
92
77
 
93
- interface IGreetRequest {
94
- method: 'greet';
95
- request: {
96
- name: string;
97
- };
98
- response: {
99
- reply: string;
78
+ interface ISettingsChangedEvent extends ITypedEvent<{ section: string }> {
79
+ name: 'settingsChanged';
80
+ uniqueEventId: string;
81
+ payload: {
82
+ section: string;
100
83
  };
101
84
  }
102
85
 
103
- const appDeesComms = new DeesComms();
104
-
105
- // Setting up the listener
106
- appDeesComms.createTypedHandler<IGreetRequest>('greet', async (request) => {
107
- console.log(`Greetings received from ${request.name}`);
108
- return { reply: `Hello ${request.name}, nice to meet you!` };
86
+ // in every tab that has to follow
87
+ const listening = new DeesComms({ channelName: 'my-app' });
88
+ const subscription = listening.subscribe<ISettingsChangedEvent>('settingsChanged', (payload, event) => {
89
+ console.log(`settings changed in ${payload.section}`, event.uniqueEventId);
109
90
  });
110
91
 
111
- // Sending a greeting message
112
- const greetRequest = appDeesComms.createTypedRequest<IGreetRequest>('greet');
113
- const response = await greetRequest.fire({ name: 'Alice' });
114
- console.log(response.reply); // Expected output: "Hello Alice, nice to meet you!"
92
+ // in the tab where it happened
93
+ const telling = new DeesComms({ channelName: 'my-app' });
94
+ await telling.broadcast<ISettingsChangedEvent>('settingsChanged', { section: 'profile' });
95
+
96
+ // later
97
+ subscription.unsubscribe();
115
98
  ```
116
99
 
117
- This module provides a powerful and flexible way to handle communications within the DOM, leveraging modern JavaScript features and patterns. The usage of TypeScript ensures type safety and enhances code understandability and maintainability.
100
+ - The instance that broadcasts an event does not receive it; every other instance on the channel does, in the same context or another one.
101
+ - `broadcast()` gives every event a new `uniqueEventId`.
102
+ - Handlers of one name run in the order they subscribed. A handler that throws or rejects is reported with `console.error`, naming the event and the error but never the payload, and does not keep the others from running. A handler that an earlier one unsubscribes during the same event is not called for it.
103
+ - Events and typed requests stay apart: an event never reaches a typed handler, and a typed request never reaches a subscription, whatever their names.
104
+ - An instance of 1.0.32 routes every message it receives as a typed request; on the platform's own BroadcastChannel, `@api.global/typedrequest` 8 then logs `TypedRouter received a malformed request DTO` for every event. That is one more reason to broadcast on a channel of your application's own.
105
+
106
+ ### Closing
107
+
108
+ ```typescript
109
+ await comms.close();
110
+ ```
111
+
112
+ A closed instance receives nothing: its subscriptions end and its typed handlers are no longer asked. `postMessage()`, `broadcast()` and the typed requests it fires reject, and `subscribe()` throws. A typed request that is still waiting for its response when the channel closes cannot receive it and ends with its own timeout. Closing again does nothing. An open channel keeps a Node.js process alive, so close every instance a test or a script opens.
113
+
114
+ ### What travels
115
+
116
+ Requests, responses and events are copied with the structured clone algorithm, so they carry data, not functions or class instances. A BroadcastChannel is confined to its origin, but every script of that origin can listen on a channel whose name it knows: send no secrets, and nothing that identifies a person, on it.
118
117
 
119
118
  ## License and Legal Information
120
119
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-comms',
6
- version: '1.0.31',
6
+ version: '1.1.0',
7
7
  description: 'A communications module for enabling DOM-based messaging and synchronization across browser tabs and workers.'
8
8
  }