@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.
- package/LICENSE +21 -0
- package/README.md +446 -0
- package/dist/adapters/client.d.ts +117 -0
- package/dist/adapters/client.js +241 -0
- package/dist/adapters/cloudflare-do.d.ts +72 -0
- package/dist/adapters/cloudflare-do.js +192 -0
- package/dist/adapters/index.d.ts +13 -0
- package/dist/adapters/index.js +16 -0
- package/dist/adapters/server.d.ts +10 -0
- package/dist/adapters/server.js +122 -0
- package/dist/adapters/types.d.ts +125 -0
- package/dist/adapters/types.js +3 -0
- package/dist/codecs/cbor.d.ts +16 -0
- package/dist/codecs/cbor.js +36 -0
- package/dist/codecs/factory.d.ts +3 -0
- package/dist/codecs/factory.js +3 -0
- package/dist/codecs/index.d.ts +5 -0
- package/dist/codecs/index.js +5 -0
- package/dist/codecs/json.d.ts +4 -0
- package/dist/codecs/json.js +4 -0
- package/dist/codecs/msgpack.d.ts +16 -0
- package/dist/codecs/msgpack.js +34 -0
- package/dist/codecs-BmYG2d_U.js +0 -0
- package/dist/default-BkrMd28n.js +253 -0
- package/dist/default-xDNNMrg0.d.ts +129 -0
- package/dist/durable-MZjkvyS6.js +165 -0
- package/dist/errors-5BfreE63.js +96 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.js +7 -0
- package/dist/factory-3ziwTuZe.js +132 -0
- package/dist/factory-C1v0AEHY.d.ts +101 -0
- package/dist/index-Be7jjS77.d.ts +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/interface-C4S-WCqW.d.ts +120 -0
- package/dist/json-54Z2bIIs.d.ts +22 -0
- package/dist/json-Bshec-bZ.js +41 -0
- package/dist/memory-Bqb3KEVr.js +48 -0
- package/dist/memory-D1nGjzzH.d.ts +41 -0
- package/dist/multi-peer-BAi9yVzp.js +242 -0
- package/dist/peers/default.d.ts +8 -0
- package/dist/peers/default.js +8 -0
- package/dist/peers/durable.d.ts +136 -0
- package/dist/peers/durable.js +9 -0
- package/dist/peers/index.d.ts +10 -0
- package/dist/peers/index.js +9 -0
- package/dist/protocol-DA84zrc2.d.ts +211 -0
- package/dist/protocol-_mpoOPp6.js +192 -0
- package/dist/protocol.d.ts +6 -0
- package/dist/protocol.js +6 -0
- package/dist/reconnect-CGAA_1Gf.js +26 -0
- package/dist/reconnect-DbcN0R_1.d.ts +35 -0
- package/dist/schema-CN5HHHku.d.ts +108 -0
- package/dist/schema.d.ts +2 -0
- package/dist/schema.js +43 -0
- package/dist/server-zTjpJpoX.d.ts +209 -0
- package/dist/sql-CCjc6Bid.js +142 -0
- package/dist/sql-DPmHOeZy.d.ts +131 -0
- package/dist/storage/index.d.ts +8 -0
- package/dist/storage/index.js +7 -0
- package/dist/storage/interface.d.ts +3 -0
- package/dist/storage/interface.js +0 -0
- package/dist/storage/memory.d.ts +7 -0
- package/dist/storage/memory.js +6 -0
- package/dist/storage/sql.d.ts +7 -0
- package/dist/storage/sql.js +6 -0
- package/dist/types-Be-qmQu0.d.ts +111 -0
- package/dist/types-D_psiH09.js +13 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.js +3 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/reconnect.d.ts +2 -0
- package/dist/utils/reconnect.js +3 -0
- package/package.json +156 -0
- package/src/adapters/client.ts +396 -0
- package/src/adapters/cloudflare-do.ts +346 -0
- package/src/adapters/index.ts +16 -0
- package/src/adapters/multi-peer.ts +404 -0
- package/src/adapters/server.ts +192 -0
- package/src/adapters/types.ts +202 -0
- package/src/codecs/cbor.ts +42 -0
- package/src/codecs/factory.ts +210 -0
- package/src/codecs/index.ts +30 -0
- package/src/codecs/json.ts +42 -0
- package/src/codecs/msgpack.ts +36 -0
- package/src/errors.ts +105 -0
- package/src/index.ts +102 -0
- package/src/peers/default.ts +433 -0
- package/src/peers/durable.ts +280 -0
- package/src/peers/index.ts +13 -0
- package/src/protocol.ts +306 -0
- package/src/schema.ts +167 -0
- package/src/storage/index.ts +20 -0
- package/src/storage/interface.ts +146 -0
- package/src/storage/memory.ts +84 -0
- package/src/storage/sql.ts +266 -0
- package/src/types.ts +158 -0
- package/src/utils/index.ts +9 -0
- 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
|
+
}
|