@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.
- package/dist_bundle/bundle.js +9 -9
- package/dist_bundle/bundle.js.map +1 -1
- package/dist_bundle/bundle.js.third-party-notices.json +408 -0
- package/dist_bundle/bundle.js.third-party-notices.txt +968 -0
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/dees-comms.classes.deescomms.d.ts +62 -2
- package/dist_ts/dees-comms.classes.deescomms.js +210 -20
- package/package.json +6 -6
- package/readme.md +71 -70
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/dees-comms.classes.deescomms.ts +305 -19
- package/readme.hints.md +0 -1
|
@@ -1,38 +1,186 @@
|
|
|
1
1
|
import * as plugins from './dees-comms.plugins.js';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
export interface IDeesCommsOptions {
|
|
4
|
+
/**
|
|
5
|
+
* The channel this instance talks on. Instances hear each other only when
|
|
6
|
+
* they use the same name, in any tab, frame or worker of the same origin.
|
|
7
|
+
* Defaults to `DeesComms.defaultChannelName`, the channel every instance
|
|
8
|
+
* used before names existed.
|
|
9
|
+
*/
|
|
10
|
+
channelName?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Receives the payload of one typed event, and the event itself.
|
|
15
|
+
*/
|
|
16
|
+
export type TDeesCommsEventHandler<T extends plugins.typedrequestInterfaces.ITypedEvent<unknown>> = (
|
|
17
|
+
payloadArg: T['payload'],
|
|
18
|
+
eventArg: T
|
|
19
|
+
) => void | Promise<void>;
|
|
20
|
+
|
|
21
|
+
export interface IDeesCommsSubscription {
|
|
22
|
+
/**
|
|
23
|
+
* Stops this subscription. An event that is being delivered right now does
|
|
24
|
+
* not reach it any more; calling it again does nothing.
|
|
25
|
+
*/
|
|
26
|
+
unsubscribe(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* How a typed event travels on the channel. The envelope keeps it apart from
|
|
31
|
+
* typed requests and their responses, which are routed by the typedrouter.
|
|
32
|
+
*/
|
|
33
|
+
interface IDeesCommsEventMessage {
|
|
34
|
+
deesCommsEvent: plugins.typedrequestInterfaces.ITypedEvent<unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type TStoredEventHandler = (
|
|
38
|
+
payloadArg: unknown,
|
|
39
|
+
eventArg: plugins.typedrequestInterfaces.ITypedEvent<unknown>
|
|
40
|
+
) => void | Promise<void>;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The part of a BroadcastChannel that DeesComms uses: the platform's own, or
|
|
44
|
+
* the polyfill where the platform has none.
|
|
45
|
+
*/
|
|
46
|
+
interface IDeesCommsChannel {
|
|
47
|
+
post(messageArg: unknown): Promise<void>;
|
|
48
|
+
close(): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Whether the open channel keeps its process alive. Only Node.js's channel
|
|
51
|
+
* can hold a process; elsewhere this does nothing.
|
|
52
|
+
*/
|
|
53
|
+
holdProcess(holdArg: boolean): void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A channel that decides whether it keeps its process alive, as Node.js's
|
|
58
|
+
* BroadcastChannel does. A browser's channel has neither method.
|
|
59
|
+
*/
|
|
60
|
+
interface IProcessHoldingChannel {
|
|
61
|
+
ref(): void;
|
|
62
|
+
unref(): void;
|
|
6
63
|
}
|
|
7
64
|
|
|
65
|
+
const isProcessHolding = (channelArg: object): channelArg is IProcessHoldingChannel =>
|
|
66
|
+
'ref' in channelArg &&
|
|
67
|
+
typeof channelArg.ref === 'function' &&
|
|
68
|
+
'unref' in channelArg &&
|
|
69
|
+
typeof channelArg.unref === 'function';
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Opens a channel that hands every message it receives to `receiveArg` as the
|
|
73
|
+
* message itself: the platform's BroadcastChannel delivers a MessageEvent, the
|
|
74
|
+
* polyfill delivers the message.
|
|
75
|
+
*/
|
|
76
|
+
const openChannel = (
|
|
77
|
+
nameArg: string,
|
|
78
|
+
receiveArg: (messageArg: unknown) => void
|
|
79
|
+
): IDeesCommsChannel => {
|
|
80
|
+
if (typeof globalThis.BroadcastChannel === 'function') {
|
|
81
|
+
const channel = new globalThis.BroadcastChannel(nameArg);
|
|
82
|
+
channel.onmessage = (eventArg: MessageEvent<unknown>) => {
|
|
83
|
+
receiveArg(eventArg.data);
|
|
84
|
+
};
|
|
85
|
+
const processHolding = isProcessHolding(channel) ? channel : null;
|
|
86
|
+
// an open channel alone does not keep a Node.js process running
|
|
87
|
+
processHolding?.unref();
|
|
88
|
+
return {
|
|
89
|
+
post: async (messageArg) => {
|
|
90
|
+
channel.postMessage(messageArg);
|
|
91
|
+
},
|
|
92
|
+
close: async () => {
|
|
93
|
+
channel.close();
|
|
94
|
+
},
|
|
95
|
+
holdProcess: (holdArg) => {
|
|
96
|
+
if (holdArg) {
|
|
97
|
+
processHolding?.ref();
|
|
98
|
+
} else {
|
|
99
|
+
processHolding?.unref();
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const channel = new plugins.BroadCastChannelPolyfill<unknown>(nameArg);
|
|
105
|
+
channel.onmessage = (messageArg: unknown) => {
|
|
106
|
+
receiveArg(messageArg);
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
post: (messageArg) => channel.postMessage(messageArg),
|
|
110
|
+
close: () => channel.close(),
|
|
111
|
+
// the polyfill stands in only where the platform has no channel, which
|
|
112
|
+
// Node.js always has
|
|
113
|
+
holdProcess: () => undefined,
|
|
114
|
+
};
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const isObject = (valueArg: unknown): valueArg is Record<string, unknown> =>
|
|
118
|
+
typeof valueArg === 'object' && valueArg !== null;
|
|
119
|
+
|
|
120
|
+
// `Object.hasOwn` is younger than some of the browsers the polyfill serves
|
|
121
|
+
const hasOwn = (objectArg: Record<string, unknown>, keyArg: string): boolean =>
|
|
122
|
+
Object.prototype.hasOwnProperty.call(objectArg, keyArg);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The event a received message carries: `null` for a message that is no
|
|
126
|
+
* event, `'malformed'` for an envelope without a well-formed event in it.
|
|
127
|
+
*/
|
|
128
|
+
const eventOf = (
|
|
129
|
+
messageArg: unknown
|
|
130
|
+
): plugins.typedrequestInterfaces.ITypedEvent<unknown> | 'malformed' | null => {
|
|
131
|
+
if (!isObject(messageArg) || !hasOwn(messageArg, 'deesCommsEvent')) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const event = messageArg.deesCommsEvent;
|
|
135
|
+
if (
|
|
136
|
+
!isObject(event) ||
|
|
137
|
+
typeof event.name !== 'string' ||
|
|
138
|
+
typeof event.uniqueEventId !== 'string' ||
|
|
139
|
+
!hasOwn(event, 'payload')
|
|
140
|
+
) {
|
|
141
|
+
return 'malformed';
|
|
142
|
+
}
|
|
143
|
+
return { name: event.name, uniqueEventId: event.uniqueEventId, payload: event.payload };
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const createEventId = (): string => {
|
|
147
|
+
const bytes = globalThis.crypto.getRandomValues(new Uint8Array(16));
|
|
148
|
+
return Array.from(bytes, (byteArg) => byteArg.toString(16).padStart(2, '0')).join('');
|
|
149
|
+
};
|
|
150
|
+
|
|
8
151
|
/**
|
|
9
152
|
* a comm class for client side communication between workers and tabs.
|
|
10
153
|
*/
|
|
11
154
|
export class DeesComms {
|
|
12
|
-
|
|
13
|
-
|
|
155
|
+
/**
|
|
156
|
+
* The channel of every instance that is not given a name. Code that talks
|
|
157
|
+
* to other code on it, like a service worker and its pages, keeps working
|
|
158
|
+
* unchanged; an application gives its own messages a name of its own.
|
|
159
|
+
*/
|
|
160
|
+
public static readonly defaultChannelName = 'dees-comms';
|
|
161
|
+
|
|
162
|
+
public readonly channelName: string;
|
|
163
|
+
private readonly channel: IDeesCommsChannel;
|
|
164
|
+
private readonly eventHandlers = new Map<string, Set<TStoredEventHandler>>();
|
|
165
|
+
private hasTypedHandlers = false;
|
|
166
|
+
private closed = false;
|
|
167
|
+
private closing: Promise<void> | null = null;
|
|
168
|
+
|
|
14
169
|
// sending messages
|
|
15
170
|
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
16
171
|
public typedtarget = new plugins.typedrequest.TypedTarget({
|
|
17
172
|
postMethodWithTypedRouter: async (messageArg) => {
|
|
18
|
-
this.postMessage(messageArg);
|
|
173
|
+
await this.postMessage(messageArg);
|
|
19
174
|
},
|
|
20
175
|
typedRouterRef: this.typedrouter,
|
|
21
176
|
});
|
|
22
177
|
|
|
23
178
|
// receiving messages
|
|
24
|
-
constructor() {
|
|
25
|
-
this.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
const response = await this.typedrouter.routeAndAddResponse(message, {
|
|
30
|
-
skipHooks: true,
|
|
31
|
-
});
|
|
32
|
-
if (response && !response.error) {
|
|
33
|
-
this.postMessage(response);
|
|
34
|
-
}
|
|
35
|
-
};
|
|
179
|
+
constructor(optionsArg: IDeesCommsOptions = {}) {
|
|
180
|
+
this.channelName = optionsArg.channelName ?? DeesComms.defaultChannelName;
|
|
181
|
+
this.channel = openChannel(this.channelName, (messageArg) => {
|
|
182
|
+
void this.receive(messageArg);
|
|
183
|
+
});
|
|
36
184
|
}
|
|
37
185
|
|
|
38
186
|
/**
|
|
@@ -51,7 +199,8 @@ export class DeesComms {
|
|
|
51
199
|
public async postMessage<T = plugins.typedrequestInterfaces.ITypedRequest>(
|
|
52
200
|
messageArg: T
|
|
53
201
|
): Promise<void> {
|
|
54
|
-
this.
|
|
202
|
+
this.assertOpen();
|
|
203
|
+
await this.channel.post(messageArg);
|
|
55
204
|
}
|
|
56
205
|
|
|
57
206
|
/**
|
|
@@ -64,5 +213,142 @@ export class DeesComms {
|
|
|
64
213
|
this.typedrouter.addTypedHandler(
|
|
65
214
|
new plugins.typedrequest.TypedHandler<T>(methodArg, handlerFunction)
|
|
66
215
|
);
|
|
216
|
+
this.hasTypedHandlers = true;
|
|
217
|
+
this.holdProcessWhileListened();
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Sends a typed event to every other instance on this channel and expects
|
|
222
|
+
* no answer. The instance that sends it does not receive it; any other
|
|
223
|
+
* instance on the same channel does, in this context or another one.
|
|
224
|
+
*/
|
|
225
|
+
public async broadcast<T extends plugins.typedrequestInterfaces.ITypedEvent<unknown>>(
|
|
226
|
+
nameArg: T['name'],
|
|
227
|
+
payloadArg: T['payload']
|
|
228
|
+
): Promise<void> {
|
|
229
|
+
const message: IDeesCommsEventMessage = {
|
|
230
|
+
deesCommsEvent: {
|
|
231
|
+
name: nameArg,
|
|
232
|
+
uniqueEventId: createEventId(),
|
|
233
|
+
payload: payloadArg,
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
await this.postMessage(message);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Calls the handler for every typed event of that name another instance
|
|
241
|
+
* broadcasts on this channel. Handlers run in the order they subscribed; one
|
|
242
|
+
* that throws or rejects is reported and does not keep the others from
|
|
243
|
+
* running.
|
|
244
|
+
*/
|
|
245
|
+
public subscribe<T extends plugins.typedrequestInterfaces.ITypedEvent<unknown>>(
|
|
246
|
+
nameArg: T['name'],
|
|
247
|
+
handlerArg: TDeesCommsEventHandler<T>
|
|
248
|
+
): IDeesCommsSubscription {
|
|
249
|
+
this.assertOpen();
|
|
250
|
+
// the name is the contract: what arrives under it is taken as its type,
|
|
251
|
+
// as a typed handler takes the request of its method
|
|
252
|
+
const handler: TStoredEventHandler = (payloadArg, eventArg) =>
|
|
253
|
+
handlerArg(payloadArg as T['payload'], eventArg as T);
|
|
254
|
+
let handlers = this.eventHandlers.get(nameArg);
|
|
255
|
+
if (!handlers) {
|
|
256
|
+
handlers = new Set();
|
|
257
|
+
this.eventHandlers.set(nameArg, handlers);
|
|
258
|
+
}
|
|
259
|
+
handlers.add(handler);
|
|
260
|
+
this.holdProcessWhileListened();
|
|
261
|
+
return {
|
|
262
|
+
unsubscribe: () => {
|
|
263
|
+
const current = this.eventHandlers.get(nameArg);
|
|
264
|
+
if (!current) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
current.delete(handler);
|
|
268
|
+
if (current.size === 0) {
|
|
269
|
+
this.eventHandlers.delete(nameArg);
|
|
270
|
+
this.holdProcessWhileListened();
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Closes the channel. Nothing is received from then on, every subscription
|
|
278
|
+
* ends, and sending, broadcasting or subscribing throws. A typed request
|
|
279
|
+
* still waiting for its response when the channel closes cannot receive it
|
|
280
|
+
* and ends with its own timeout. Closing again does nothing.
|
|
281
|
+
*/
|
|
282
|
+
public async close(): Promise<void> {
|
|
283
|
+
if (!this.closing) {
|
|
284
|
+
this.closed = true;
|
|
285
|
+
this.eventHandlers.clear();
|
|
286
|
+
this.closing = this.channel.close();
|
|
287
|
+
}
|
|
288
|
+
await this.closing;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* In Node.js an instance keeps its process alive while something listens on
|
|
293
|
+
* it, as a MessagePort with a message listener does: a subscription, or a
|
|
294
|
+
* typed handler it created. One nobody listens on lets the process end.
|
|
295
|
+
*/
|
|
296
|
+
private holdProcessWhileListened(): void {
|
|
297
|
+
if (this.closed) {
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
this.channel.holdProcess(this.hasTypedHandlers || this.eventHandlers.size > 0);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
private assertOpen(): void {
|
|
304
|
+
if (this.closed) {
|
|
305
|
+
throw new Error(`DeesComms channel "${this.channelName}" is closed`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private async receive(messageArg: unknown): Promise<void> {
|
|
310
|
+
if (this.closed) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const event = eventOf(messageArg);
|
|
314
|
+
if (event === 'malformed') {
|
|
315
|
+
console.log('DeesComms received a malformed event');
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (event) {
|
|
319
|
+
this.dispatch(event);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
// Skip hooks for broadcast-received messages to prevent infinite loops
|
|
323
|
+
// when traffic logging hooks are set up globally. The router validates
|
|
324
|
+
// the shape of what it is given.
|
|
325
|
+
const response = await this.typedrouter.routeAndAddResponse(
|
|
326
|
+
messageArg as plugins.typedrequestInterfaces.ITypedRequest,
|
|
327
|
+
{ skipHooks: true }
|
|
328
|
+
);
|
|
329
|
+
if (response && !response.error && !this.closed) {
|
|
330
|
+
await this.postMessage(response);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private dispatch(eventArg: plugins.typedrequestInterfaces.ITypedEvent<unknown>): void {
|
|
335
|
+
const handlers = this.eventHandlers.get(eventArg.name);
|
|
336
|
+
if (!handlers) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
for (const handler of [...handlers]) {
|
|
340
|
+
// a handler that closed the channel ends the delivery, and one an
|
|
341
|
+
// earlier handler unsubscribed is not called any more
|
|
342
|
+
if (this.closed) {
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
if (!handlers.has(handler)) {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
void (async () => handler(eventArg.payload, eventArg))().catch((errorArg: unknown) => {
|
|
349
|
+
// the event's name and the failure, never its payload
|
|
350
|
+
console.error(`DeesComms: a handler of the event "${eventArg.name}" failed`, errorArg);
|
|
351
|
+
});
|
|
352
|
+
}
|
|
67
353
|
}
|
|
68
354
|
}
|
package/readme.hints.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|