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