shardwire 0.0.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/index.js ADDED
@@ -0,0 +1,855 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ createShardwire: () => createShardwire
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/transport/ws/consumer-socket.ts
38
+ var import_ws = require("ws");
39
+ function createNodeWebSocket(url) {
40
+ return new import_ws.WebSocket(url);
41
+ }
42
+
43
+ // src/utils/id.ts
44
+ var import_node_crypto = require("crypto");
45
+ function createRequestId() {
46
+ return (0, import_node_crypto.randomUUID)();
47
+ }
48
+ function createConnectionId() {
49
+ return (0, import_node_crypto.randomUUID)();
50
+ }
51
+
52
+ // src/utils/backoff.ts
53
+ function getBackoffDelay(attempt, config) {
54
+ const base = Math.min(config.maxDelayMs, config.initialDelayMs * 2 ** attempt);
55
+ if (!config.jitter) {
56
+ return base;
57
+ }
58
+ const spread = Math.floor(base * 0.2);
59
+ const min = Math.max(0, base - spread);
60
+ const max = base + spread;
61
+ return Math.floor(Math.random() * (max - min + 1)) + min;
62
+ }
63
+
64
+ // src/utils/logger.ts
65
+ function withLogger(logger) {
66
+ return {
67
+ debug: logger?.debug ?? (() => void 0),
68
+ info: logger?.info ?? (() => void 0),
69
+ warn: logger?.warn ?? (() => void 0),
70
+ error: logger?.error ?? (() => void 0)
71
+ };
72
+ }
73
+
74
+ // src/core/protocol.ts
75
+ var PROTOCOL_VERSION = 1;
76
+ function makeEnvelope(type, payload, extras) {
77
+ const envelope = {
78
+ v: PROTOCOL_VERSION,
79
+ type,
80
+ ts: Date.now(),
81
+ payload
82
+ };
83
+ if (extras?.requestId) {
84
+ envelope.requestId = extras.requestId;
85
+ }
86
+ if (extras?.source) {
87
+ envelope.source = extras.source;
88
+ }
89
+ return envelope;
90
+ }
91
+ function parseEnvelope(raw) {
92
+ const parsed = JSON.parse(raw);
93
+ if (!parsed || parsed.v !== PROTOCOL_VERSION || typeof parsed.type !== "string") {
94
+ throw new Error("Invalid wire envelope.");
95
+ }
96
+ return parsed;
97
+ }
98
+ function stringifyEnvelope(envelope) {
99
+ return JSON.stringify(envelope);
100
+ }
101
+
102
+ // src/runtime/validation.ts
103
+ function isNonEmptyString(value) {
104
+ return typeof value === "string" && value.trim().length > 0;
105
+ }
106
+ function assertPositiveNumber(name, value) {
107
+ if (typeof value !== "number" || Number.isNaN(value) || value <= 0) {
108
+ throw new Error(`${name} must be a positive number.`);
109
+ }
110
+ }
111
+ function assertHostOptions(options) {
112
+ if (!options.server) {
113
+ throw new Error("Host mode requires a server configuration.");
114
+ }
115
+ assertPositiveNumber("server.port", options.server.port);
116
+ if (!isNonEmptyString(options.server.secret)) {
117
+ throw new Error("server.secret is required.");
118
+ }
119
+ if (options.server.heartbeatMs !== void 0) {
120
+ assertPositiveNumber("server.heartbeatMs", options.server.heartbeatMs);
121
+ }
122
+ if (options.server.commandTimeoutMs !== void 0) {
123
+ assertPositiveNumber("server.commandTimeoutMs", options.server.commandTimeoutMs);
124
+ }
125
+ if (options.server.maxPayloadBytes !== void 0) {
126
+ assertPositiveNumber("server.maxPayloadBytes", options.server.maxPayloadBytes);
127
+ }
128
+ }
129
+ function assertConsumerOptions(options) {
130
+ if (!isNonEmptyString(options.url)) {
131
+ throw new Error("Consumer mode requires `url`.");
132
+ }
133
+ if (!isNonEmptyString(options.secret)) {
134
+ throw new Error("Consumer mode requires `secret`.");
135
+ }
136
+ if (options.requestTimeoutMs !== void 0) {
137
+ assertPositiveNumber("requestTimeoutMs", options.requestTimeoutMs);
138
+ }
139
+ if (options.reconnect?.initialDelayMs !== void 0) {
140
+ assertPositiveNumber("reconnect.initialDelayMs", options.reconnect.initialDelayMs);
141
+ }
142
+ if (options.reconnect?.maxDelayMs !== void 0) {
143
+ assertPositiveNumber("reconnect.maxDelayMs", options.reconnect.maxDelayMs);
144
+ }
145
+ }
146
+ function assertMessageName(kind, name) {
147
+ if (!isNonEmptyString(name)) {
148
+ throw new Error(`${kind} name must be a non-empty string.`);
149
+ }
150
+ }
151
+ function assertJsonPayload(kind, name, payload) {
152
+ try {
153
+ JSON.stringify(payload);
154
+ } catch {
155
+ throw new Error(`${kind} "${name}" payload must be JSON-serializable.`);
156
+ }
157
+ }
158
+
159
+ // src/consumer/index.ts
160
+ var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
161
+ function createConsumerShardwire(options) {
162
+ const logger = withLogger(options.logger);
163
+ const reconnectEnabled = options.reconnect?.enabled ?? true;
164
+ const initialDelayMs = options.reconnect?.initialDelayMs ?? 500;
165
+ const maxDelayMs = options.reconnect?.maxDelayMs ?? 1e4;
166
+ const jitter = options.reconnect?.jitter ?? true;
167
+ const requestTimeoutMs = options.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
168
+ let socket = null;
169
+ let isClosed = false;
170
+ let isAuthed = false;
171
+ let reconnectAttempts = 0;
172
+ let reconnectTimer = null;
173
+ let connectPromise = null;
174
+ let connectResolve = null;
175
+ let connectReject = null;
176
+ let authTimeoutTimer = null;
177
+ const pendingRequests = /* @__PURE__ */ new Map();
178
+ const eventHandlers = /* @__PURE__ */ new Map();
179
+ function clearAuthTimeout() {
180
+ if (authTimeoutTimer) {
181
+ clearTimeout(authTimeoutTimer);
182
+ authTimeoutTimer = null;
183
+ }
184
+ }
185
+ function resolveConnect() {
186
+ clearAuthTimeout();
187
+ connectResolve?.();
188
+ connectResolve = null;
189
+ connectReject = null;
190
+ connectPromise = null;
191
+ }
192
+ function rejectConnect(message) {
193
+ clearAuthTimeout();
194
+ if (connectReject) {
195
+ connectReject(new Error(message));
196
+ }
197
+ connectResolve = null;
198
+ connectReject = null;
199
+ connectPromise = null;
200
+ }
201
+ function sendRaw(data) {
202
+ if (!socket || socket.readyState !== 1) {
203
+ throw new Error("Shardwire consumer is not connected.");
204
+ }
205
+ socket.send(data);
206
+ }
207
+ function rejectAllPending(reason) {
208
+ for (const [requestId, pending] of pendingRequests.entries()) {
209
+ clearTimeout(pending.timer);
210
+ pending.reject(new Error(reason));
211
+ pendingRequests.delete(requestId);
212
+ }
213
+ }
214
+ function scheduleReconnect() {
215
+ if (isClosed || !reconnectEnabled || reconnectTimer) {
216
+ return;
217
+ }
218
+ const delay = getBackoffDelay(reconnectAttempts, { initialDelayMs, maxDelayMs, jitter });
219
+ reconnectAttempts += 1;
220
+ reconnectTimer = setTimeout(() => {
221
+ reconnectTimer = null;
222
+ void connect().catch((error) => {
223
+ logger.warn("Reconnect attempt failed.", { error: String(error) });
224
+ });
225
+ }, delay);
226
+ }
227
+ function handleEvent(name, payload, meta) {
228
+ const handlers = eventHandlers.get(name);
229
+ if (!handlers || handlers.size === 0) {
230
+ return;
231
+ }
232
+ for (const handler of handlers) {
233
+ try {
234
+ handler(payload, meta);
235
+ } catch (error) {
236
+ logger.warn("Event handler threw an error.", { name, error: String(error) });
237
+ }
238
+ }
239
+ }
240
+ async function connect() {
241
+ if (isClosed) {
242
+ return;
243
+ }
244
+ if (socket && socket.readyState === 1 && isAuthed) {
245
+ return;
246
+ }
247
+ if (connectPromise) {
248
+ return connectPromise;
249
+ }
250
+ connectPromise = new Promise((resolve, reject) => {
251
+ connectResolve = resolve;
252
+ connectReject = reject;
253
+ });
254
+ socket = options.webSocketFactory ? options.webSocketFactory(options.url) : createNodeWebSocket(options.url);
255
+ socket.on("open", () => {
256
+ reconnectAttempts = 0;
257
+ isAuthed = false;
258
+ const hello = makeEnvelope("auth.hello", { secret: options.secret });
259
+ socket?.send(stringifyEnvelope(hello));
260
+ authTimeoutTimer = setTimeout(() => {
261
+ if (!isAuthed) {
262
+ rejectConnect("Shardwire auth timed out.");
263
+ socket?.close();
264
+ }
265
+ }, requestTimeoutMs);
266
+ });
267
+ socket.on("message", (raw) => {
268
+ try {
269
+ const serialized = typeof raw === "string" ? raw : String(raw);
270
+ const envelope = parseEnvelope(serialized);
271
+ switch (envelope.type) {
272
+ case "auth.ok":
273
+ isAuthed = true;
274
+ resolveConnect();
275
+ break;
276
+ case "auth.error": {
277
+ const payload = envelope.payload;
278
+ logger.error("Authentication failed for consumer.", {
279
+ code: payload.code,
280
+ message: payload.message
281
+ });
282
+ rejectConnect(payload.message);
283
+ rejectAllPending("Shardwire authentication failed.");
284
+ socket?.close();
285
+ break;
286
+ }
287
+ case "command.result":
288
+ case "command.error": {
289
+ const requestId = envelope.requestId;
290
+ if (!requestId) {
291
+ return;
292
+ }
293
+ const pending = pendingRequests.get(requestId);
294
+ if (!pending) {
295
+ return;
296
+ }
297
+ clearTimeout(pending.timer);
298
+ pending.resolve(envelope.payload);
299
+ pendingRequests.delete(requestId);
300
+ break;
301
+ }
302
+ case "event.emit": {
303
+ const payload = envelope.payload;
304
+ const meta = { ts: envelope.ts };
305
+ if (envelope.source) {
306
+ meta.source = envelope.source;
307
+ }
308
+ handleEvent(payload.name, payload.data, meta);
309
+ break;
310
+ }
311
+ case "ping":
312
+ sendRaw(stringifyEnvelope(makeEnvelope("pong", {})));
313
+ break;
314
+ default:
315
+ break;
316
+ }
317
+ } catch (error) {
318
+ logger.warn("Failed to parse consumer message.", { error: String(error) });
319
+ }
320
+ });
321
+ socket.on("close", () => {
322
+ rejectConnect("Shardwire connection closed.");
323
+ isAuthed = false;
324
+ if (!isClosed) {
325
+ scheduleReconnect();
326
+ }
327
+ });
328
+ socket.on("error", (error) => {
329
+ logger.warn("Consumer socket error.", { error: String(error) });
330
+ });
331
+ return connectPromise;
332
+ }
333
+ void connect().catch((error) => {
334
+ logger.warn("Initial connection attempt failed.", { error: String(error) });
335
+ });
336
+ return {
337
+ mode: "consumer",
338
+ async send(name, payload, sendOptions) {
339
+ assertMessageName("command", name);
340
+ assertJsonPayload("command", name, payload);
341
+ if (!isAuthed) {
342
+ try {
343
+ await connect();
344
+ } catch (error) {
345
+ return {
346
+ ok: false,
347
+ requestId: sendOptions?.requestId ?? "unknown",
348
+ ts: Date.now(),
349
+ error: {
350
+ code: "AUTH_ERROR",
351
+ message: error instanceof Error ? error.message : "Failed to authenticate."
352
+ }
353
+ };
354
+ }
355
+ }
356
+ if (!socket || socket.readyState !== 1) {
357
+ return {
358
+ ok: false,
359
+ requestId: sendOptions?.requestId ?? "unknown",
360
+ ts: Date.now(),
361
+ error: {
362
+ code: "TIMEOUT",
363
+ message: "Not connected to Shardwire host."
364
+ }
365
+ };
366
+ }
367
+ const requestId = sendOptions?.requestId ?? createRequestId();
368
+ const timeoutMs = sendOptions?.timeoutMs ?? requestTimeoutMs;
369
+ const promise = new Promise((resolve, reject) => {
370
+ const timer = setTimeout(() => {
371
+ pendingRequests.delete(requestId);
372
+ reject(new Error(`Command "${name}" timed out after ${timeoutMs}ms.`));
373
+ }, timeoutMs);
374
+ pendingRequests.set(requestId, { resolve, reject, timer });
375
+ });
376
+ sendRaw(
377
+ stringifyEnvelope(
378
+ makeEnvelope(
379
+ "command.request",
380
+ {
381
+ name,
382
+ data: payload
383
+ },
384
+ { requestId }
385
+ )
386
+ )
387
+ );
388
+ try {
389
+ return await promise;
390
+ } catch (error) {
391
+ return {
392
+ ok: false,
393
+ requestId,
394
+ ts: Date.now(),
395
+ error: {
396
+ code: "TIMEOUT",
397
+ message: error instanceof Error ? error.message : "Command request timeout."
398
+ }
399
+ };
400
+ }
401
+ },
402
+ on(name, handler) {
403
+ assertMessageName("event", name);
404
+ const casted = handler;
405
+ const existing = eventHandlers.get(name);
406
+ if (existing) {
407
+ existing.add(casted);
408
+ } else {
409
+ eventHandlers.set(name, /* @__PURE__ */ new Set([casted]));
410
+ }
411
+ return () => {
412
+ const handlers = eventHandlers.get(name);
413
+ if (!handlers) {
414
+ return;
415
+ }
416
+ handlers.delete(casted);
417
+ if (handlers.size === 0) {
418
+ eventHandlers.delete(name);
419
+ }
420
+ };
421
+ },
422
+ off(name, handler) {
423
+ assertMessageName("event", name);
424
+ const handlers = eventHandlers.get(name);
425
+ if (!handlers) {
426
+ return;
427
+ }
428
+ handlers.delete(handler);
429
+ if (handlers.size === 0) {
430
+ eventHandlers.delete(name);
431
+ }
432
+ },
433
+ connected() {
434
+ return Boolean(socket && socket.readyState === 1 && isAuthed);
435
+ },
436
+ async close() {
437
+ isClosed = true;
438
+ isAuthed = false;
439
+ rejectConnect("Shardwire consumer has been closed.");
440
+ if (reconnectTimer) {
441
+ clearTimeout(reconnectTimer);
442
+ reconnectTimer = null;
443
+ }
444
+ rejectAllPending("Shardwire consumer has been closed.");
445
+ if (!socket) {
446
+ return;
447
+ }
448
+ await new Promise((resolve) => {
449
+ const current = socket;
450
+ if (!current) {
451
+ resolve();
452
+ return;
453
+ }
454
+ current.once("close", () => resolve());
455
+ current.close();
456
+ });
457
+ socket = null;
458
+ }
459
+ };
460
+ }
461
+
462
+ // src/discord/client.ts
463
+ async function resolveDiscordClient(options) {
464
+ if (options.client) {
465
+ return { client: options.client, owned: false };
466
+ }
467
+ if (!options.token) {
468
+ return { owned: false };
469
+ }
470
+ const discordModule = await import("discord.js");
471
+ const created = new discordModule.Client({
472
+ intents: []
473
+ });
474
+ await created.login(options.token);
475
+ return { client: created, owned: true };
476
+ }
477
+
478
+ // src/runtime/reliability.ts
479
+ async function withTimeout(promise, timeoutMs, timeoutMessage = "Operation timed out.") {
480
+ return new Promise((resolve, reject) => {
481
+ const timeout = setTimeout(() => {
482
+ reject(new Error(timeoutMessage));
483
+ }, timeoutMs);
484
+ promise.then((value) => {
485
+ clearTimeout(timeout);
486
+ resolve(value);
487
+ }).catch((error) => {
488
+ clearTimeout(timeout);
489
+ reject(error);
490
+ });
491
+ });
492
+ }
493
+ var DedupeCache = class {
494
+ constructor(ttlMs) {
495
+ this.ttlMs = ttlMs;
496
+ }
497
+ ttlMs;
498
+ cache = /* @__PURE__ */ new Map();
499
+ get(key) {
500
+ const entry = this.cache.get(key);
501
+ if (!entry) {
502
+ return void 0;
503
+ }
504
+ if (entry.expiresAt <= Date.now()) {
505
+ this.cache.delete(key);
506
+ return void 0;
507
+ }
508
+ return entry.value;
509
+ }
510
+ set(key, value) {
511
+ this.cache.set(key, { value, expiresAt: Date.now() + this.ttlMs });
512
+ }
513
+ };
514
+
515
+ // src/transport/ws/host-server.ts
516
+ var import_ws2 = require("ws");
517
+
518
+ // src/runtime/security.ts
519
+ var import_node_crypto2 = require("crypto");
520
+ function isSecretValid(provided, expected) {
521
+ const providedBuffer = Buffer.from(provided);
522
+ const expectedBuffer = Buffer.from(expected);
523
+ if (providedBuffer.length !== expectedBuffer.length) {
524
+ return false;
525
+ }
526
+ return (0, import_node_crypto2.timingSafeEqual)(providedBuffer, expectedBuffer);
527
+ }
528
+
529
+ // src/transport/ws/host-server.ts
530
+ var CLOSE_AUTH_REQUIRED = 4001;
531
+ var CLOSE_AUTH_FAILED = 4003;
532
+ var CLOSE_INVALID_PAYLOAD = 4004;
533
+ var HostWebSocketServer = class {
534
+ constructor(config) {
535
+ this.config = config;
536
+ const serverConfig = config.options.server;
537
+ this.heartbeatMs = serverConfig.heartbeatMs ?? 3e4;
538
+ this.logger = withLogger(config.options.logger);
539
+ this.wss = new import_ws2.WebSocketServer({
540
+ host: serverConfig.host,
541
+ port: serverConfig.port,
542
+ path: serverConfig.path ?? "/shardwire",
543
+ maxPayload: serverConfig.maxPayloadBytes ?? 65536
544
+ });
545
+ this.wss.on("connection", (socket, request) => this.handleConnection(socket, request));
546
+ this.wss.on(
547
+ "error",
548
+ (error) => this.logger.error("Shardwire host server error.", { error: String(error) })
549
+ );
550
+ this.interval = setInterval(() => {
551
+ this.checkHeartbeats();
552
+ }, this.heartbeatMs);
553
+ }
554
+ config;
555
+ wss;
556
+ connections = /* @__PURE__ */ new Map();
557
+ logger;
558
+ heartbeatMs;
559
+ authTimeoutMs = 5e3;
560
+ interval;
561
+ emitEvent(name, data, source) {
562
+ const envelope = makeEnvelope(
563
+ "event.emit",
564
+ { name, data },
565
+ source ? { source } : void 0
566
+ );
567
+ const raw = stringifyEnvelope(envelope);
568
+ for (const state of this.connections.values()) {
569
+ if (!state.authenticated) {
570
+ continue;
571
+ }
572
+ this.safeSend(state.socket, raw);
573
+ }
574
+ }
575
+ async close() {
576
+ clearInterval(this.interval);
577
+ for (const connection of this.connections.values()) {
578
+ connection.socket.close();
579
+ }
580
+ this.connections.clear();
581
+ await new Promise((resolve, reject) => {
582
+ this.wss.close((error) => {
583
+ if (error) {
584
+ reject(error);
585
+ return;
586
+ }
587
+ resolve();
588
+ });
589
+ });
590
+ }
591
+ handleConnection(socket, request) {
592
+ const allowlist = this.config.options.server.corsOrigins;
593
+ if (allowlist && allowlist.length > 0) {
594
+ const origin = request.headers.origin;
595
+ if (!origin || !allowlist.includes(origin)) {
596
+ socket.close(CLOSE_AUTH_FAILED, "Origin not allowed.");
597
+ return;
598
+ }
599
+ }
600
+ const state = {
601
+ id: createConnectionId(),
602
+ socket,
603
+ authenticated: false,
604
+ lastHeartbeatAt: Date.now()
605
+ };
606
+ this.connections.set(socket, state);
607
+ const authTimer = setTimeout(() => {
608
+ if (!state.authenticated) {
609
+ socket.close(CLOSE_AUTH_REQUIRED, "Authentication required.");
610
+ }
611
+ }, this.authTimeoutMs);
612
+ socket.on("message", async (raw) => {
613
+ try {
614
+ const parsed = parseEnvelope(raw.toString());
615
+ await this.handleMessage(state, parsed);
616
+ } catch (error) {
617
+ this.logger.warn("Invalid message payload from client.", { error: String(error) });
618
+ socket.close(CLOSE_INVALID_PAYLOAD, "Invalid payload.");
619
+ }
620
+ });
621
+ socket.on("close", () => {
622
+ clearTimeout(authTimer);
623
+ this.connections.delete(socket);
624
+ });
625
+ socket.on(
626
+ "error",
627
+ (error) => this.logger.warn("Socket error.", { connectionId: state.id, error: String(error) })
628
+ );
629
+ }
630
+ async handleMessage(state, envelope) {
631
+ if (envelope.type === "ping") {
632
+ state.lastHeartbeatAt = Date.now();
633
+ this.safeSend(state.socket, stringifyEnvelope(makeEnvelope("pong", {})));
634
+ return;
635
+ }
636
+ if (envelope.type === "pong") {
637
+ state.lastHeartbeatAt = Date.now();
638
+ return;
639
+ }
640
+ if (!state.authenticated) {
641
+ if (envelope.type !== "auth.hello") {
642
+ state.socket.close(CLOSE_AUTH_REQUIRED, "Authentication required.");
643
+ return;
644
+ }
645
+ const payload = envelope.payload;
646
+ if (!payload?.secret || !isSecretValid(payload.secret, this.config.options.server.secret)) {
647
+ this.safeSend(
648
+ state.socket,
649
+ stringifyEnvelope(
650
+ makeEnvelope("auth.error", {
651
+ code: "AUTH_ERROR",
652
+ message: "Invalid shared secret."
653
+ })
654
+ )
655
+ );
656
+ state.socket.close(CLOSE_AUTH_FAILED, "Invalid secret.");
657
+ return;
658
+ }
659
+ state.authenticated = true;
660
+ state.lastHeartbeatAt = Date.now();
661
+ if (payload.clientName) {
662
+ state.clientName = payload.clientName;
663
+ }
664
+ this.safeSend(
665
+ state.socket,
666
+ stringifyEnvelope(makeEnvelope("auth.ok", { connectionId: state.id }))
667
+ );
668
+ return;
669
+ }
670
+ if (envelope.type === "command.request") {
671
+ const payload = envelope.payload;
672
+ if (!envelope.requestId || !payload?.name) {
673
+ const invalid = {
674
+ ok: false,
675
+ requestId: envelope.requestId ?? "unknown",
676
+ ts: Date.now(),
677
+ error: {
678
+ code: "VALIDATION_ERROR",
679
+ message: "Invalid command request envelope."
680
+ }
681
+ };
682
+ this.safeSend(
683
+ state.socket,
684
+ stringifyEnvelope(makeEnvelope("command.error", invalid, { requestId: invalid.requestId }))
685
+ );
686
+ return;
687
+ }
688
+ const response = await this.config.onCommandRequest(
689
+ state,
690
+ payload,
691
+ envelope.requestId,
692
+ envelope.source
693
+ );
694
+ const responseType = response.ok ? "command.result" : "command.error";
695
+ this.safeSend(
696
+ state.socket,
697
+ stringifyEnvelope(makeEnvelope(responseType, response, { requestId: response.requestId }))
698
+ );
699
+ return;
700
+ }
701
+ }
702
+ checkHeartbeats() {
703
+ const now = Date.now();
704
+ const threshold = this.heartbeatMs * 2;
705
+ for (const state of this.connections.values()) {
706
+ if (!state.authenticated) {
707
+ continue;
708
+ }
709
+ if (now - state.lastHeartbeatAt > threshold) {
710
+ state.socket.terminate();
711
+ this.connections.delete(state.socket);
712
+ continue;
713
+ }
714
+ this.safeSend(state.socket, stringifyEnvelope(makeEnvelope("ping", {})));
715
+ }
716
+ }
717
+ safeSend(socket, payload) {
718
+ if (socket.readyState === 1) {
719
+ socket.send(payload);
720
+ }
721
+ }
722
+ };
723
+
724
+ // src/host/index.ts
725
+ var DEFAULT_COMMAND_TIMEOUT_MS = 1e4;
726
+ function createHostShardwire(options, runtimeHooks) {
727
+ const commandHandlers = /* @__PURE__ */ new Map();
728
+ const commandTimeoutMs = options.server.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
729
+ const dedupeCache = new DedupeCache(commandTimeoutMs * 2);
730
+ const hostServer = new HostWebSocketServer({
731
+ options,
732
+ onCommandRequest: async (connection, payload, requestId, source) => {
733
+ const cacheKey = `${requestId}:${payload.name}`;
734
+ const cached = dedupeCache.get(cacheKey);
735
+ if (cached) {
736
+ return cached;
737
+ }
738
+ const handler = commandHandlers.get(payload.name);
739
+ if (!handler) {
740
+ const failure = {
741
+ ok: false,
742
+ requestId,
743
+ ts: Date.now(),
744
+ error: {
745
+ code: "COMMAND_NOT_FOUND",
746
+ message: `No command handler registered for "${payload.name}".`
747
+ }
748
+ };
749
+ dedupeCache.set(cacheKey, failure);
750
+ return failure;
751
+ }
752
+ const context = {
753
+ requestId,
754
+ connectionId: connection.id,
755
+ receivedAt: Date.now()
756
+ };
757
+ if (source) {
758
+ context.source = source;
759
+ }
760
+ try {
761
+ const maybePromise = Promise.resolve(handler(payload.data, context));
762
+ const value = await withTimeout(
763
+ maybePromise,
764
+ commandTimeoutMs,
765
+ `Command "${payload.name}" timed out after ${commandTimeoutMs}ms.`
766
+ );
767
+ const success = {
768
+ ok: true,
769
+ requestId,
770
+ ts: Date.now(),
771
+ data: value
772
+ };
773
+ dedupeCache.set(cacheKey, success);
774
+ return success;
775
+ } catch (error) {
776
+ const isTimeout = error instanceof Error && /timed out/i.test(error.message);
777
+ const failure = {
778
+ ok: false,
779
+ requestId,
780
+ ts: Date.now(),
781
+ error: {
782
+ code: isTimeout ? "TIMEOUT" : "INTERNAL_ERROR",
783
+ message: error instanceof Error ? error.message : "Unknown command execution error."
784
+ }
785
+ };
786
+ dedupeCache.set(cacheKey, failure);
787
+ return failure;
788
+ }
789
+ }
790
+ });
791
+ return {
792
+ mode: "host",
793
+ onCommand(name, handler) {
794
+ assertMessageName("command", name);
795
+ commandHandlers.set(name, handler);
796
+ return () => {
797
+ commandHandlers.delete(name);
798
+ };
799
+ },
800
+ emitEvent(name, payload) {
801
+ assertMessageName("event", name);
802
+ assertJsonPayload("event", name, payload);
803
+ hostServer.emitEvent(name, payload, options.name);
804
+ },
805
+ broadcast(name, payload) {
806
+ assertMessageName("event", name);
807
+ assertJsonPayload("event", name, payload);
808
+ hostServer.emitEvent(name, payload, options.name);
809
+ },
810
+ close() {
811
+ return hostServer.close().then(async () => {
812
+ await runtimeHooks?.onClose?.();
813
+ });
814
+ }
815
+ };
816
+ }
817
+
818
+ // src/index.ts
819
+ function isHostOptions(options) {
820
+ return "server" in options;
821
+ }
822
+ function createShardwire(options) {
823
+ if (!isHostOptions(options)) {
824
+ assertConsumerOptions(options);
825
+ return createConsumerShardwire(options);
826
+ }
827
+ assertHostOptions(options);
828
+ let ownedClientPromise;
829
+ if (!options.client && options.token) {
830
+ ownedClientPromise = resolveDiscordClient(options).then((state) => {
831
+ if (!state.owned || !state.client) {
832
+ return void 0;
833
+ }
834
+ return {
835
+ destroy: () => state.client?.destroy()
836
+ };
837
+ }).catch((error) => {
838
+ options.logger?.error?.("Failed to initialize discord.js client from token.", {
839
+ error: String(error)
840
+ });
841
+ return void 0;
842
+ });
843
+ }
844
+ return createHostShardwire(options, {
845
+ onClose: async () => {
846
+ const owned = await ownedClientPromise;
847
+ owned?.destroy();
848
+ }
849
+ });
850
+ }
851
+ // Annotate the CommonJS export names for ESM import in node:
852
+ 0 && (module.exports = {
853
+ createShardwire
854
+ });
855
+ //# sourceMappingURL=index.js.map