@igoforth/ws-rpc 1.0.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.
Files changed (100) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +446 -0
  3. package/dist/adapters/client.d.ts +117 -0
  4. package/dist/adapters/client.js +241 -0
  5. package/dist/adapters/cloudflare-do.d.ts +72 -0
  6. package/dist/adapters/cloudflare-do.js +192 -0
  7. package/dist/adapters/index.d.ts +13 -0
  8. package/dist/adapters/index.js +16 -0
  9. package/dist/adapters/server.d.ts +10 -0
  10. package/dist/adapters/server.js +122 -0
  11. package/dist/adapters/types.d.ts +125 -0
  12. package/dist/adapters/types.js +3 -0
  13. package/dist/codecs/cbor.d.ts +16 -0
  14. package/dist/codecs/cbor.js +36 -0
  15. package/dist/codecs/factory.d.ts +3 -0
  16. package/dist/codecs/factory.js +3 -0
  17. package/dist/codecs/index.d.ts +5 -0
  18. package/dist/codecs/index.js +5 -0
  19. package/dist/codecs/json.d.ts +4 -0
  20. package/dist/codecs/json.js +4 -0
  21. package/dist/codecs/msgpack.d.ts +16 -0
  22. package/dist/codecs/msgpack.js +34 -0
  23. package/dist/codecs-BmYG2d_U.js +0 -0
  24. package/dist/default-BkrMd28n.js +253 -0
  25. package/dist/default-xDNNMrg0.d.ts +129 -0
  26. package/dist/durable-MZjkvyS6.js +165 -0
  27. package/dist/errors-5BfreE63.js +96 -0
  28. package/dist/errors.d.ts +69 -0
  29. package/dist/errors.js +7 -0
  30. package/dist/factory-3ziwTuZe.js +132 -0
  31. package/dist/factory-C1v0AEHY.d.ts +101 -0
  32. package/dist/index-Be7jjS77.d.ts +1 -0
  33. package/dist/index.d.ts +14 -0
  34. package/dist/index.js +14 -0
  35. package/dist/interface-C4S-WCqW.d.ts +120 -0
  36. package/dist/json-54Z2bIIs.d.ts +22 -0
  37. package/dist/json-Bshec-bZ.js +41 -0
  38. package/dist/memory-Bqb3KEVr.js +48 -0
  39. package/dist/memory-D1nGjzzH.d.ts +41 -0
  40. package/dist/multi-peer-BAi9yVzp.js +242 -0
  41. package/dist/peers/default.d.ts +8 -0
  42. package/dist/peers/default.js +8 -0
  43. package/dist/peers/durable.d.ts +136 -0
  44. package/dist/peers/durable.js +9 -0
  45. package/dist/peers/index.d.ts +10 -0
  46. package/dist/peers/index.js +9 -0
  47. package/dist/protocol-DA84zrc2.d.ts +211 -0
  48. package/dist/protocol-_mpoOPp6.js +192 -0
  49. package/dist/protocol.d.ts +6 -0
  50. package/dist/protocol.js +6 -0
  51. package/dist/reconnect-CGAA_1Gf.js +26 -0
  52. package/dist/reconnect-DbcN0R_1.d.ts +35 -0
  53. package/dist/schema-CN5HHHku.d.ts +108 -0
  54. package/dist/schema.d.ts +2 -0
  55. package/dist/schema.js +43 -0
  56. package/dist/server-zTjpJpoX.d.ts +209 -0
  57. package/dist/sql-CCjc6Bid.js +142 -0
  58. package/dist/sql-DPmHOeZy.d.ts +131 -0
  59. package/dist/storage/index.d.ts +8 -0
  60. package/dist/storage/index.js +7 -0
  61. package/dist/storage/interface.d.ts +3 -0
  62. package/dist/storage/interface.js +0 -0
  63. package/dist/storage/memory.d.ts +7 -0
  64. package/dist/storage/memory.js +6 -0
  65. package/dist/storage/sql.d.ts +7 -0
  66. package/dist/storage/sql.js +6 -0
  67. package/dist/types-Be-qmQu0.d.ts +111 -0
  68. package/dist/types-D_psiH09.js +13 -0
  69. package/dist/types.d.ts +7 -0
  70. package/dist/types.js +3 -0
  71. package/dist/utils/index.d.ts +2 -0
  72. package/dist/utils/index.js +3 -0
  73. package/dist/utils/reconnect.d.ts +2 -0
  74. package/dist/utils/reconnect.js +3 -0
  75. package/package.json +156 -0
  76. package/src/adapters/client.ts +396 -0
  77. package/src/adapters/cloudflare-do.ts +346 -0
  78. package/src/adapters/index.ts +16 -0
  79. package/src/adapters/multi-peer.ts +404 -0
  80. package/src/adapters/server.ts +192 -0
  81. package/src/adapters/types.ts +202 -0
  82. package/src/codecs/cbor.ts +42 -0
  83. package/src/codecs/factory.ts +210 -0
  84. package/src/codecs/index.ts +30 -0
  85. package/src/codecs/json.ts +42 -0
  86. package/src/codecs/msgpack.ts +36 -0
  87. package/src/errors.ts +105 -0
  88. package/src/index.ts +102 -0
  89. package/src/peers/default.ts +433 -0
  90. package/src/peers/durable.ts +280 -0
  91. package/src/peers/index.ts +13 -0
  92. package/src/protocol.ts +306 -0
  93. package/src/schema.ts +167 -0
  94. package/src/storage/index.ts +20 -0
  95. package/src/storage/interface.ts +146 -0
  96. package/src/storage/memory.ts +84 -0
  97. package/src/storage/sql.ts +266 -0
  98. package/src/types.ts +158 -0
  99. package/src/utils/index.ts +9 -0
  100. package/src/utils/reconnect.ts +51 -0
package/src/schema.ts ADDED
@@ -0,0 +1,167 @@
1
+ /**
2
+ * Schema Definition Utilities
3
+ *
4
+ * Provides helpers to define RPC methods and events with Zod schemas,
5
+ * enabling TypeScript type inference for the entire RPC contract.
6
+ */
7
+
8
+ import type { LiteralUnion } from "type-fest";
9
+ import type * as z from "zod";
10
+
11
+ export type StringKeys<T> = keyof T extends string ? keyof T : never;
12
+ export type LiteralString = "" | (string & Record<never, never>);
13
+ export type LiteralStringUnion<T> = LiteralUnion<T, string>;
14
+
15
+ /**
16
+ * Method definition with input and output schemas
17
+ */
18
+ export interface MethodDef<
19
+ TInput extends z.ZodType = z.ZodType,
20
+ TOutput extends z.ZodType = z.ZodType,
21
+ > {
22
+ _type: "method";
23
+ input: TInput;
24
+ output: TOutput;
25
+ }
26
+
27
+ /**
28
+ * Event definition with data schema
29
+ */
30
+ export interface EventDef<TData extends z.ZodType = z.ZodType> {
31
+ _type: "event";
32
+ data: TData;
33
+ }
34
+
35
+ /**
36
+ * Define an RPC method with input/output schemas
37
+ *
38
+ * @param def - Object containing input and output Zod schemas
39
+ * @returns MethodDef with preserved type information
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const getUser = method({
44
+ * input: z.object({ id: z.string() }),
45
+ * output: z.object({ name: z.string(), email: z.string() }),
46
+ * });
47
+ * ```
48
+ */
49
+ export function method<
50
+ TInput extends z.ZodType,
51
+ TOutput extends z.ZodType,
52
+ >(def: { input: TInput; output: TOutput }): MethodDef<TInput, TOutput> {
53
+ return { _type: "method", ...def };
54
+ }
55
+
56
+ /**
57
+ * Define a fire-and-forget event with data schema
58
+ *
59
+ * @param def - Object containing data Zod schema
60
+ * @returns EventDef with preserved type information
61
+ *
62
+ * @example
63
+ * ```ts
64
+ * const userCreated = event({
65
+ * data: z.object({ id: z.string(), name: z.string() }),
66
+ * });
67
+ * ```
68
+ */
69
+ export function event<TData extends z.ZodType>(def: {
70
+ data: TData;
71
+ }): EventDef<TData> {
72
+ return { _type: "event", ...def };
73
+ }
74
+
75
+ /**
76
+ * Schema definition containing methods and events
77
+ */
78
+ export interface RpcSchema {
79
+ methods?: Record<string, MethodDef>;
80
+ events?: Record<string, EventDef>;
81
+ }
82
+
83
+ /**
84
+ * Infer the input type from a method definition
85
+ *
86
+ * @typeParam T - A MethodDef type to extract the input from
87
+ */
88
+ export type InferInput<T> =
89
+ T extends MethodDef<infer TInput, z.ZodType> ? z.infer<TInput> : never;
90
+
91
+ /**
92
+ * Infer the output type from a method definition
93
+ *
94
+ * @typeParam T - A MethodDef type to extract the output from
95
+ */
96
+ export type InferOutput<T> =
97
+ T extends MethodDef<z.ZodType, infer TOutput> ? z.infer<TOutput> : never;
98
+
99
+ /**
100
+ * Infer the data type from an event definition
101
+ *
102
+ * @typeParam T - An EventDef type to extract the data type from
103
+ */
104
+ export type InferEventData<T> =
105
+ T extends EventDef<infer TData> ? z.infer<TData> : never;
106
+
107
+ /**
108
+ * Infer method signatures from a schema's methods
109
+ */
110
+ export type InferMethods<T extends RpcSchema> =
111
+ T["methods"] extends Record<string, MethodDef>
112
+ ? {
113
+ [K in StringKeys<T["methods"]>]: (
114
+ input: InferInput<T["methods"][K]>,
115
+ ) => Promise<InferOutput<T["methods"][K]>>;
116
+ }
117
+ : object;
118
+
119
+ /**
120
+ * Infer event emitter signatures from a schema's events
121
+ */
122
+ export type InferEvents<T extends RpcSchema> =
123
+ T["events"] extends Record<string, EventDef>
124
+ ? {
125
+ [K in StringKeys<T["events"]>]: InferEventData<T["events"][K]>;
126
+ }
127
+ : object;
128
+
129
+ /**
130
+ * Provider type - implements the local methods defined in a schema
131
+ */
132
+ export type Provider<T extends RpcSchema> = InferMethods<T>;
133
+
134
+ /**
135
+ * Driver type - proxy to call remote methods defined in a schema
136
+ */
137
+ export type Driver<T extends RpcSchema> = InferMethods<T>;
138
+
139
+ /**
140
+ * Event handler type - handles incoming events
141
+ */
142
+ export type EventHandler<T extends RpcSchema, ExtraArgs extends any[] = []> = <
143
+ K extends StringKeys<T["events"]>,
144
+ >(
145
+ ...args: [
146
+ ...ExtraArgs,
147
+ event: K,
148
+ data: T["events"] extends Record<string, EventDef>
149
+ ? InferEventData<T["events"][K]>
150
+ : never,
151
+ ]
152
+ ) => void;
153
+
154
+ /**
155
+ * Event emitter type - emits outgoing events
156
+ */
157
+ export type EventEmitter<T extends RpcSchema, ExtraArgs extends any[] = []> = <
158
+ K extends StringKeys<T["events"]>,
159
+ >(
160
+ ...args: [
161
+ event: K,
162
+ data: T["events"] extends Record<string, EventDef>
163
+ ? InferEventData<T["events"][K]>
164
+ : never,
165
+ ...ExtraArgs,
166
+ ]
167
+ ) => void;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Storage Module Exports
3
+ */
4
+
5
+ export {
6
+ type AsyncPendingCallStorage,
7
+ type MaybePromise,
8
+ type PendingCall,
9
+ type PendingCallStorage,
10
+ type StorageMode,
11
+ type SyncPendingCallStorage,
12
+ } from "./interface.js";
13
+ export {
14
+ MemoryPendingCallStorage,
15
+ type MemoryPendingCallStorageOptions,
16
+ } from "./memory.js";
17
+ export {
18
+ SqlPendingCallStorage,
19
+ type SqlPendingCallStorageOptions,
20
+ } from "./sql.js";
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Pending Call Storage Interface
3
+ *
4
+ * Abstraction for persisting RPC calls that need to survive hibernation.
5
+ * Supports both sync (DO SQL/KV) and async (file, external KV) backends.
6
+ */
7
+
8
+ import type { RpcSchema } from "../schema";
9
+
10
+ /**
11
+ * A pending RPC call awaiting a response
12
+ */
13
+ export interface PendingCall {
14
+ /** Unique request ID */
15
+ id: string;
16
+ /** Remote method name */
17
+ method: string;
18
+ /** Serialized parameters */
19
+ params: unknown;
20
+ /** Method name on actor to call with response */
21
+ callback: string;
22
+ /** When the call was sent (Unix ms) */
23
+ sentAt: number;
24
+ /** When the call should timeout (Unix ms) */
25
+ timeoutAt: number;
26
+ }
27
+
28
+ /**
29
+ * Storage mode discriminant
30
+ */
31
+ export type StorageMode = "sync" | "async";
32
+
33
+ /**
34
+ * Conditional return type based on storage mode
35
+ */
36
+ export type MaybePromise<T, TMode extends StorageMode> = TMode extends "sync"
37
+ ? T
38
+ : Promise<T>;
39
+
40
+ /**
41
+ * Pending call storage interface
42
+ *
43
+ * Generic over sync/async mode to preserve type information about
44
+ * whether operations block or return promises.
45
+ *
46
+ * @typeParam TMode - 'sync' for synchronous storage (DO SQL/KV), 'async' for file/external
47
+ */
48
+ export interface PendingCallStorage<TMode extends StorageMode = "async"> {
49
+ /** Storage mode discriminant for runtime checks */
50
+ readonly mode: TMode;
51
+
52
+ /**
53
+ * Save a pending call
54
+ */
55
+ save(call: PendingCall): MaybePromise<void, TMode>;
56
+
57
+ /**
58
+ * Get a pending call by ID
59
+ */
60
+ get(id: string): MaybePromise<PendingCall | null, TMode>;
61
+
62
+ /**
63
+ * Delete a pending call by ID
64
+ */
65
+ delete(id: string): MaybePromise<boolean, TMode>;
66
+
67
+ /**
68
+ * List all calls that have exceeded their timeout
69
+ */
70
+ listExpired(before: number): MaybePromise<PendingCall[], TMode>;
71
+
72
+ /**
73
+ * List all pending calls (for debugging/recovery)
74
+ */
75
+ listAll(): MaybePromise<PendingCall[], TMode>;
76
+
77
+ /**
78
+ * Delete all pending calls (for cleanup)
79
+ */
80
+ clear(): MaybePromise<void, TMode>;
81
+ }
82
+
83
+ /**
84
+ * Convenience alias for synchronous storage (DO SQL/KV)
85
+ */
86
+ export type SyncPendingCallStorage = PendingCallStorage<"sync">;
87
+
88
+ /**
89
+ * Convenience alias for asynchronous storage (file, external KV)
90
+ */
91
+ export type AsyncPendingCallStorage = PendingCallStorage<"async">;
92
+
93
+ export interface IContinuationHandler<TRemoteSchema extends RpcSchema> {
94
+ /**
95
+ * Make a hibernation-safe RPC call using continuation-passing style
96
+ *
97
+ * Instead of returning a Promise, the result will be passed to the
98
+ * named callback method on the actor. This survives DO hibernation.
99
+ *
100
+ * @param method - Remote method to call
101
+ * @param params - Parameters for the method
102
+ * @param callback - Name of method on actor to call with result
103
+ * @param timeout - Optional timeout override (ms)
104
+ *
105
+ * @example
106
+ * ```ts
107
+ * // Make the call
108
+ * peer.callWithCallback('executeOrder', { market, side }, 'onOrderExecuted');
109
+ *
110
+ * // Define the callback on your actor
111
+ * onOrderExecuted(result: OrderResult, context: CallContext) {
112
+ * console.log('Order executed:', result);
113
+ * console.log('Latency:', context.latencyMs, 'ms');
114
+ * }
115
+ * ```
116
+ */
117
+ callWithCallback<K extends keyof TRemoteSchema["methods"] & string>(
118
+ method: K,
119
+ params: unknown,
120
+ callback: keyof this & string,
121
+ timeout?: number,
122
+ ): void;
123
+
124
+ /**
125
+ * Get all pending durable calls (for debugging/monitoring)
126
+ */
127
+ getPendingCalls(): PendingCall[];
128
+
129
+ /**
130
+ * Get expired calls that have exceeded their timeout
131
+ */
132
+ getExpiredCalls(): PendingCall[];
133
+
134
+ /**
135
+ * Clean up expired calls
136
+ *
137
+ * Call this periodically (e.g., on alarm) to remove stale calls.
138
+ * Returns the expired calls for optional error handling.
139
+ */
140
+ cleanupExpired(): PendingCall[];
141
+
142
+ /**
143
+ * Clear all pending durable calls
144
+ */
145
+ clearPendingCalls(): void;
146
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Memory Pending Call Storage
3
+ *
4
+ * In-memory storage implementation for testing purposes.
5
+ * Synchronous mode for compatibility with DurableRpcPeer tests.
6
+ */
7
+
8
+ import { type StringCodec } from "../codecs/index.js";
9
+ import type { PendingCall, SyncPendingCallStorage } from "./interface.js";
10
+
11
+ /**
12
+ * Options for memory pending call storage
13
+ */
14
+ export interface MemoryPendingCallStorageOptions {
15
+ /**
16
+ * Codec for serializing/deserializing params
17
+ *
18
+ * While memory storage doesn't strictly need serialization,
19
+ * using a codec ensures consistency with SQL storage and
20
+ * validates that params can be round-tripped.
21
+ */
22
+ paramsCodec?: StringCodec;
23
+ }
24
+
25
+ /**
26
+ * In-memory pending call storage for testing
27
+ *
28
+ * Optionally round-trips params through a codec for consistency testing.
29
+ */
30
+ export class MemoryPendingCallStorage implements SyncPendingCallStorage {
31
+ readonly mode = "sync" as const;
32
+ private readonly calls = new Map<string, PendingCall>();
33
+ private readonly paramsCodec: StringCodec | null;
34
+
35
+ constructor(options?: MemoryPendingCallStorageOptions) {
36
+ // Only use codec if explicitly provided (for testing codec behavior)
37
+ this.paramsCodec = options?.paramsCodec ?? null;
38
+ }
39
+
40
+ save(call: PendingCall): void {
41
+ // Optionally round-trip params through codec
42
+ const params = this.paramsCodec
43
+ ? this.paramsCodec.decode(this.paramsCodec.encode(call.params))
44
+ : call.params;
45
+
46
+ this.calls.set(call.id, { ...call, params });
47
+ }
48
+
49
+ get(id: string): PendingCall | null {
50
+ const call = this.calls.get(id);
51
+ return call ? { ...call } : null;
52
+ }
53
+
54
+ delete(id: string): boolean {
55
+ return this.calls.delete(id);
56
+ }
57
+
58
+ listExpired(before: number): PendingCall[] {
59
+ const expired: PendingCall[] = [];
60
+ for (const call of this.calls.values()) {
61
+ if (call.timeoutAt <= before) {
62
+ expired.push({ ...call });
63
+ }
64
+ }
65
+ return expired.sort((a, b) => a.timeoutAt - b.timeoutAt);
66
+ }
67
+
68
+ listAll(): PendingCall[] {
69
+ return [...this.calls.values()]
70
+ .map((c) => ({ ...c }))
71
+ .sort((a, b) => a.sentAt - b.sentAt);
72
+ }
73
+
74
+ clear(): void {
75
+ this.calls.clear();
76
+ }
77
+
78
+ /**
79
+ * Get the number of stored calls (for testing)
80
+ */
81
+ get size(): number {
82
+ return this.calls.size;
83
+ }
84
+ }
@@ -0,0 +1,266 @@
1
+ /**
2
+ * SQL Pending Call Storage
3
+ *
4
+ * Synchronous storage implementation using Durable Object SQL storage.
5
+ * Calls are persisted to SQLite and survive hibernation.
6
+ */
7
+
8
+ import * as z from "zod";
9
+ import { createJsonCodec, type StringCodec } from "../codecs/index.js";
10
+ import type { PendingCall, SyncPendingCallStorage } from "./interface.js";
11
+
12
+ /**
13
+ * SQL storage value types (matches Cloudflare SqlStorageValue)
14
+ */
15
+ type SqlStorageValue = ArrayBuffer | string | number | null;
16
+
17
+ /**
18
+ * Cursor returned by SqlStorage.exec()
19
+ *
20
+ * Compatible with Cloudflare's SqlStorageCursor.
21
+ */
22
+ export interface SqlStorageCursor<T extends Record<string, SqlStorageValue>>
23
+ extends Iterable<T> {
24
+ next(): { done?: false; value: T } | { done: true; value?: never };
25
+ toArray(): T[];
26
+ one(): T;
27
+ readonly rowsRead: number;
28
+ readonly rowsWritten: number;
29
+ }
30
+
31
+ /**
32
+ * Minimal SqlStorage interface for Durable Object SQL
33
+ *
34
+ * Compatible with `DurableObjectState.storage.sql` in Cloudflare Workers.
35
+ */
36
+ export interface SqlStorage {
37
+ exec<T extends Record<string, SqlStorageValue>>(
38
+ query: string,
39
+ ...bindings: unknown[]
40
+ ): SqlStorageCursor<T>;
41
+ }
42
+
43
+ /**
44
+ * Row shape from the pending calls table
45
+ */
46
+ interface PendingCallRow extends Record<string, SqlStorageValue> {
47
+ id: string;
48
+ method: string;
49
+ params: string;
50
+ callback: string;
51
+ sent_at: number;
52
+ timeout_at: number;
53
+ }
54
+
55
+ /**
56
+ * Table name for pending calls
57
+ */
58
+ const TABLE_NAME = "_rpc_pending_calls";
59
+
60
+ /**
61
+ * Default codec for params serialization (JSON)
62
+ */
63
+ const defaultParamsCodec = createJsonCodec(z.unknown());
64
+
65
+ /**
66
+ * Options for SQL pending call storage
67
+ */
68
+ export interface SqlPendingCallStorageOptions {
69
+ /**
70
+ * Codec for serializing/deserializing params
71
+ *
72
+ * Use a custom codec to support additional JavaScript types like
73
+ * Date, Map, Set, BigInt, etc.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * import superjson from "superjson";
78
+ * import { createStringCodecFactory } from "@igoforth/ws-rpc/codecs";
79
+ *
80
+ * const superJsonCodec = createStringCodecFactory(
81
+ * superjson.stringify,
82
+ * superjson.parse,
83
+ * "superjson"
84
+ * );
85
+ *
86
+ * const storage = new SqlPendingCallStorage(sql, {
87
+ * paramsCodec: superJsonCodec(z.unknown()),
88
+ * });
89
+ * ```
90
+ */
91
+ paramsCodec?: StringCodec;
92
+ }
93
+
94
+ /**
95
+ * SQL-backed pending call storage for Durable Objects
96
+ *
97
+ * Uses synchronous SQLite operations available in DO context.
98
+ */
99
+ export class SqlPendingCallStorage implements SyncPendingCallStorage {
100
+ readonly mode = "sync" as const;
101
+ private readonly sql: SqlStorage;
102
+ private readonly paramsCodec: StringCodec;
103
+ private initialized = false;
104
+
105
+ /**
106
+ * Create a SQL-backed pending call storage
107
+ *
108
+ * @param sql - Durable Object SQL storage instance
109
+ * @param options - Optional configuration including custom params codec
110
+ */
111
+ constructor(sql: SqlStorage, options?: SqlPendingCallStorageOptions) {
112
+ this.sql = sql;
113
+ this.paramsCodec = options?.paramsCodec ?? defaultParamsCodec;
114
+ }
115
+
116
+ /**
117
+ * Ensure table exists (lazy initialization)
118
+ */
119
+ private ensureTable(): void {
120
+ if (this.initialized) return;
121
+
122
+ this.sql.exec(`
123
+ CREATE TABLE IF NOT EXISTS ${TABLE_NAME} (
124
+ id TEXT PRIMARY KEY NOT NULL,
125
+ method TEXT NOT NULL,
126
+ params TEXT NOT NULL,
127
+ callback TEXT NOT NULL,
128
+ sent_at INTEGER NOT NULL,
129
+ timeout_at INTEGER NOT NULL
130
+ )
131
+ `);
132
+
133
+ // Index for timeout queries
134
+ this.sql.exec(`
135
+ CREATE INDEX IF NOT EXISTS idx_${TABLE_NAME}_timeout
136
+ ON ${TABLE_NAME}(timeout_at)
137
+ `);
138
+
139
+ this.initialized = true;
140
+ }
141
+
142
+ /**
143
+ * Save a pending call to storage
144
+ *
145
+ * @param call - The pending call to persist
146
+ */
147
+ save(call: PendingCall): void {
148
+ this.ensureTable();
149
+
150
+ const encodedParams = this.paramsCodec.encode(call.params);
151
+
152
+ this.sql.exec(
153
+ `INSERT OR REPLACE INTO ${TABLE_NAME}
154
+ (id, method, params, callback, sent_at, timeout_at)
155
+ VALUES (?, ?, ?, ?, ?, ?)`,
156
+ call.id,
157
+ call.method,
158
+ encodedParams,
159
+ call.callback,
160
+ call.sentAt,
161
+ call.timeoutAt,
162
+ );
163
+ }
164
+
165
+ /**
166
+ * Get a pending call by ID
167
+ *
168
+ * @param id - The unique request ID
169
+ * @returns The pending call or null if not found
170
+ */
171
+ get(id: string): PendingCall | null {
172
+ this.ensureTable();
173
+
174
+ const results = [
175
+ ...this.sql.exec<PendingCallRow>(
176
+ `SELECT * FROM ${TABLE_NAME} WHERE id = ?`,
177
+ id,
178
+ ),
179
+ ];
180
+
181
+ const row = results[0];
182
+ if (!row) return null;
183
+
184
+ return this.rowToCall(row);
185
+ }
186
+
187
+ /**
188
+ * Delete a pending call by ID
189
+ *
190
+ * @param id - The unique request ID
191
+ * @returns true if the call existed and was deleted
192
+ */
193
+ delete(id: string): boolean {
194
+ this.ensureTable();
195
+
196
+ // SQLite doesn't return affected rows easily, so check existence first
197
+ const exists = this.get(id) !== null;
198
+ if (exists) {
199
+ this.sql.exec(`DELETE FROM ${TABLE_NAME} WHERE id = ?`, id);
200
+ }
201
+ return exists;
202
+ }
203
+
204
+ /**
205
+ * List all calls that have exceeded their timeout
206
+ *
207
+ * @param before - Unix timestamp (ms); returns calls with timeoutAt <= before
208
+ * @returns Array of expired pending calls ordered by timeout
209
+ */
210
+ listExpired(before: number): PendingCall[] {
211
+ this.ensureTable();
212
+
213
+ const results = [
214
+ ...this.sql.exec<PendingCallRow>(
215
+ `SELECT * FROM ${TABLE_NAME} WHERE timeout_at <= ? ORDER BY timeout_at ASC`,
216
+ before,
217
+ ),
218
+ ];
219
+
220
+ return results.map((row) => this.rowToCall(row));
221
+ }
222
+
223
+ /**
224
+ * List all pending calls
225
+ *
226
+ * @returns Array of all pending calls ordered by sent time
227
+ */
228
+ listAll(): PendingCall[] {
229
+ this.ensureTable();
230
+
231
+ const results = [
232
+ ...this.sql.exec<PendingCallRow>(
233
+ `SELECT * FROM ${TABLE_NAME} ORDER BY sent_at ASC`,
234
+ ),
235
+ ];
236
+
237
+ return results.map((row) => this.rowToCall(row));
238
+ }
239
+
240
+ /**
241
+ * Delete all pending calls
242
+ */
243
+ clear(): void {
244
+ this.ensureTable();
245
+ this.sql.exec(`DELETE FROM ${TABLE_NAME}`);
246
+ }
247
+
248
+ /**
249
+ * Convert a database row to a PendingCall
250
+ *
251
+ * @param row - Database row with call data
252
+ * @returns Deserialized pending call
253
+ */
254
+ private rowToCall(row: PendingCallRow): PendingCall {
255
+ const params = this.paramsCodec.decode(row.params);
256
+
257
+ return {
258
+ id: row.id,
259
+ method: row.method,
260
+ params,
261
+ callback: row.callback,
262
+ sentAt: row.sent_at,
263
+ timeoutAt: row.timeout_at,
264
+ };
265
+ }
266
+ }