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/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 Error(reason));
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: "TIMEOUT",
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 Error(`Command "${name}" timed out after ${timeoutMs}ms.`));
483
+ reject(new CommandRequestError("TIMEOUT", `Command "${name}" timed out after ${timeoutMs}ms.`));
387
484
  }, timeoutMs);
388
- pendingRequests.set(requestId, { resolve, reject, timer });
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: "TIMEOUT",
411
- message: error instanceof Error ? error.message : "Command request timeout."
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 maybePromise = Promise.resolve(handler(payload.data, context));
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: value
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
- assertJsonPayload("event", name, payload);
840
- hostServer.emitEvent(name, payload, options.name);
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
- assertJsonPayload("event", name, payload);
845
- hostServer.emitEvent(name, payload, options.name);
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