shardwire 0.0.3 → 0.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.
- package/README.md +91 -3
- package/dist/index.d.mts +72 -6
- package/dist/index.d.ts +72 -6
- package/dist/index.js +224 -18
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +221 -17
- package/dist/index.mjs.map +1 -1
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -30,7 +30,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
30
30
|
// src/index.ts
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
|
-
createShardwire: () => createShardwire
|
|
33
|
+
createShardwire: () => createShardwire,
|
|
34
|
+
fromSafeParseSchema: () => fromSafeParseSchema,
|
|
35
|
+
fromZodSchema: () => fromZodSchema
|
|
34
36
|
});
|
|
35
37
|
module.exports = __toCommonJS(index_exports);
|
|
36
38
|
|
|
@@ -100,6 +102,14 @@ function stringifyEnvelope(envelope) {
|
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
// src/runtime/validation.ts
|
|
105
|
+
var PayloadValidationError = class extends Error {
|
|
106
|
+
constructor(message, details) {
|
|
107
|
+
super(message);
|
|
108
|
+
this.details = details;
|
|
109
|
+
this.name = "PayloadValidationError";
|
|
110
|
+
}
|
|
111
|
+
details;
|
|
112
|
+
};
|
|
103
113
|
function isNonEmptyString(value) {
|
|
104
114
|
return typeof value === "string" && value.trim().length > 0;
|
|
105
115
|
}
|
|
@@ -144,6 +154,9 @@ function assertConsumerOptions(options) {
|
|
|
144
154
|
if (options.secretId !== void 0 && !isNonEmptyString(options.secretId)) {
|
|
145
155
|
throw new Error("Consumer option `secretId` must be a non-empty string.");
|
|
146
156
|
}
|
|
157
|
+
if (options.clientName !== void 0 && !isNonEmptyString(options.clientName)) {
|
|
158
|
+
throw new Error("Consumer option `clientName` must be a non-empty string.");
|
|
159
|
+
}
|
|
147
160
|
if (options.requestTimeoutMs !== void 0) {
|
|
148
161
|
assertPositiveNumber("requestTimeoutMs", options.requestTimeoutMs);
|
|
149
162
|
}
|
|
@@ -166,9 +179,54 @@ function assertJsonPayload(kind, name, payload) {
|
|
|
166
179
|
throw new Error(`${kind} "${name}" payload must be JSON-serializable.`);
|
|
167
180
|
}
|
|
168
181
|
}
|
|
182
|
+
function normalizeSchemaIssues(error) {
|
|
183
|
+
if (!error || typeof error !== "object") {
|
|
184
|
+
return void 0;
|
|
185
|
+
}
|
|
186
|
+
const issues = error.issues;
|
|
187
|
+
if (!Array.isArray(issues)) {
|
|
188
|
+
return void 0;
|
|
189
|
+
}
|
|
190
|
+
const normalized = issues.map((issue) => {
|
|
191
|
+
if (!issue || typeof issue !== "object") {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
const message = issue.message;
|
|
195
|
+
const rawPath = issue.path;
|
|
196
|
+
if (typeof message !== "string") {
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
const path = Array.isArray(rawPath) && rawPath.length > 0 ? rawPath.map((segment) => typeof segment === "string" || typeof segment === "number" ? String(segment) : "").filter(Boolean).join(".") : "";
|
|
200
|
+
return { path, message };
|
|
201
|
+
}).filter((issue) => Boolean(issue));
|
|
202
|
+
return normalized.length > 0 ? normalized : void 0;
|
|
203
|
+
}
|
|
204
|
+
function parsePayloadWithSchema(schema, payload, context) {
|
|
205
|
+
if (!schema) {
|
|
206
|
+
return payload;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
return schema.parse(payload);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
const message = error instanceof Error && error.message.trim().length > 0 ? error.message : `Payload validation failed for ${context.stage} "${context.name}".`;
|
|
212
|
+
const issues = normalizeSchemaIssues(error);
|
|
213
|
+
throw new PayloadValidationError(message, {
|
|
214
|
+
...context,
|
|
215
|
+
...issues ? { issues } : {}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
169
219
|
|
|
170
220
|
// src/consumer/index.ts
|
|
171
221
|
var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
|
|
222
|
+
var CommandRequestError = class extends Error {
|
|
223
|
+
constructor(code, message) {
|
|
224
|
+
super(message);
|
|
225
|
+
this.code = code;
|
|
226
|
+
this.name = "CommandRequestError";
|
|
227
|
+
}
|
|
228
|
+
code;
|
|
229
|
+
};
|
|
172
230
|
function createConsumerShardwire(options) {
|
|
173
231
|
const logger = withLogger(options.logger);
|
|
174
232
|
const reconnectEnabled = options.reconnect?.enabled ?? true;
|
|
@@ -185,8 +243,12 @@ function createConsumerShardwire(options) {
|
|
|
185
243
|
let connectResolve = null;
|
|
186
244
|
let connectReject = null;
|
|
187
245
|
let authTimeoutTimer = null;
|
|
246
|
+
let currentConnectionId = null;
|
|
188
247
|
const pendingRequests = /* @__PURE__ */ new Map();
|
|
189
248
|
const eventHandlers = /* @__PURE__ */ new Map();
|
|
249
|
+
const connectedHandlers = /* @__PURE__ */ new Set();
|
|
250
|
+
const disconnectedHandlers = /* @__PURE__ */ new Set();
|
|
251
|
+
const reconnectingHandlers = /* @__PURE__ */ new Set();
|
|
190
252
|
function clearAuthTimeout() {
|
|
191
253
|
if (authTimeoutTimer) {
|
|
192
254
|
clearTimeout(authTimeoutTimer);
|
|
@@ -215,10 +277,10 @@ function createConsumerShardwire(options) {
|
|
|
215
277
|
}
|
|
216
278
|
socket.send(data);
|
|
217
279
|
}
|
|
218
|
-
function rejectAllPending(reason) {
|
|
280
|
+
function rejectAllPending(code, reason) {
|
|
219
281
|
for (const [requestId, pending] of pendingRequests.entries()) {
|
|
220
282
|
clearTimeout(pending.timer);
|
|
221
|
-
pending.reject(new
|
|
283
|
+
pending.reject(new CommandRequestError(code, reason));
|
|
222
284
|
pendingRequests.delete(requestId);
|
|
223
285
|
}
|
|
224
286
|
}
|
|
@@ -228,6 +290,13 @@ function createConsumerShardwire(options) {
|
|
|
228
290
|
}
|
|
229
291
|
const delay = getBackoffDelay(reconnectAttempts, { initialDelayMs, maxDelayMs, jitter });
|
|
230
292
|
reconnectAttempts += 1;
|
|
293
|
+
for (const handler of reconnectingHandlers) {
|
|
294
|
+
try {
|
|
295
|
+
handler({ attempt: reconnectAttempts, delayMs: delay, at: Date.now() });
|
|
296
|
+
} catch (error) {
|
|
297
|
+
logger.warn("Reconnect handler threw an error.", { error: String(error) });
|
|
298
|
+
}
|
|
299
|
+
}
|
|
231
300
|
reconnectTimer = setTimeout(() => {
|
|
232
301
|
reconnectTimer = null;
|
|
233
302
|
void connect().catch((error) => {
|
|
@@ -266,9 +335,11 @@ function createConsumerShardwire(options) {
|
|
|
266
335
|
socket.on("open", () => {
|
|
267
336
|
reconnectAttempts = 0;
|
|
268
337
|
isAuthed = false;
|
|
338
|
+
currentConnectionId = null;
|
|
269
339
|
const hello = makeEnvelope("auth.hello", {
|
|
270
340
|
secret: options.secret,
|
|
271
|
-
secretId: options.secretId
|
|
341
|
+
secretId: options.secretId,
|
|
342
|
+
clientName: options.clientName
|
|
272
343
|
});
|
|
273
344
|
socket?.send(stringifyEnvelope(hello));
|
|
274
345
|
authTimeoutTimer = setTimeout(() => {
|
|
@@ -285,6 +356,18 @@ function createConsumerShardwire(options) {
|
|
|
285
356
|
switch (envelope.type) {
|
|
286
357
|
case "auth.ok":
|
|
287
358
|
isAuthed = true;
|
|
359
|
+
if (envelope.payload && typeof envelope.payload === "object" && "connectionId" in envelope.payload && typeof envelope.payload.connectionId === "string") {
|
|
360
|
+
currentConnectionId = envelope.payload.connectionId;
|
|
361
|
+
}
|
|
362
|
+
if (currentConnectionId) {
|
|
363
|
+
for (const handler of connectedHandlers) {
|
|
364
|
+
try {
|
|
365
|
+
handler({ connectionId: currentConnectionId, connectedAt: Date.now() });
|
|
366
|
+
} catch (error) {
|
|
367
|
+
logger.warn("Connected handler threw an error.", { error: String(error) });
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
288
371
|
resolveConnect();
|
|
289
372
|
break;
|
|
290
373
|
case "auth.error": {
|
|
@@ -294,7 +377,7 @@ function createConsumerShardwire(options) {
|
|
|
294
377
|
message: payload.message
|
|
295
378
|
});
|
|
296
379
|
rejectConnect(payload.message);
|
|
297
|
-
rejectAllPending("Shardwire authentication failed.");
|
|
380
|
+
rejectAllPending("UNAUTHORIZED", "Shardwire authentication failed.");
|
|
298
381
|
socket?.close();
|
|
299
382
|
break;
|
|
300
383
|
}
|
|
@@ -333,8 +416,22 @@ function createConsumerShardwire(options) {
|
|
|
333
416
|
}
|
|
334
417
|
});
|
|
335
418
|
socket.on("close", () => {
|
|
419
|
+
const willReconnect = !isClosed && reconnectEnabled;
|
|
336
420
|
rejectConnect("Shardwire connection closed.");
|
|
337
421
|
isAuthed = false;
|
|
422
|
+
currentConnectionId = null;
|
|
423
|
+
rejectAllPending("DISCONNECTED", "Shardwire connection closed before command completed.");
|
|
424
|
+
for (const handler of disconnectedHandlers) {
|
|
425
|
+
try {
|
|
426
|
+
handler({
|
|
427
|
+
reason: "Shardwire connection closed.",
|
|
428
|
+
at: Date.now(),
|
|
429
|
+
willReconnect
|
|
430
|
+
});
|
|
431
|
+
} catch (error) {
|
|
432
|
+
logger.warn("Disconnected handler threw an error.", { error: String(error) });
|
|
433
|
+
}
|
|
434
|
+
}
|
|
338
435
|
if (!isClosed) {
|
|
339
436
|
scheduleReconnect();
|
|
340
437
|
}
|
|
@@ -373,7 +470,7 @@ function createConsumerShardwire(options) {
|
|
|
373
470
|
requestId: sendOptions?.requestId ?? "unknown",
|
|
374
471
|
ts: Date.now(),
|
|
375
472
|
error: {
|
|
376
|
-
code: "
|
|
473
|
+
code: "DISCONNECTED",
|
|
377
474
|
message: "Not connected to Shardwire host."
|
|
378
475
|
}
|
|
379
476
|
};
|
|
@@ -383,9 +480,13 @@ function createConsumerShardwire(options) {
|
|
|
383
480
|
const promise = new Promise((resolve, reject) => {
|
|
384
481
|
const timer = setTimeout(() => {
|
|
385
482
|
pendingRequests.delete(requestId);
|
|
386
|
-
reject(new
|
|
483
|
+
reject(new CommandRequestError("TIMEOUT", `Command "${name}" timed out after ${timeoutMs}ms.`));
|
|
387
484
|
}, timeoutMs);
|
|
388
|
-
pendingRequests.set(requestId, {
|
|
485
|
+
pendingRequests.set(requestId, {
|
|
486
|
+
resolve,
|
|
487
|
+
reject: (error) => reject(error),
|
|
488
|
+
timer
|
|
489
|
+
});
|
|
389
490
|
});
|
|
390
491
|
sendRaw(
|
|
391
492
|
stringifyEnvelope(
|
|
@@ -402,13 +503,14 @@ function createConsumerShardwire(options) {
|
|
|
402
503
|
try {
|
|
403
504
|
return await promise;
|
|
404
505
|
} catch (error) {
|
|
506
|
+
const failureCode = error instanceof CommandRequestError ? error.code : !socket || socket.readyState !== 1 ? "DISCONNECTED" : "TIMEOUT";
|
|
405
507
|
return {
|
|
406
508
|
ok: false,
|
|
407
509
|
requestId,
|
|
408
510
|
ts: Date.now(),
|
|
409
511
|
error: {
|
|
410
|
-
code:
|
|
411
|
-
message: error instanceof Error ? error.message : "Command request
|
|
512
|
+
code: failureCode,
|
|
513
|
+
message: error instanceof Error ? error.message : "Command request failed."
|
|
412
514
|
}
|
|
413
515
|
};
|
|
414
516
|
}
|
|
@@ -444,18 +546,43 @@ function createConsumerShardwire(options) {
|
|
|
444
546
|
eventHandlers.delete(name);
|
|
445
547
|
}
|
|
446
548
|
},
|
|
549
|
+
onConnected(handler) {
|
|
550
|
+
connectedHandlers.add(handler);
|
|
551
|
+
return () => {
|
|
552
|
+
connectedHandlers.delete(handler);
|
|
553
|
+
};
|
|
554
|
+
},
|
|
555
|
+
onDisconnected(handler) {
|
|
556
|
+
disconnectedHandlers.add(handler);
|
|
557
|
+
return () => {
|
|
558
|
+
disconnectedHandlers.delete(handler);
|
|
559
|
+
};
|
|
560
|
+
},
|
|
561
|
+
onReconnecting(handler) {
|
|
562
|
+
reconnectingHandlers.add(handler);
|
|
563
|
+
return () => {
|
|
564
|
+
reconnectingHandlers.delete(handler);
|
|
565
|
+
};
|
|
566
|
+
},
|
|
567
|
+
ready() {
|
|
568
|
+
return connect();
|
|
569
|
+
},
|
|
447
570
|
connected() {
|
|
448
571
|
return Boolean(socket && socket.readyState === 1 && isAuthed);
|
|
449
572
|
},
|
|
573
|
+
connectionId() {
|
|
574
|
+
return currentConnectionId;
|
|
575
|
+
},
|
|
450
576
|
async close() {
|
|
451
577
|
isClosed = true;
|
|
452
578
|
isAuthed = false;
|
|
579
|
+
currentConnectionId = null;
|
|
453
580
|
rejectConnect("Shardwire consumer has been closed.");
|
|
454
581
|
if (reconnectTimer) {
|
|
455
582
|
clearTimeout(reconnectTimer);
|
|
456
583
|
reconnectTimer = null;
|
|
457
584
|
}
|
|
458
|
-
rejectAllPending("Shardwire consumer has been closed.");
|
|
585
|
+
rejectAllPending("DISCONNECTED", "Shardwire consumer has been closed.");
|
|
459
586
|
if (!socket) {
|
|
460
587
|
return;
|
|
461
588
|
}
|
|
@@ -795,21 +922,44 @@ function createHostShardwire(options, runtimeHooks) {
|
|
|
795
922
|
context.source = source;
|
|
796
923
|
}
|
|
797
924
|
try {
|
|
798
|
-
const
|
|
925
|
+
const commandValidation = options.validation?.commands?.[payload.name];
|
|
926
|
+
const validatedRequest = parsePayloadWithSchema(commandValidation?.request, payload.data, {
|
|
927
|
+
name: payload.name,
|
|
928
|
+
stage: "command.request"
|
|
929
|
+
});
|
|
930
|
+
const maybePromise = Promise.resolve(handler(validatedRequest, context));
|
|
799
931
|
const value = await withTimeout(
|
|
800
932
|
maybePromise,
|
|
801
933
|
commandTimeoutMs,
|
|
802
934
|
`Command "${payload.name}" timed out after ${commandTimeoutMs}ms.`
|
|
803
935
|
);
|
|
936
|
+
const validatedResponse = parsePayloadWithSchema(commandValidation?.response, value, {
|
|
937
|
+
name: payload.name,
|
|
938
|
+
stage: "command.response"
|
|
939
|
+
});
|
|
804
940
|
const success = {
|
|
805
941
|
ok: true,
|
|
806
942
|
requestId,
|
|
807
943
|
ts: Date.now(),
|
|
808
|
-
data:
|
|
944
|
+
data: validatedResponse
|
|
809
945
|
};
|
|
810
946
|
dedupeCache.set(cacheKey, success);
|
|
811
947
|
return success;
|
|
812
948
|
} catch (error) {
|
|
949
|
+
if (error instanceof PayloadValidationError) {
|
|
950
|
+
const failure2 = {
|
|
951
|
+
ok: false,
|
|
952
|
+
requestId,
|
|
953
|
+
ts: Date.now(),
|
|
954
|
+
error: {
|
|
955
|
+
code: "VALIDATION_ERROR",
|
|
956
|
+
message: error.message,
|
|
957
|
+
details: error.details
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
dedupeCache.set(cacheKey, failure2);
|
|
961
|
+
return failure2;
|
|
962
|
+
}
|
|
813
963
|
const isTimeout = error instanceof Error && /timed out/i.test(error.message);
|
|
814
964
|
const failure = {
|
|
815
965
|
ok: false,
|
|
@@ -836,13 +986,21 @@ function createHostShardwire(options, runtimeHooks) {
|
|
|
836
986
|
},
|
|
837
987
|
emitEvent(name, payload) {
|
|
838
988
|
assertMessageName("event", name);
|
|
839
|
-
|
|
840
|
-
|
|
989
|
+
const validatedPayload = parsePayloadWithSchema(options.validation?.events?.[name], payload, {
|
|
990
|
+
name,
|
|
991
|
+
stage: "event.emit"
|
|
992
|
+
});
|
|
993
|
+
assertJsonPayload("event", name, validatedPayload);
|
|
994
|
+
hostServer.emitEvent(name, validatedPayload, options.name);
|
|
841
995
|
},
|
|
842
996
|
broadcast(name, payload) {
|
|
843
997
|
assertMessageName("event", name);
|
|
844
|
-
|
|
845
|
-
|
|
998
|
+
const validatedPayload = parsePayloadWithSchema(options.validation?.events?.[name], payload, {
|
|
999
|
+
name,
|
|
1000
|
+
stage: "event.emit"
|
|
1001
|
+
});
|
|
1002
|
+
assertJsonPayload("event", name, validatedPayload);
|
|
1003
|
+
hostServer.emitEvent(name, validatedPayload, options.name);
|
|
846
1004
|
},
|
|
847
1005
|
close() {
|
|
848
1006
|
return hostServer.close().then(async () => {
|
|
@@ -852,6 +1010,52 @@ function createHostShardwire(options, runtimeHooks) {
|
|
|
852
1010
|
};
|
|
853
1011
|
}
|
|
854
1012
|
|
|
1013
|
+
// src/schema/index.ts
|
|
1014
|
+
function fromSafeParseSchema(schema) {
|
|
1015
|
+
return {
|
|
1016
|
+
parse(value) {
|
|
1017
|
+
const result = schema.safeParse(value);
|
|
1018
|
+
if (result.success) {
|
|
1019
|
+
return result.data;
|
|
1020
|
+
}
|
|
1021
|
+
const error = new Error(result.error.message);
|
|
1022
|
+
if (result.error.issues) {
|
|
1023
|
+
error.issues = result.error.issues;
|
|
1024
|
+
}
|
|
1025
|
+
throw error;
|
|
1026
|
+
}
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// src/schema/zod.ts
|
|
1031
|
+
function normalizeZodPath(path) {
|
|
1032
|
+
if (path.length === 0) {
|
|
1033
|
+
return "";
|
|
1034
|
+
}
|
|
1035
|
+
return path.filter((segment) => typeof segment === "string" || typeof segment === "number").map((segment) => String(segment)).join(".");
|
|
1036
|
+
}
|
|
1037
|
+
function fromZodSchema(schema) {
|
|
1038
|
+
return fromSafeParseSchema({
|
|
1039
|
+
safeParse(value) {
|
|
1040
|
+
const result = schema.safeParse(value);
|
|
1041
|
+
if (result.success) {
|
|
1042
|
+
return { success: true, data: result.data };
|
|
1043
|
+
}
|
|
1044
|
+
const issues = result.error.issues.map((issue) => ({
|
|
1045
|
+
path: normalizeZodPath(issue.path),
|
|
1046
|
+
message: issue.message
|
|
1047
|
+
}));
|
|
1048
|
+
return {
|
|
1049
|
+
success: false,
|
|
1050
|
+
error: {
|
|
1051
|
+
message: "Schema validation failed.",
|
|
1052
|
+
issues
|
|
1053
|
+
}
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
855
1059
|
// src/index.ts
|
|
856
1060
|
function isHostOptions(options) {
|
|
857
1061
|
return "server" in options;
|
|
@@ -887,6 +1091,8 @@ function createShardwire(options) {
|
|
|
887
1091
|
}
|
|
888
1092
|
// Annotate the CommonJS export names for ESM import in node:
|
|
889
1093
|
0 && (module.exports = {
|
|
890
|
-
createShardwire
|
|
1094
|
+
createShardwire,
|
|
1095
|
+
fromSafeParseSchema,
|
|
1096
|
+
fromZodSchema
|
|
891
1097
|
});
|
|
892
1098
|
//# sourceMappingURL=index.js.map
|