libzapitu 1.0.0-alpha.24 → 1.0.0-alpha.26
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/lib/worker/child.js +72 -17
- package/lib/worker/proxy.js +121 -12
- package/package.json +1 -1
package/lib/worker/child.js
CHANGED
|
@@ -76,22 +76,79 @@ function proxyCallback(socketId, key, log, ...args) {
|
|
|
76
76
|
// Create a socket for a given socketId
|
|
77
77
|
// ---------------------------------------------------------------------------
|
|
78
78
|
/**
|
|
79
|
-
* Recursively clone a value, replacing non-cloneable types
|
|
80
|
-
* with plain serializable representations
|
|
79
|
+
* Recursively clone a value for postMessage, replacing non-cloneable types
|
|
80
|
+
* (Error, Function) with plain serializable representations, preserving
|
|
81
|
+
* built-in value objects, and converting protobufjs message instances to JSON.
|
|
82
|
+
*
|
|
83
|
+
* Protobuf messages (e.g. proto.Message, proto.WebMessageInfo) carry a
|
|
84
|
+
* toJSON() method that serializes byte fields as base64 strings and 64-bit
|
|
85
|
+
* integer fields as strings. Structured clone strips the prototype chain, so
|
|
86
|
+
* without this step Buffers become Uint8Array plain objects and Longs become
|
|
87
|
+
* { low, high, unsigned } objects on the parent side. Converting them before
|
|
88
|
+
* crossing the boundary keeps the worker event shape identical to direct mode
|
|
89
|
+
* when the data is logged or JSON.stringify'd.
|
|
81
90
|
*/
|
|
82
|
-
function
|
|
91
|
+
function serializeForPostMessage(value, seen = new WeakSet()) {
|
|
83
92
|
if (value instanceof Error) {
|
|
93
|
+
// Boom carries critical metadata (statusCode, output, data, isServer).
|
|
94
|
+
// Preserve it so the parent can reconstruct a real Boom instance.
|
|
95
|
+
if (value.isBoom) {
|
|
96
|
+
return {
|
|
97
|
+
__error__: true,
|
|
98
|
+
__boom__: true,
|
|
99
|
+
message: value.message,
|
|
100
|
+
name: value.name,
|
|
101
|
+
stack: value.stack,
|
|
102
|
+
data: value.data,
|
|
103
|
+
output: value.output,
|
|
104
|
+
isServer: value.isServer
|
|
105
|
+
};
|
|
106
|
+
}
|
|
84
107
|
return { __error__: true, message: value.message, name: value.name, stack: value.stack };
|
|
85
108
|
}
|
|
86
109
|
if (typeof value === 'function') {
|
|
87
110
|
return '__fn__';
|
|
88
111
|
}
|
|
112
|
+
// Preserve built-in value objects that structured clone handles, but tag
|
|
113
|
+
// them so the parent can reconstruct the exact same runtime type.
|
|
114
|
+
if (value instanceof Date) {
|
|
115
|
+
return { __date__: true, value: value.toISOString() };
|
|
116
|
+
}
|
|
117
|
+
if (value instanceof RegExp) {
|
|
118
|
+
return { __regexp__: true, source: value.source, flags: value.flags };
|
|
119
|
+
}
|
|
120
|
+
if (value instanceof URL) {
|
|
121
|
+
return { __url__: true, href: value.href };
|
|
122
|
+
}
|
|
123
|
+
if (value instanceof Map) {
|
|
124
|
+
return { __map__: true, entries: serializeForPostMessage(Array.from(value.entries()), seen) };
|
|
125
|
+
}
|
|
126
|
+
if (value instanceof Set) {
|
|
127
|
+
return { __set__: true, entries: serializeForPostMessage(Array.from(value.values()), seen) };
|
|
128
|
+
}
|
|
89
129
|
if (value !== null && typeof value === 'object') {
|
|
90
130
|
if (seen.has(value))
|
|
91
131
|
return '[Circular]';
|
|
92
132
|
seen.add(value);
|
|
133
|
+
// Detect a protobufjs message instance by the static methods its
|
|
134
|
+
// constructor exposes, then use the instance's own toJSON() so nested
|
|
135
|
+
// bytes/long fields are serialized consistently. Tag the resulting
|
|
136
|
+
// plain object with the constructor name so the parent can revive it
|
|
137
|
+
// back into a real protobuf instance (and therefore recover the
|
|
138
|
+
// runtime Buffer types downstream code expects).
|
|
139
|
+
const ctor = value.constructor;
|
|
140
|
+
if (ctor &&
|
|
141
|
+
typeof ctor === 'function' &&
|
|
142
|
+
typeof ctor.encode === 'function' &&
|
|
143
|
+
typeof ctor.decode === 'function' &&
|
|
144
|
+
typeof ctor.toObject === 'function' &&
|
|
145
|
+
typeof value.toJSON === 'function') {
|
|
146
|
+
const json = value.toJSON();
|
|
147
|
+
json.__protobufType__ = ctor.name;
|
|
148
|
+
return serializeForPostMessage(json, seen);
|
|
149
|
+
}
|
|
93
150
|
if (Array.isArray(value)) {
|
|
94
|
-
return value.map(v =>
|
|
151
|
+
return value.map(v => serializeForPostMessage(v, seen));
|
|
95
152
|
}
|
|
96
153
|
// Preserve binary/typed-array types — structured clone handles them natively
|
|
97
154
|
if (Buffer.isBuffer(value) ||
|
|
@@ -102,7 +159,7 @@ function sanitizeForPostMessage(value, seen = new WeakSet()) {
|
|
|
102
159
|
}
|
|
103
160
|
const result = {};
|
|
104
161
|
for (const key of Object.keys(value)) {
|
|
105
|
-
result[key] =
|
|
162
|
+
result[key] = serializeForPostMessage(value[key], seen);
|
|
106
163
|
}
|
|
107
164
|
return result;
|
|
108
165
|
}
|
|
@@ -188,6 +245,8 @@ function createSocket(socketId, rawConfig) {
|
|
|
188
245
|
log.info('socket created successfully');
|
|
189
246
|
// Send user and authState to the parent when they become available.
|
|
190
247
|
// These are needed for wsocket.user and wsocket.authState on the proxy side.
|
|
248
|
+
// authState.keys contains forwarding functions, so we must serialize the
|
|
249
|
+
// info before postMessage or the whole message is dropped.
|
|
191
250
|
const sendSocketInfo = () => {
|
|
192
251
|
try {
|
|
193
252
|
const info = {};
|
|
@@ -196,7 +255,11 @@ function createSocket(socketId, rawConfig) {
|
|
|
196
255
|
if (sock.authState)
|
|
197
256
|
info.authState = sock.authState;
|
|
198
257
|
if (Object.keys(info).length > 0) {
|
|
199
|
-
worker_threads_1.parentPort.postMessage({
|
|
258
|
+
worker_threads_1.parentPort.postMessage({
|
|
259
|
+
type: 'socket-info',
|
|
260
|
+
socketId,
|
|
261
|
+
info: serializeForPostMessage(info)
|
|
262
|
+
});
|
|
200
263
|
}
|
|
201
264
|
}
|
|
202
265
|
catch (_) {
|
|
@@ -226,18 +289,10 @@ function createSocket(socketId, rawConfig) {
|
|
|
226
289
|
sock.ev.on('event', (map) => {
|
|
227
290
|
log.trace({ eventKeys: Object.keys(map) }, 'event → parent');
|
|
228
291
|
try {
|
|
229
|
-
worker_threads_1.parentPort.postMessage({ type: 'event', socketId, map });
|
|
292
|
+
worker_threads_1.parentPort.postMessage({ type: 'event', socketId, map: serializeForPostMessage(map) });
|
|
230
293
|
}
|
|
231
294
|
catch (err) {
|
|
232
295
|
log.error({ err, eventKeys: Object.keys(map) }, 'failed to send event to parent (non-cloneable data)');
|
|
233
|
-
// Attempt to send a sanitized version without Error objects
|
|
234
|
-
try {
|
|
235
|
-
const sanitized = sanitizeForPostMessage(map);
|
|
236
|
-
worker_threads_1.parentPort.postMessage({ type: 'event', socketId, map: sanitized });
|
|
237
|
-
}
|
|
238
|
-
catch (_) {
|
|
239
|
-
log.error({ eventKeys: Object.keys(map) }, 'event dropped — could not sanitize');
|
|
240
|
-
}
|
|
241
296
|
}
|
|
242
297
|
});
|
|
243
298
|
// Forward raw WS events to the parent so ws.on() works on the proxy side.
|
|
@@ -305,11 +360,11 @@ worker_threads_1.parentPort.on('message', async (msg) => {
|
|
|
305
360
|
const thisCtx = path.length > 1 ? resolveTarget(sock, path.slice(0, -1)) : sock;
|
|
306
361
|
const result = await target.apply(thisCtx, args || []);
|
|
307
362
|
log.trace({ rpcId: id, method }, 'RPC call succeeded');
|
|
308
|
-
worker_threads_1.parentPort.postMessage({ type: 'result', socketId, id, result });
|
|
363
|
+
worker_threads_1.parentPort.postMessage({ type: 'result', socketId, id, result: serializeForPostMessage(result) });
|
|
309
364
|
}
|
|
310
365
|
else {
|
|
311
366
|
log.trace({ rpcId: id, method }, 'RPC property read');
|
|
312
|
-
worker_threads_1.parentPort.postMessage({ type: 'result', socketId, id, result: target });
|
|
367
|
+
worker_threads_1.parentPort.postMessage({ type: 'result', socketId, id, result: serializeForPostMessage(target) });
|
|
313
368
|
}
|
|
314
369
|
}
|
|
315
370
|
catch (err) {
|
package/lib/worker/proxy.js
CHANGED
|
@@ -36,6 +36,8 @@ const stream_1 = require("stream");
|
|
|
36
36
|
const util_1 = require("util");
|
|
37
37
|
const os_2 = require("os");
|
|
38
38
|
const long_1 = __importDefault(require("long"));
|
|
39
|
+
const boom_1 = require("@hapi/boom");
|
|
40
|
+
const WAProto_1 = require("../../WAProto");
|
|
39
41
|
const pipelineAsync = (0, util_1.promisify)(stream_1.pipeline);
|
|
40
42
|
// ---------------------------------------------------------------------------
|
|
41
43
|
// Stream serialization for RPC boundary
|
|
@@ -224,23 +226,98 @@ function createNoopLogger() {
|
|
|
224
226
|
};
|
|
225
227
|
}
|
|
226
228
|
/**
|
|
227
|
-
*
|
|
228
|
-
*
|
|
229
|
-
*
|
|
229
|
+
* Build a lookup table from protobufjs type name to the generated
|
|
230
|
+
* constructor. This lets us revive a message that was serialized to JSON
|
|
231
|
+
* in the worker back into a real protobuf instance on the parent side.
|
|
232
|
+
*/
|
|
233
|
+
function buildProtobufTypeRegistry(namespace, result = new Map()) {
|
|
234
|
+
for (const [key, value] of Object.entries(namespace)) {
|
|
235
|
+
if (typeof value === 'function' && value.fromObject && value.encode && value.decode) {
|
|
236
|
+
result.set(value.name || key, value);
|
|
237
|
+
}
|
|
238
|
+
else if (value !== null && typeof value === 'object' && !value.fromObject) {
|
|
239
|
+
buildProtobufTypeRegistry(value, result);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
const PROTOBUF_TYPE_REGISTRY = buildProtobufTypeRegistry(WAProto_1.proto);
|
|
245
|
+
/**
|
|
246
|
+
* Recursively revive objects that were serialized by the child's
|
|
247
|
+
* serializeForPostMessage (e.g. Error sentinels, protobuf messages) or
|
|
248
|
+
* decomposed by structured clone (e.g. Long → { low, high, unsigned },
|
|
249
|
+
* Buffer → Uint8Array).
|
|
230
250
|
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
251
|
+
* Protobuf message instances are converted to JSON by the child, tagged with
|
|
252
|
+
* __protobufType__, and then revived here with the corresponding
|
|
253
|
+
* constructor's fromObject(). This restores the runtime Buffer/Long types
|
|
254
|
+
* that downstream code expects while keeping JSON.stringify output identical
|
|
255
|
+
* to direct mode (base64 strings for bytes, strings for 64-bit integers).
|
|
235
256
|
*/
|
|
236
257
|
function revivePostMessage(value) {
|
|
258
|
+
var _a;
|
|
259
|
+
// After structured clone, Buffer subclasses become plain Uint8Arrays.
|
|
260
|
+
// Restore them to Buffer so downstream code gets the same type as in
|
|
261
|
+
// direct mode for non-protobuf binary fields.
|
|
262
|
+
if (value instanceof Uint8Array && !Buffer.isBuffer(value)) {
|
|
263
|
+
return Buffer.from(value);
|
|
264
|
+
}
|
|
237
265
|
if (value !== null && typeof value === 'object') {
|
|
238
266
|
if (value.__error__) {
|
|
267
|
+
// Boom errors carry status codes, payloads, and extra data. Rebuild
|
|
268
|
+
// a real Boom instance so callers can inspect err.output.statusCode.
|
|
269
|
+
if (value.__boom__) {
|
|
270
|
+
const boom = new boom_1.Boom(value.message, {
|
|
271
|
+
statusCode: (_a = value.output) === null || _a === void 0 ? void 0 : _a.statusCode,
|
|
272
|
+
data: value.data
|
|
273
|
+
});
|
|
274
|
+
// Restore the exact output payload/headers from the worker so the
|
|
275
|
+
// status code and error name match direct mode.
|
|
276
|
+
if (value.output) {
|
|
277
|
+
Object.assign(boom.output, value.output);
|
|
278
|
+
}
|
|
279
|
+
boom.stack = value.stack || boom.stack;
|
|
280
|
+
return boom;
|
|
281
|
+
}
|
|
239
282
|
const err = new Error(value.message);
|
|
240
283
|
err.name = value.name;
|
|
241
284
|
err.stack = value.stack;
|
|
242
285
|
return err;
|
|
243
286
|
}
|
|
287
|
+
// Revive built-in value objects tagged by the child.
|
|
288
|
+
if (value.__date__) {
|
|
289
|
+
return new Date(value.value);
|
|
290
|
+
}
|
|
291
|
+
if (value.__regexp__) {
|
|
292
|
+
return new RegExp(value.source, value.flags);
|
|
293
|
+
}
|
|
294
|
+
if (value.__url__) {
|
|
295
|
+
return new URL(value.href);
|
|
296
|
+
}
|
|
297
|
+
if (value.__map__) {
|
|
298
|
+
return new Map(revivePostMessage(value.entries));
|
|
299
|
+
}
|
|
300
|
+
if (value.__set__) {
|
|
301
|
+
return new Set(revivePostMessage(value.entries));
|
|
302
|
+
}
|
|
303
|
+
// Revive protobuf messages tagged by the child. fromObject() will
|
|
304
|
+
// recursively recreate nested protobuf instances and convert base64
|
|
305
|
+
// strings back into Buffer instances.
|
|
306
|
+
const protobufType = value.__protobufType__;
|
|
307
|
+
if (protobufType) {
|
|
308
|
+
const type = PROTOBUF_TYPE_REGISTRY.get(protobufType);
|
|
309
|
+
if (type === null || type === void 0 ? void 0 : type.fromObject) {
|
|
310
|
+
const copy = { ...value };
|
|
311
|
+
delete copy.__protobufType__;
|
|
312
|
+
try {
|
|
313
|
+
return type.fromObject(copy);
|
|
314
|
+
}
|
|
315
|
+
catch (_b) {
|
|
316
|
+
// Fall back to the plain JSON object if fromObject fails;
|
|
317
|
+
// downstream code will see base64 strings instead of Buffers.
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
244
321
|
// Revive Long-like objects that were decomposed by structured clone.
|
|
245
322
|
// protobufjs uses Long for 64-bit integers; after postMessage they
|
|
246
323
|
// become { low, high, unsigned } plain objects.
|
|
@@ -248,7 +325,7 @@ function revivePostMessage(value) {
|
|
|
248
325
|
return new long_1.default(value.low, value.high, value.unsigned);
|
|
249
326
|
}
|
|
250
327
|
if (Array.isArray(value)) {
|
|
251
|
-
// Arrays: recurse to revive any Error sentinels / Longs inside
|
|
328
|
+
// Arrays: recurse to revive any Error sentinels / Longs / Uint8Arrays / protobuf messages / built-ins inside
|
|
252
329
|
let changed = false;
|
|
253
330
|
const result = value.map(v => {
|
|
254
331
|
const revived = revivePostMessage(v);
|
|
@@ -258,8 +335,8 @@ function revivePostMessage(value) {
|
|
|
258
335
|
});
|
|
259
336
|
return changed ? result : value;
|
|
260
337
|
}
|
|
261
|
-
// Plain objects: only recurse if they might contain __error__ sentinels
|
|
262
|
-
//
|
|
338
|
+
// Plain objects: only recurse if they might contain __error__ sentinels,
|
|
339
|
+
// Long-like objects, Uint8Arrays, protobuf markers, or built-in tags.
|
|
263
340
|
if (value.constructor === Object || value.constructor === undefined) {
|
|
264
341
|
let changed = false;
|
|
265
342
|
const result = {};
|
|
@@ -471,7 +548,12 @@ async function handleCallbackCall(slot, socketId, msg) {
|
|
|
471
548
|
try {
|
|
472
549
|
log === null || log === void 0 ? void 0 : log.debug({ keystoreMethod: method, argCount: args.length }, 'invoking keystore method');
|
|
473
550
|
const result = await keystore[method](...args);
|
|
474
|
-
slot.worker.postMessage({
|
|
551
|
+
slot.worker.postMessage({
|
|
552
|
+
type: 'callback-result',
|
|
553
|
+
socketId,
|
|
554
|
+
id,
|
|
555
|
+
result: deepStripFunctions(result)
|
|
556
|
+
});
|
|
475
557
|
}
|
|
476
558
|
catch (err) {
|
|
477
559
|
log === null || log === void 0 ? void 0 : log.error({ err, keystoreMethod: method }, 'keystore method threw error');
|
|
@@ -499,7 +581,12 @@ async function handleCallbackCall(slot, socketId, msg) {
|
|
|
499
581
|
try {
|
|
500
582
|
log === null || log === void 0 ? void 0 : log.debug({ callbackKey: key, argCount: args.length }, 'invoking proxied callback');
|
|
501
583
|
const result = await fn(...args);
|
|
502
|
-
slot.worker.postMessage({
|
|
584
|
+
slot.worker.postMessage({
|
|
585
|
+
type: 'callback-result',
|
|
586
|
+
socketId,
|
|
587
|
+
id,
|
|
588
|
+
result: deepStripFunctions(result)
|
|
589
|
+
});
|
|
503
590
|
}
|
|
504
591
|
catch (err) {
|
|
505
592
|
log === null || log === void 0 ? void 0 : log.error({ err, callbackKey: key }, 'proxied callback threw error');
|
|
@@ -572,8 +659,30 @@ async function drainPool() {
|
|
|
572
659
|
* Recursively clone a value, replacing all functions with the sentinel
|
|
573
660
|
* string '__proxy_fn__'. This ensures the config can be sent via
|
|
574
661
|
* postMessage (structured clone).
|
|
662
|
+
*
|
|
663
|
+
* Built-in value objects (Date, RegExp, URL, Map, Set) are preserved in
|
|
664
|
+
* their serializable tagged form so they survive the boundary instead of
|
|
665
|
+
* being reduced to empty objects by Object.keys().
|
|
575
666
|
*/
|
|
576
667
|
function deepStripFunctions(value, seen = new WeakSet()) {
|
|
668
|
+
if (typeof value === 'function') {
|
|
669
|
+
return '__proxy_fn__';
|
|
670
|
+
}
|
|
671
|
+
if (value instanceof Date) {
|
|
672
|
+
return { __date__: true, value: value.toISOString() };
|
|
673
|
+
}
|
|
674
|
+
if (value instanceof RegExp) {
|
|
675
|
+
return { __regexp__: true, source: value.source, flags: value.flags };
|
|
676
|
+
}
|
|
677
|
+
if (value instanceof URL) {
|
|
678
|
+
return { __url__: true, href: value.href };
|
|
679
|
+
}
|
|
680
|
+
if (value instanceof Map) {
|
|
681
|
+
return { __map__: true, entries: deepStripFunctions(Array.from(value.entries()), seen) };
|
|
682
|
+
}
|
|
683
|
+
if (value instanceof Set) {
|
|
684
|
+
return { __set__: true, entries: deepStripFunctions(Array.from(value.values()), seen) };
|
|
685
|
+
}
|
|
577
686
|
if (typeof value === 'function') {
|
|
578
687
|
return '__proxy_fn__';
|
|
579
688
|
}
|