@design.estate/dees-comms 1.0.32 → 1.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.
@@ -3,7 +3,7 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-comms',
6
- version: '1.0.32',
6
+ version: '1.1.1',
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,43 @@
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 hasTypedHandlers;
36
+ private closed;
37
+ private closing;
7
38
  typedrouter: plugins.typedrequest.TypedRouter<plugins.typedrequestInterfaces.ITypedRequest>;
8
39
  typedtarget: plugins.typedrequest.TypedTarget;
9
- constructor();
40
+ constructor(optionsArg?: IDeesCommsOptions);
10
41
  /**
11
42
  * creates a typedrequest with this classes postMessage as postMethod
12
43
  */
@@ -19,4 +50,33 @@ export declare class DeesComms {
19
50
  * subscribe to messages
20
51
  */
21
52
  createTypedHandler<T extends plugins.typedrequestInterfaces.ITypedRequest>(methodArg: T['method'], handlerFunction: plugins.typedrequest.THandlerFunction<T>): Promise<void>;
53
+ /**
54
+ * Sends a typed event to every other instance on this channel and expects
55
+ * no answer. The instance that sends it does not receive it; any other
56
+ * instance on the same channel does, in this context or another one.
57
+ */
58
+ broadcast<T extends plugins.typedrequestInterfaces.ITypedEvent<unknown>>(nameArg: T['name'], payloadArg: T['payload']): Promise<void>;
59
+ /**
60
+ * Calls the handler for every typed event of that name another instance
61
+ * broadcasts on this channel. Handlers run in the order they subscribed; one
62
+ * that throws or rejects is reported and does not keep the others from
63
+ * running.
64
+ */
65
+ subscribe<T extends plugins.typedrequestInterfaces.ITypedEvent<unknown>>(nameArg: T['name'], handlerArg: TDeesCommsEventHandler<T>): IDeesCommsSubscription;
66
+ /**
67
+ * Closes the channel. Nothing is received from then on, every subscription
68
+ * ends, and sending, broadcasting or subscribing throws. A typed request
69
+ * still waiting for its response when the channel closes cannot receive it
70
+ * and ends with its own timeout. Closing again does nothing.
71
+ */
72
+ close(): Promise<void>;
73
+ /**
74
+ * In Node.js an instance keeps its process alive while something listens on
75
+ * it, as a MessagePort with a message listener does: a subscription, or a
76
+ * typed handler it created. One nobody listens on lets the process end.
77
+ */
78
+ private holdProcessWhileListened;
79
+ private assertOpen;
80
+ private receive;
81
+ private dispatch;
22
82
  }
@@ -1,34 +1,103 @@
1
1
  import * as plugins from './dees-comms.plugins.js';
2
- let BroadcastChannel = globalThis.BroadcastChannel;
3
- if (!BroadcastChannel) {
4
- BroadcastChannel = plugins.BroadCastChannelPolyfill;
5
- }
2
+ const isProcessHolding = (channelArg) => 'ref' in channelArg &&
3
+ typeof channelArg.ref === 'function' &&
4
+ 'unref' in channelArg &&
5
+ typeof channelArg.unref === 'function';
6
+ /**
7
+ * Opens a channel that hands every message it receives to `receiveArg` as the
8
+ * message itself: the platform's BroadcastChannel delivers a MessageEvent, the
9
+ * polyfill delivers the message.
10
+ */
11
+ const openChannel = (nameArg, receiveArg) => {
12
+ if (typeof globalThis.BroadcastChannel === 'function') {
13
+ const channel = new globalThis.BroadcastChannel(nameArg);
14
+ channel.onmessage = (eventArg) => {
15
+ receiveArg(eventArg.data);
16
+ };
17
+ const processHolding = isProcessHolding(channel) ? channel : null;
18
+ // an open channel alone does not keep a Node.js process running
19
+ processHolding?.unref();
20
+ return {
21
+ post: async (messageArg) => {
22
+ channel.postMessage(messageArg);
23
+ },
24
+ close: async () => {
25
+ channel.close();
26
+ },
27
+ holdProcess: (holdArg) => {
28
+ if (holdArg) {
29
+ processHolding?.ref();
30
+ }
31
+ else {
32
+ processHolding?.unref();
33
+ }
34
+ },
35
+ };
36
+ }
37
+ const channel = new plugins.BroadCastChannelPolyfill(nameArg);
38
+ channel.onmessage = (messageArg) => {
39
+ receiveArg(messageArg);
40
+ };
41
+ return {
42
+ post: (messageArg) => channel.postMessage(messageArg),
43
+ close: () => channel.close(),
44
+ // the polyfill stands in only where the platform has no channel, which
45
+ // Node.js always has
46
+ holdProcess: () => undefined,
47
+ };
48
+ };
49
+ const isObject = (valueArg) => typeof valueArg === 'object' && valueArg !== null;
50
+ // `Object.hasOwn` is younger than some of the browsers the polyfill serves
51
+ const hasOwn = (objectArg, keyArg) => Object.prototype.hasOwnProperty.call(objectArg, keyArg);
52
+ /**
53
+ * The event a received message carries: `null` for a message that is no
54
+ * event, `'malformed'` for an envelope without a well-formed event in it.
55
+ */
56
+ const eventOf = (messageArg) => {
57
+ if (!isObject(messageArg) || !hasOwn(messageArg, 'deesCommsEvent')) {
58
+ return null;
59
+ }
60
+ const event = messageArg.deesCommsEvent;
61
+ if (!isObject(event) ||
62
+ typeof event.name !== 'string' ||
63
+ typeof event.uniqueEventId !== 'string' ||
64
+ !hasOwn(event, 'payload')) {
65
+ return 'malformed';
66
+ }
67
+ return { name: event.name, uniqueEventId: event.uniqueEventId, payload: event.payload };
68
+ };
69
+ const createEventId = () => {
70
+ const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
71
+ return Array.from(bytes, (byteArg) => byteArg.toString(16).padStart(2, '0')).join('');
72
+ };
6
73
  /**
7
74
  * a comm class for client side communication between workers and tabs.
8
75
  */
9
76
  export class DeesComms {
77
+ /**
78
+ * The channel of every instance that is not given a name. Code that talks
79
+ * to other code on it, like a service worker and its pages, keeps working
80
+ * unchanged; an application gives its own messages a name of its own.
81
+ */
82
+ static { this.defaultChannelName = 'dees-comms'; }
10
83
  // receiving messages
11
- constructor() {
12
- this.broadcastChannel = new BroadcastChannel('dees-comms');
84
+ constructor(optionsArg = {}) {
85
+ this.eventHandlers = new Map();
86
+ this.hasTypedHandlers = false;
87
+ this.closed = false;
88
+ this.closing = null;
13
89
  // sending messages
14
90
  this.typedrouter = new plugins.typedrequest.TypedRouter();
15
91
  this.typedtarget = new plugins.typedrequest.TypedTarget({
16
92
  postMethodWithTypedRouter: async (messageArg) => {
17
- this.postMessage(messageArg);
93
+ await this.postMessage(messageArg);
18
94
  },
19
95
  typedRouterRef: this.typedrouter,
20
96
  });
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
- };
97
+ this.channelName = optionsArg.channelName ?? DeesComms.defaultChannelName;
98
+ this.channel = openChannel(this.channelName, (messageArg) => {
99
+ void this.receive(messageArg);
100
+ });
32
101
  }
33
102
  /**
34
103
  * creates a typedrequest with this classes postMessage as postMethod
@@ -41,13 +110,134 @@ export class DeesComms {
41
110
  * posts a typedrequestmessage
42
111
  */
43
112
  async postMessage(messageArg) {
44
- this.broadcastChannel.postMessage(messageArg);
113
+ this.assertOpen();
114
+ await this.channel.post(messageArg);
45
115
  }
46
116
  /**
47
117
  * subscribe to messages
48
118
  */
49
119
  async createTypedHandler(methodArg, handlerFunction) {
50
120
  this.typedrouter.addTypedHandler(new plugins.typedrequest.TypedHandler(methodArg, handlerFunction));
121
+ this.hasTypedHandlers = true;
122
+ this.holdProcessWhileListened();
123
+ }
124
+ /**
125
+ * Sends a typed event to every other instance on this channel and expects
126
+ * no answer. The instance that sends it does not receive it; any other
127
+ * instance on the same channel does, in this context or another one.
128
+ */
129
+ async broadcast(nameArg, payloadArg) {
130
+ const message = {
131
+ deesCommsEvent: {
132
+ name: nameArg,
133
+ uniqueEventId: createEventId(),
134
+ payload: payloadArg,
135
+ },
136
+ };
137
+ await this.postMessage(message);
138
+ }
139
+ /**
140
+ * Calls the handler for every typed event of that name another instance
141
+ * broadcasts on this channel. Handlers run in the order they subscribed; one
142
+ * that throws or rejects is reported and does not keep the others from
143
+ * running.
144
+ */
145
+ subscribe(nameArg, handlerArg) {
146
+ this.assertOpen();
147
+ // the name is the contract: what arrives under it is taken as its type,
148
+ // as a typed handler takes the request of its method
149
+ const handler = (payloadArg, eventArg) => handlerArg(payloadArg, eventArg);
150
+ let handlers = this.eventHandlers.get(nameArg);
151
+ if (!handlers) {
152
+ handlers = new Set();
153
+ this.eventHandlers.set(nameArg, handlers);
154
+ }
155
+ handlers.add(handler);
156
+ this.holdProcessWhileListened();
157
+ return {
158
+ unsubscribe: () => {
159
+ const current = this.eventHandlers.get(nameArg);
160
+ if (!current) {
161
+ return;
162
+ }
163
+ current.delete(handler);
164
+ if (current.size === 0) {
165
+ this.eventHandlers.delete(nameArg);
166
+ this.holdProcessWhileListened();
167
+ }
168
+ },
169
+ };
170
+ }
171
+ /**
172
+ * Closes the channel. Nothing is received from then on, every subscription
173
+ * ends, and sending, broadcasting or subscribing throws. A typed request
174
+ * still waiting for its response when the channel closes cannot receive it
175
+ * and ends with its own timeout. Closing again does nothing.
176
+ */
177
+ async close() {
178
+ if (!this.closing) {
179
+ this.closed = true;
180
+ this.eventHandlers.clear();
181
+ this.closing = this.channel.close();
182
+ }
183
+ await this.closing;
184
+ }
185
+ /**
186
+ * In Node.js an instance keeps its process alive while something listens on
187
+ * it, as a MessagePort with a message listener does: a subscription, or a
188
+ * typed handler it created. One nobody listens on lets the process end.
189
+ */
190
+ holdProcessWhileListened() {
191
+ if (this.closed) {
192
+ return;
193
+ }
194
+ this.channel.holdProcess(this.hasTypedHandlers || this.eventHandlers.size > 0);
195
+ }
196
+ assertOpen() {
197
+ if (this.closed) {
198
+ throw new Error(`DeesComms channel "${this.channelName}" is closed`);
199
+ }
200
+ }
201
+ async receive(messageArg) {
202
+ if (this.closed) {
203
+ return;
204
+ }
205
+ const event = eventOf(messageArg);
206
+ if (event === 'malformed') {
207
+ console.log('DeesComms received a malformed event');
208
+ return;
209
+ }
210
+ if (event) {
211
+ this.dispatch(event);
212
+ return;
213
+ }
214
+ // Skip hooks for broadcast-received messages to prevent infinite loops
215
+ // when traffic logging hooks are set up globally. The router validates
216
+ // the shape of what it is given.
217
+ const response = await this.typedrouter.routeAndAddResponse(messageArg, { skipHooks: true });
218
+ if (response && !response.error && !this.closed) {
219
+ await this.postMessage(response);
220
+ }
221
+ }
222
+ dispatch(eventArg) {
223
+ const handlers = this.eventHandlers.get(eventArg.name);
224
+ if (!handlers) {
225
+ return;
226
+ }
227
+ for (const handler of [...handlers]) {
228
+ // a handler that closed the channel ends the delivery, and one an
229
+ // earlier handler unsubscribed is not called any more
230
+ if (this.closed) {
231
+ return;
232
+ }
233
+ if (!handlers.has(handler)) {
234
+ continue;
235
+ }
236
+ void (async () => handler(eventArg.payload, eventArg))().catch((errorArg) => {
237
+ // the event's name and the failure, never its payload
238
+ console.error(`DeesComms: a handler of the event "${eventArg.name}" failed`, errorArg);
239
+ });
240
+ }
51
241
  }
52
242
  }
53
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVlcy1jb21tcy5jbGFzc2VzLmRlZXNjb21tcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2RlZXMtY29tbXMuY2xhc3Nlcy5kZWVzY29tbXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSx5QkFBeUIsQ0FBQztBQUVuRCxJQUFJLGdCQUFnQixHQUFHLFVBQVUsQ0FBQyxnQkFBZ0IsQ0FBQztBQUNuRCxJQUFJLENBQUMsZ0JBQWdCLEVBQUUsQ0FBQztJQUN0QixnQkFBZ0IsR0FBRyxPQUFPLENBQUMsd0JBQStCLENBQUM7QUFDN0QsQ0FBQztBQUVEOztHQUVHO0FBQ0gsTUFBTSxPQUFPLFNBQVM7SUFZcEIscUJBQXFCO0lBQ3JCO1FBWlEscUJBQWdCLEdBQUcsSUFBSSxnQkFBZ0IsQ0FBQyxZQUFZLENBQUMsQ0FBQztRQUU5RCxtQkFBbUI7UUFDWixnQkFBVyxHQUFHLElBQUksT0FBTyxDQUFDLFlBQVksQ0FBQyxXQUFXLEVBQUUsQ0FBQztRQUNyRCxnQkFBVyxHQUFHLElBQUksT0FBTyxDQUFDLFlBQVksQ0FBQyxXQUFXLENBQUM7WUFDeEQseUJBQXlCLEVBQUUsS0FBSyxFQUFFLFVBQVUsRUFBRSxFQUFFO2dCQUM5QyxJQUFJLENBQUMsV0FBVyxDQUFDLFVBQVUsQ0FBQyxDQUFDO1lBQy9CLENBQUM7WUFDRCxjQUFjLEVBQUUsSUFBSSxDQUFDLFdBQVc7U0FDakMsQ0FBQyxDQUFDO1FBSUQsSUFBSSxDQUFDLGdCQUFnQixDQUFDLFNBQVMsR0FBRyxLQUFLLEVBQUUsUUFBUSxFQUFFLEVBQUU7WUFDbkQsTUFBTSxPQUFPLEdBQUksUUFBZ0IsQ0FBQyxNQUFNLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLElBQUksQ0FBQztZQUNwRSx1RUFBdUU7WUFDdkUsaURBQWlEO1lBQ2pELE1BQU0sUUFBUSxHQUFHLE1BQU0sSUFBSSxDQUFDLFdBQVcsQ0FBQyxtQkFBbUIsQ0FBQyxPQUFPLEVBQUU7Z0JBQ25FLFNBQVMsRUFBRSxJQUFJO2FBQ2hCLENBQUMsQ0FBQztZQUNILElBQUksUUFBUSxJQUFJLENBQUMsUUFBUSxDQUFDLEtBQUssRUFBRSxDQUFDO2dCQUNoQyxJQUFJLENBQUMsV0FBVyxDQUFDLFFBQVEsQ0FBQyxDQUFDO1lBQzdCLENBQUM7UUFDSCxDQUFDLENBQUM7SUFDSixDQUFDO0lBRUQ7O09BRUc7SUFDSSxrQkFBa0IsQ0FDdkIsVUFBdUI7UUFFdkIsTUFBTSxZQUFZLEdBQUcsSUFBSSxPQUFPLENBQUMsWUFBWSxDQUFDLFlBQVksQ0FBQyxJQUFJLENBQUMsV0FBVyxFQUFFLFVBQVUsQ0FBQyxDQUFDO1FBQ3pGLE9BQU8sWUFBWSxDQUFDO0lBQ3RCLENBQUM7SUFFRDs7T0FFRztJQUNJLEtBQUssQ0FBQyxXQUFXLENBQ3RCLFVBQWE7UUFFYixJQUFJLENBQUMsZ0JBQWdCLENBQUMsV0FBVyxDQUFDLFVBQVUsQ0FBQyxDQUFDO0lBQ2hELENBQUM7SUFFRDs7T0FFRztJQUNJLEtBQUssQ0FBQyxrQkFBa0IsQ0FDN0IsU0FBc0IsRUFDdEIsZUFBeUQ7UUFFekQsSUFBSSxDQUFDLFdBQVcsQ0FBQyxlQUFlLENBQzlCLElBQUksT0FBTyxDQUFDLFlBQVksQ0FBQyxZQUFZLENBQUksU0FBUyxFQUFFLGVBQWUsQ0FBQyxDQUNyRSxDQUFDO0lBQ0osQ0FBQztDQUNGIn0=
243
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZGVlcy1jb21tcy5jbGFzc2VzLmRlZXNjb21tcy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3RzL2RlZXMtY29tbXMuY2xhc3Nlcy5kZWVzY29tbXMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsT0FBTyxLQUFLLE9BQU8sTUFBTSx5QkFBeUIsQ0FBQztBQWdFbkQsTUFBTSxnQkFBZ0IsR0FBRyxDQUFDLFVBQWtCLEVBQXdDLEVBQUUsQ0FDcEYsS0FBSyxJQUFJLFVBQVU7SUFDbkIsT0FBTyxVQUFVLENBQUMsR0FBRyxLQUFLLFVBQVU7SUFDcEMsT0FBTyxJQUFJLFVBQVU7SUFDckIsT0FBTyxVQUFVLENBQUMsS0FBSyxLQUFLLFVBQVUsQ0FBQztBQUV6Qzs7OztHQUlHO0FBQ0gsTUFBTSxXQUFXLEdBQUcsQ0FDbEIsT0FBZSxFQUNmLFVBQXlDLEVBQ3RCLEVBQUU7SUFDckIsSUFBSSxPQUFPLFVBQVUsQ0FBQyxnQkFBZ0IsS0FBSyxVQUFVLEVBQUUsQ0FBQztRQUN0RCxNQUFNLE9BQU8sR0FBRyxJQUFJLFVBQVUsQ0FBQyxnQkFBZ0IsQ0FBQyxPQUFPLENBQUMsQ0FBQztRQUN6RCxPQUFPLENBQUMsU0FBUyxHQUFHLENBQUMsUUFBK0IsRUFBRSxFQUFFO1lBQ3RELFVBQVUsQ0FBQyxRQUFRLENBQUMsSUFBSSxDQUFDLENBQUM7UUFDNUIsQ0FBQyxDQUFDO1FBQ0YsTUFBTSxjQUFjLEdBQUcsZ0JBQWdCLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDO1FBQ2xFLGdFQUFnRTtRQUNoRSxjQUFjLEVBQUUsS0FBSyxFQUFFLENBQUM7UUFDeEIsT0FBTztZQUNMLElBQUksRUFBRSxLQUFLLEVBQUUsVUFBVSxFQUFFLEVBQUU7Z0JBQ3pCLE9BQU8sQ0FBQyxXQUFXLENBQUMsVUFBVSxDQUFDLENBQUM7WUFDbEMsQ0FBQztZQUNELEtBQUssRUFBRSxLQUFLLElBQUksRUFBRTtn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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.32",
3
+ "version": "1.1.1",
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,11 +9,11 @@
9
9
  "author": "Lossless GmbH",
10
10
  "license": "MIT",
11
11
  "devDependencies": {
12
- "@git.zone/tsbuild": "^4.4.2",
13
- "@git.zone/tsbundle": "^2.13.0",
14
- "@git.zone/tsrun": "^2.0.6",
15
- "@git.zone/tstest": "^3.6.7",
16
- "@types/node": "^26.1.1"
12
+ "@git.zone/tsbuild": "^5.0.0",
13
+ "@git.zone/tsbundle": "^3.0.0",
14
+ "@git.zone/tsrun": "^3.0.1",
15
+ "@git.zone/tstest": "^6.3.2",
16
+ "@types/node": "^26.6.2"
17
17
  },
18
18
  "dependencies": {
19
19
  "@api.global/typedrequest": "^8.0.3",
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,114 @@ 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.
17
+ ### Channels
18
+
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.
20
+
21
+ ```typescript
22
+ import { DeesComms } from '@design.estate/dees-comms';
23
+
24
+ // the default channel, 'dees-comms'
25
+ const comms = new DeesComms();
26
+
27
+ // a channel of this application's own
28
+ const appComms = new DeesComms({ channelName: 'my-app' });
29
+
30
+ console.log(DeesComms.defaultChannelName); // 'dees-comms'
31
+ console.log(appComms.channelName); // 'my-app'
32
+ ```
33
+
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.
35
+
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.
22
37
 
23
- ### Setting Up
38
+ ### Typed requests
24
39
 
25
- First, make sure to import the module in your TypeScript file:
40
+ A typed request goes to every other instance on the channel; an instance with a handler for its method answers it.
26
41
 
27
42
  ```typescript
28
43
  import { DeesComms } from '@design.estate/dees-comms';
44
+ import type { ITypedRequest } from '@api.global/typedrequest-interfaces';
29
45
 
30
- interface IMyMessage {
31
- method: 'myMethod';
46
+ interface IGreetRequest extends ITypedRequest {
47
+ method: 'greet';
32
48
  request: {
33
- someData: string;
49
+ name: string;
34
50
  };
35
51
  response: {
36
52
  reply: string;
37
53
  };
38
54
  }
39
- ```
40
55
 
41
- ### Initializing the Communication Module
42
-
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.
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!` };
60
+ });
44
61
 
45
- ```typescript
46
- const myDeesComms = new DeesComms();
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!'
47
66
  ```
48
67
 
49
- ### Sending Messages
50
-
51
- To send messages, you'll need to create a typed request. Let's assume we are sending a message that requires a response:
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.
52
69
 
53
- ```typescript
54
- // Creating a typed request
55
- const myTypedRequest = myDeesComms.createTypedRequest<IMyMessage>('myMethod');
70
+ ### Typed events
56
71
 
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
65
-
66
- To handle incoming messages, you need to define handlers for the types of messages you expect to receive.
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.
67
73
 
68
74
  ```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!'
75
+ import { DeesComms } from '@design.estate/dees-comms';
76
+ import type { ITypedEvent } from '@api.global/typedrequest-interfaces';
77
+
78
+ interface ISettingsChangedEvent extends ITypedEvent<{ section: string }> {
79
+ name: 'settingsChanged';
80
+ uniqueEventId: string;
81
+ payload: {
82
+ section: string;
78
83
  };
84
+ }
85
+
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);
79
90
  });
80
- ```
81
91
 
82
- ### Communication Between Tabs or Workers
92
+ // in the tab where it happened
93
+ const telling = new DeesComms({ channelName: 'my-app' });
94
+ await telling.broadcast<ISettingsChangedEvent>('settingsChanged', { section: 'profile' });
83
95
 
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.
96
+ // later
97
+ subscription.unsubscribe();
98
+ ```
85
99
 
86
- ### Complete Example
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.
87
105
 
88
- Putting it all together, let's create a small application that sends a message and listens for a response:
106
+ ### Closing
89
107
 
90
108
  ```typescript
91
- import { DeesComms } from '@design.estate/dees-comms';
92
-
93
- interface IGreetRequest {
94
- method: 'greet';
95
- request: {
96
- name: string;
97
- };
98
- response: {
99
- reply: string;
100
- };
101
- }
109
+ await comms.close();
110
+ ```
102
111
 
103
- const appDeesComms = new DeesComms();
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.
104
113
 
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!` };
109
- });
114
+ In Node.js an instance keeps the process alive only while something listens on it, as a `MessagePort` with a message listener does: while it has a subscription, or once it has created a typed handler. An instance nobody listens on, such as the one `@design.estate/dees-domtools` opens on import, lets the process end. An unsubscribed or closed instance lets it end too, so close every instance with a typed handler that a test or a script opens. In browsers and workers nothing changes.
110
115
 
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!"
115
- ```
116
+ ### What travels
116
117
 
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.
118
+ 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
119
 
119
120
  ## License and Legal Information
120
121
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-comms',
6
- version: '1.0.32',
6
+ version: '1.1.1',
7
7
  description: 'A communications module for enabling DOM-based messaging and synchronization across browser tabs and workers.'
8
8
  }