@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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { m as RpcSchema } from "./schema-CN5HHHku.js";
|
|
2
|
+
|
|
3
|
+
//#region src/storage/interface.d.ts
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* A pending RPC call awaiting a response
|
|
7
|
+
*/
|
|
8
|
+
interface PendingCall {
|
|
9
|
+
/** Unique request ID */
|
|
10
|
+
id: string;
|
|
11
|
+
/** Remote method name */
|
|
12
|
+
method: string;
|
|
13
|
+
/** Serialized parameters */
|
|
14
|
+
params: unknown;
|
|
15
|
+
/** Method name on actor to call with response */
|
|
16
|
+
callback: string;
|
|
17
|
+
/** When the call was sent (Unix ms) */
|
|
18
|
+
sentAt: number;
|
|
19
|
+
/** When the call should timeout (Unix ms) */
|
|
20
|
+
timeoutAt: number;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Storage mode discriminant
|
|
24
|
+
*/
|
|
25
|
+
type StorageMode = "sync" | "async";
|
|
26
|
+
/**
|
|
27
|
+
* Conditional return type based on storage mode
|
|
28
|
+
*/
|
|
29
|
+
type MaybePromise<T, TMode extends StorageMode> = TMode extends "sync" ? T : Promise<T>;
|
|
30
|
+
/**
|
|
31
|
+
* Pending call storage interface
|
|
32
|
+
*
|
|
33
|
+
* Generic over sync/async mode to preserve type information about
|
|
34
|
+
* whether operations block or return promises.
|
|
35
|
+
*
|
|
36
|
+
* @typeParam TMode - 'sync' for synchronous storage (DO SQL/KV), 'async' for file/external
|
|
37
|
+
*/
|
|
38
|
+
interface PendingCallStorage<TMode extends StorageMode = "async"> {
|
|
39
|
+
/** Storage mode discriminant for runtime checks */
|
|
40
|
+
readonly mode: TMode;
|
|
41
|
+
/**
|
|
42
|
+
* Save a pending call
|
|
43
|
+
*/
|
|
44
|
+
save(call: PendingCall): MaybePromise<void, TMode>;
|
|
45
|
+
/**
|
|
46
|
+
* Get a pending call by ID
|
|
47
|
+
*/
|
|
48
|
+
get(id: string): MaybePromise<PendingCall | null, TMode>;
|
|
49
|
+
/**
|
|
50
|
+
* Delete a pending call by ID
|
|
51
|
+
*/
|
|
52
|
+
delete(id: string): MaybePromise<boolean, TMode>;
|
|
53
|
+
/**
|
|
54
|
+
* List all calls that have exceeded their timeout
|
|
55
|
+
*/
|
|
56
|
+
listExpired(before: number): MaybePromise<PendingCall[], TMode>;
|
|
57
|
+
/**
|
|
58
|
+
* List all pending calls (for debugging/recovery)
|
|
59
|
+
*/
|
|
60
|
+
listAll(): MaybePromise<PendingCall[], TMode>;
|
|
61
|
+
/**
|
|
62
|
+
* Delete all pending calls (for cleanup)
|
|
63
|
+
*/
|
|
64
|
+
clear(): MaybePromise<void, TMode>;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Convenience alias for synchronous storage (DO SQL/KV)
|
|
68
|
+
*/
|
|
69
|
+
type SyncPendingCallStorage = PendingCallStorage<"sync">;
|
|
70
|
+
/**
|
|
71
|
+
* Convenience alias for asynchronous storage (file, external KV)
|
|
72
|
+
*/
|
|
73
|
+
type AsyncPendingCallStorage = PendingCallStorage<"async">;
|
|
74
|
+
interface IContinuationHandler<TRemoteSchema extends RpcSchema> {
|
|
75
|
+
/**
|
|
76
|
+
* Make a hibernation-safe RPC call using continuation-passing style
|
|
77
|
+
*
|
|
78
|
+
* Instead of returning a Promise, the result will be passed to the
|
|
79
|
+
* named callback method on the actor. This survives DO hibernation.
|
|
80
|
+
*
|
|
81
|
+
* @param method - Remote method to call
|
|
82
|
+
* @param params - Parameters for the method
|
|
83
|
+
* @param callback - Name of method on actor to call with result
|
|
84
|
+
* @param timeout - Optional timeout override (ms)
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```ts
|
|
88
|
+
* // Make the call
|
|
89
|
+
* peer.callWithCallback('executeOrder', { market, side }, 'onOrderExecuted');
|
|
90
|
+
*
|
|
91
|
+
* // Define the callback on your actor
|
|
92
|
+
* onOrderExecuted(result: OrderResult, context: CallContext) {
|
|
93
|
+
* console.log('Order executed:', result);
|
|
94
|
+
* console.log('Latency:', context.latencyMs, 'ms');
|
|
95
|
+
* }
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
callWithCallback<K extends keyof TRemoteSchema["methods"] & string>(method: K, params: unknown, callback: keyof this & string, timeout?: number): void;
|
|
99
|
+
/**
|
|
100
|
+
* Get all pending durable calls (for debugging/monitoring)
|
|
101
|
+
*/
|
|
102
|
+
getPendingCalls(): PendingCall[];
|
|
103
|
+
/**
|
|
104
|
+
* Get expired calls that have exceeded their timeout
|
|
105
|
+
*/
|
|
106
|
+
getExpiredCalls(): PendingCall[];
|
|
107
|
+
/**
|
|
108
|
+
* Clean up expired calls
|
|
109
|
+
*
|
|
110
|
+
* Call this periodically (e.g., on alarm) to remove stale calls.
|
|
111
|
+
* Returns the expired calls for optional error handling.
|
|
112
|
+
*/
|
|
113
|
+
cleanupExpired(): PendingCall[];
|
|
114
|
+
/**
|
|
115
|
+
* Clear all pending durable calls
|
|
116
|
+
*/
|
|
117
|
+
clearPendingCalls(): void;
|
|
118
|
+
}
|
|
119
|
+
//#endregion
|
|
120
|
+
export { PendingCallStorage as a, PendingCall as i, IContinuationHandler as n, StorageMode as o, MaybePromise as r, SyncPendingCallStorage as s, AsyncPendingCallStorage as t };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { n as CodecFactory } from "./factory-C1v0AEHY.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/codecs/json.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Factory for creating JSON codecs with Zod schema validation
|
|
8
|
+
*
|
|
9
|
+
* @param schema - Zod schema for validation
|
|
10
|
+
* @param options - Optional codec configuration
|
|
11
|
+
* @returns A codec that encodes to JSON string and validates on decode
|
|
12
|
+
*/
|
|
13
|
+
declare const createJsonCodec: CodecFactory<string>;
|
|
14
|
+
/**
|
|
15
|
+
* Default JSON codec for unknown values
|
|
16
|
+
*
|
|
17
|
+
* Use when you need to serialize arbitrary data without schema validation.
|
|
18
|
+
* The decoded value will be `unknown` and should be validated separately.
|
|
19
|
+
*/
|
|
20
|
+
declare const JsonCodec: z.ZodCodec<z.ZodString, z.ZodUnknown>;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { createJsonCodec as n, JsonCodec as t };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { n as createStringCodecFactory } from "./factory-3ziwTuZe.js";
|
|
2
|
+
import * as z from "zod";
|
|
3
|
+
|
|
4
|
+
//#region src/codecs/json.ts
|
|
5
|
+
/**
|
|
6
|
+
* JSON Codec
|
|
7
|
+
*
|
|
8
|
+
* String-based codec using native JSON serialization.
|
|
9
|
+
* Suitable for simple data types (no Date, Map, Set, BigInt support).
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { createJsonCodec } from "@igoforth/ws-rpc/codecs";
|
|
14
|
+
*
|
|
15
|
+
* const UserCodec = createJsonCodec(z.object({
|
|
16
|
+
* id: z.string(),
|
|
17
|
+
* name: z.string(),
|
|
18
|
+
* }));
|
|
19
|
+
*
|
|
20
|
+
* const encoded = UserCodec.encode({ id: "1", name: "John" });
|
|
21
|
+
* const decoded = UserCodec.decode(encoded);
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
/**
|
|
25
|
+
* Factory for creating JSON codecs with Zod schema validation
|
|
26
|
+
*
|
|
27
|
+
* @param schema - Zod schema for validation
|
|
28
|
+
* @param options - Optional codec configuration
|
|
29
|
+
* @returns A codec that encodes to JSON string and validates on decode
|
|
30
|
+
*/
|
|
31
|
+
const createJsonCodec = createStringCodecFactory(JSON.stringify, JSON.parse, "json_string");
|
|
32
|
+
/**
|
|
33
|
+
* Default JSON codec for unknown values
|
|
34
|
+
*
|
|
35
|
+
* Use when you need to serialize arbitrary data without schema validation.
|
|
36
|
+
* The decoded value will be `unknown` and should be validated separately.
|
|
37
|
+
*/
|
|
38
|
+
const JsonCodec = createJsonCodec(z.unknown());
|
|
39
|
+
|
|
40
|
+
//#endregion
|
|
41
|
+
export { createJsonCodec as n, JsonCodec as t };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/storage/memory.ts
|
|
2
|
+
/**
|
|
3
|
+
* In-memory pending call storage for testing
|
|
4
|
+
*
|
|
5
|
+
* Optionally round-trips params through a codec for consistency testing.
|
|
6
|
+
*/
|
|
7
|
+
var MemoryPendingCallStorage = class {
|
|
8
|
+
mode = "sync";
|
|
9
|
+
calls = /* @__PURE__ */ new Map();
|
|
10
|
+
paramsCodec;
|
|
11
|
+
constructor(options) {
|
|
12
|
+
this.paramsCodec = options?.paramsCodec ?? null;
|
|
13
|
+
}
|
|
14
|
+
save(call) {
|
|
15
|
+
const params = this.paramsCodec ? this.paramsCodec.decode(this.paramsCodec.encode(call.params)) : call.params;
|
|
16
|
+
this.calls.set(call.id, {
|
|
17
|
+
...call,
|
|
18
|
+
params
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
get(id) {
|
|
22
|
+
const call = this.calls.get(id);
|
|
23
|
+
return call ? { ...call } : null;
|
|
24
|
+
}
|
|
25
|
+
delete(id) {
|
|
26
|
+
return this.calls.delete(id);
|
|
27
|
+
}
|
|
28
|
+
listExpired(before) {
|
|
29
|
+
const expired = [];
|
|
30
|
+
for (const call of this.calls.values()) if (call.timeoutAt <= before) expired.push({ ...call });
|
|
31
|
+
return expired.sort((a, b) => a.timeoutAt - b.timeoutAt);
|
|
32
|
+
}
|
|
33
|
+
listAll() {
|
|
34
|
+
return [...this.calls.values()].map((c) => ({ ...c })).sort((a, b) => a.sentAt - b.sentAt);
|
|
35
|
+
}
|
|
36
|
+
clear() {
|
|
37
|
+
this.calls.clear();
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Get the number of stored calls (for testing)
|
|
41
|
+
*/
|
|
42
|
+
get size() {
|
|
43
|
+
return this.calls.size;
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
export { MemoryPendingCallStorage as t };
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { i as StringCodec } from "./factory-C1v0AEHY.js";
|
|
2
|
+
import { i as PendingCall, s as SyncPendingCallStorage } from "./interface-C4S-WCqW.js";
|
|
3
|
+
|
|
4
|
+
//#region src/storage/memory.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Options for memory pending call storage
|
|
8
|
+
*/
|
|
9
|
+
interface MemoryPendingCallStorageOptions {
|
|
10
|
+
/**
|
|
11
|
+
* Codec for serializing/deserializing params
|
|
12
|
+
*
|
|
13
|
+
* While memory storage doesn't strictly need serialization,
|
|
14
|
+
* using a codec ensures consistency with SQL storage and
|
|
15
|
+
* validates that params can be round-tripped.
|
|
16
|
+
*/
|
|
17
|
+
paramsCodec?: StringCodec;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* In-memory pending call storage for testing
|
|
21
|
+
*
|
|
22
|
+
* Optionally round-trips params through a codec for consistency testing.
|
|
23
|
+
*/
|
|
24
|
+
declare class MemoryPendingCallStorage implements SyncPendingCallStorage {
|
|
25
|
+
readonly mode: "sync";
|
|
26
|
+
private readonly calls;
|
|
27
|
+
private readonly paramsCodec;
|
|
28
|
+
constructor(options?: MemoryPendingCallStorageOptions);
|
|
29
|
+
save(call: PendingCall): void;
|
|
30
|
+
get(id: string): PendingCall | null;
|
|
31
|
+
delete(id: string): boolean;
|
|
32
|
+
listExpired(before: number): PendingCall[];
|
|
33
|
+
listAll(): PendingCall[];
|
|
34
|
+
clear(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Get the number of stored calls (for testing)
|
|
37
|
+
*/
|
|
38
|
+
get size(): number;
|
|
39
|
+
}
|
|
40
|
+
//#endregion
|
|
41
|
+
export { MemoryPendingCallStorageOptions as n, MemoryPendingCallStorage as t };
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { t as RpcPeer } from "./default-BkrMd28n.js";
|
|
2
|
+
|
|
3
|
+
//#region src/adapters/multi-peer.ts
|
|
4
|
+
/**
|
|
5
|
+
* Multi-Peer Base Class
|
|
6
|
+
*
|
|
7
|
+
* Abstract base class for adapters managing multiple RPC peers.
|
|
8
|
+
* Extended by RpcServer and Cloudflare DO adapter.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Abstract base class for multi-peer adapters
|
|
12
|
+
*
|
|
13
|
+
* Provides shared functionality for managing multiple RPC peers:
|
|
14
|
+
* - Driver for calling methods on multiple peers
|
|
15
|
+
* - Broadcast emit to all peers
|
|
16
|
+
* - Peer lookup by ID
|
|
17
|
+
* - Connection count/IDs
|
|
18
|
+
*
|
|
19
|
+
* @typeParam TLocalSchema - Schema for methods/events this side provides
|
|
20
|
+
* @typeParam TRemoteSchema - Schema for methods/events the remote side provides
|
|
21
|
+
* @typeParam TConnection - Connection type (IWebSocket, WebSocket, etc.)
|
|
22
|
+
*/
|
|
23
|
+
var MultiPeerBase = class {
|
|
24
|
+
peers = /* @__PURE__ */ new Map();
|
|
25
|
+
/** Local schema */
|
|
26
|
+
localSchema;
|
|
27
|
+
/** Remote schema */
|
|
28
|
+
remoteSchema;
|
|
29
|
+
/** Implementation of local methods */
|
|
30
|
+
provider;
|
|
31
|
+
/** Default timeout for RPC calls */
|
|
32
|
+
timeout;
|
|
33
|
+
/** Lifecycle hooks */
|
|
34
|
+
hooks;
|
|
35
|
+
constructor(options) {
|
|
36
|
+
this.localSchema = options.localSchema;
|
|
37
|
+
this.remoteSchema = options.remoteSchema;
|
|
38
|
+
this.provider = options.provider;
|
|
39
|
+
this.timeout = options.timeout ?? 3e4;
|
|
40
|
+
this.hooks = options.hooks ?? {};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Create an RPC peer for a WebSocket connection.
|
|
44
|
+
* Subclasses can override to customize peer creation.
|
|
45
|
+
*/
|
|
46
|
+
createPeer(ws) {
|
|
47
|
+
return new RpcPeer({
|
|
48
|
+
ws,
|
|
49
|
+
localSchema: this.localSchema,
|
|
50
|
+
remoteSchema: this.remoteSchema,
|
|
51
|
+
provider: this.provider,
|
|
52
|
+
onEvent: this.hooks.onEvent ? (event, data) => {
|
|
53
|
+
const peer = this.findPeerByWs(ws);
|
|
54
|
+
if (peer) this.hooks.onEvent(peer, event, data);
|
|
55
|
+
} : void 0,
|
|
56
|
+
timeout: this.timeout
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Find peer by WebSocket (internal helper for event routing)
|
|
61
|
+
*/
|
|
62
|
+
findPeerByWs(ws) {
|
|
63
|
+
for (const peer of this.peers.values()) if (peer.getWebSocket() === ws) return peer;
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Add a peer (called by subclass when connection established)
|
|
68
|
+
*/
|
|
69
|
+
addPeer(connection, peer) {
|
|
70
|
+
this.peers.set(connection, peer);
|
|
71
|
+
this.hooks.onConnect?.(peer);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Remove a peer (called by subclass when connection closes)
|
|
75
|
+
*/
|
|
76
|
+
removePeer(connection) {
|
|
77
|
+
const peer = this.peers.get(connection);
|
|
78
|
+
if (peer) {
|
|
79
|
+
peer.close();
|
|
80
|
+
this.peers.delete(connection);
|
|
81
|
+
this.hooks.onDisconnect?.(peer);
|
|
82
|
+
return peer;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get peer by connection object
|
|
88
|
+
*/
|
|
89
|
+
getPeerFor(connection) {
|
|
90
|
+
return this.peers.get(connection) ?? null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Get peer by ID
|
|
94
|
+
*/
|
|
95
|
+
getPeer(id) {
|
|
96
|
+
for (const peer of this.peers.values()) if (peer.id === id) return peer;
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Find peer entry by ID (internal - includes connection)
|
|
101
|
+
*/
|
|
102
|
+
findPeerEntry(id) {
|
|
103
|
+
for (const [connection, peer] of this.peers) if (peer.id === id) return {
|
|
104
|
+
peer,
|
|
105
|
+
connection
|
|
106
|
+
};
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get all peers
|
|
111
|
+
*/
|
|
112
|
+
getPeers() {
|
|
113
|
+
return this.peers.values();
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Get all open peer entries (internal)
|
|
117
|
+
*/
|
|
118
|
+
getOpenEntries() {
|
|
119
|
+
const result = [];
|
|
120
|
+
for (const [connection, peer] of this.peers) if (peer.isOpen) result.push({
|
|
121
|
+
peer,
|
|
122
|
+
connection
|
|
123
|
+
});
|
|
124
|
+
return result;
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Get count of open connections
|
|
128
|
+
*/
|
|
129
|
+
getConnectionCount() {
|
|
130
|
+
let count = 0;
|
|
131
|
+
for (const peer of this.peers.values()) if (peer.isOpen) count++;
|
|
132
|
+
return count;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Get all open peer IDs
|
|
136
|
+
*/
|
|
137
|
+
getConnectionIds() {
|
|
138
|
+
const ids = [];
|
|
139
|
+
for (const peer of this.peers.values()) if (peer.isOpen) ids.push(peer.id);
|
|
140
|
+
return ids;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Driver for calling remote methods on connected peers
|
|
144
|
+
*
|
|
145
|
+
* @returns MultiDriver proxy for calling methods on all or specific peers
|
|
146
|
+
*/
|
|
147
|
+
get driver() {
|
|
148
|
+
return this.createMultiDriver();
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Emit an event to connected peers
|
|
152
|
+
*
|
|
153
|
+
* @param event - Event name from local schema
|
|
154
|
+
* @param data - Event data matching the schema
|
|
155
|
+
* @param ids - Optional array of peer IDs to emit to (broadcasts to all if omitted)
|
|
156
|
+
*/
|
|
157
|
+
emit(event, data, ids) {
|
|
158
|
+
const validPeers = ids ? this.peers.values().filter((p) => ids.includes(p.id) && p.isOpen) : this.peers.values().filter((p) => p.isOpen);
|
|
159
|
+
for (const peer of validPeers) peer.emit(event, data);
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Close a specific peer by ID
|
|
163
|
+
*
|
|
164
|
+
* @param id - Peer ID to close
|
|
165
|
+
* @returns true if peer was found and closed, false otherwise
|
|
166
|
+
*/
|
|
167
|
+
closePeer(id) {
|
|
168
|
+
const entry = this.findPeerEntry(id);
|
|
169
|
+
if (entry) {
|
|
170
|
+
entry.peer.close();
|
|
171
|
+
this.peers.delete(entry.connection);
|
|
172
|
+
this.hooks.onDisconnect?.(entry.peer);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Close all peers
|
|
179
|
+
*/
|
|
180
|
+
closeAll() {
|
|
181
|
+
for (const peer of this.peers.values()) {
|
|
182
|
+
peer.close();
|
|
183
|
+
this.hooks.onDisconnect?.(peer);
|
|
184
|
+
}
|
|
185
|
+
this.peers.clear();
|
|
186
|
+
this.hooks.onClose?.();
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Create a driver proxy for calling remote methods on multiple peers
|
|
190
|
+
*/
|
|
191
|
+
createMultiDriver() {
|
|
192
|
+
const methods = this.remoteSchema.methods ?? {};
|
|
193
|
+
const driver = {};
|
|
194
|
+
for (const methodName of Object.keys(methods)) driver[methodName] = async (input, options) => {
|
|
195
|
+
return this.callMethod(methodName, input, options);
|
|
196
|
+
};
|
|
197
|
+
return driver;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Call a method on multiple peers with timeout handling
|
|
201
|
+
*
|
|
202
|
+
* @param method - Method name to call
|
|
203
|
+
* @param input - Method input parameters
|
|
204
|
+
* @param options - Call options including target peer IDs and timeout
|
|
205
|
+
* @returns Array of results from each peer (success or error)
|
|
206
|
+
*/
|
|
207
|
+
async callMethod(method, input, options) {
|
|
208
|
+
const ids = options?.ids;
|
|
209
|
+
const timeout = options?.timeout ?? this.timeout;
|
|
210
|
+
let targetPeers;
|
|
211
|
+
if (ids === void 0) targetPeers = Array.from(this.peers.values()).filter((p) => p.isOpen);
|
|
212
|
+
else if (typeof ids === "string") {
|
|
213
|
+
const peer = this.getPeer(ids);
|
|
214
|
+
targetPeers = peer?.isOpen ? [peer] : [];
|
|
215
|
+
} else targetPeers = ids.map((id) => this.getPeer(id)).filter((p) => p?.isOpen === true);
|
|
216
|
+
const promises = targetPeers.map(async (peer) => {
|
|
217
|
+
try {
|
|
218
|
+
const callPromise = peer.driver[method](input);
|
|
219
|
+
const value = await Promise.race([callPromise, new Promise((_, reject) => setTimeout(() => reject(/* @__PURE__ */ new Error(`Timeout after ${timeout}ms`)), timeout))]);
|
|
220
|
+
return {
|
|
221
|
+
id: peer.id,
|
|
222
|
+
result: {
|
|
223
|
+
success: true,
|
|
224
|
+
value
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return {
|
|
229
|
+
id: peer.id,
|
|
230
|
+
result: {
|
|
231
|
+
success: false,
|
|
232
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
return Promise.all(promises);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
//#endregion
|
|
242
|
+
export { MultiPeerBase as t };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import "../schema-CN5HHHku.js";
|
|
2
|
+
import "../factory-C1v0AEHY.js";
|
|
3
|
+
import "../json-54Z2bIIs.js";
|
|
4
|
+
import "../index-Be7jjS77.js";
|
|
5
|
+
import "../protocol-DA84zrc2.js";
|
|
6
|
+
import "../types-Be-qmQu0.js";
|
|
7
|
+
import { n as RpcPeerOptions, t as RpcPeer } from "../default-xDNNMrg0.js";
|
|
8
|
+
export { RpcPeer, RpcPeerOptions };
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { m as RpcSchema } from "../schema-CN5HHHku.js";
|
|
2
|
+
import "../factory-C1v0AEHY.js";
|
|
3
|
+
import "../json-54Z2bIIs.js";
|
|
4
|
+
import "../index-Be7jjS77.js";
|
|
5
|
+
import { g as WireInput } from "../protocol-DA84zrc2.js";
|
|
6
|
+
import "../types-Be-qmQu0.js";
|
|
7
|
+
import { n as RpcPeerOptions, t as RpcPeer } from "../default-xDNNMrg0.js";
|
|
8
|
+
import { i as PendingCall, s as SyncPendingCallStorage } from "../interface-C4S-WCqW.js";
|
|
9
|
+
|
|
10
|
+
//#region src/peers/durable.d.ts
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Options for creating a DurableRpcPeer
|
|
14
|
+
*/
|
|
15
|
+
interface DurableRpcPeerOptions<TActor> {
|
|
16
|
+
/** Synchronous storage for persisting pending calls */
|
|
17
|
+
storage: SyncPendingCallStorage;
|
|
18
|
+
/** The actor instance (for resolving callback methods) */
|
|
19
|
+
actor: TActor;
|
|
20
|
+
/** Default timeout for continuation-based calls (ms) */
|
|
21
|
+
durableTimeout?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Context passed to callback methods along with the result
|
|
25
|
+
*/
|
|
26
|
+
interface CallContext {
|
|
27
|
+
/** The original pending call */
|
|
28
|
+
call: PendingCall;
|
|
29
|
+
/** Time from send to response (ms) */
|
|
30
|
+
latencyMs: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Durable RPC Peer
|
|
34
|
+
*
|
|
35
|
+
* Extends RpcPeer to add:
|
|
36
|
+
* - Hibernation-safe continuation-based calls via `callWithCallback`
|
|
37
|
+
* - Automatic recovery of pending calls after hibernation
|
|
38
|
+
* - Timeout cleanup for stale calls
|
|
39
|
+
*/
|
|
40
|
+
declare class DurableRpcPeer<TLocalSchema extends RpcSchema, TRemoteSchema extends RpcSchema, TActor> extends RpcPeer<TLocalSchema, TRemoteSchema> {
|
|
41
|
+
private readonly storage;
|
|
42
|
+
private readonly actor;
|
|
43
|
+
private readonly durableTimeout;
|
|
44
|
+
private durableRequestCounter;
|
|
45
|
+
/**
|
|
46
|
+
* Create a durable RPC peer
|
|
47
|
+
*
|
|
48
|
+
* @param options - Combined RPC peer and durable options
|
|
49
|
+
*/
|
|
50
|
+
constructor(options: RpcPeerOptions<TLocalSchema, TRemoteSchema> & DurableRpcPeerOptions<TActor>);
|
|
51
|
+
/**
|
|
52
|
+
* Make a hibernation-safe RPC call using continuation-passing style
|
|
53
|
+
*
|
|
54
|
+
* Instead of returning a Promise, the result will be passed to the
|
|
55
|
+
* named callback method on the actor. This survives DO hibernation.
|
|
56
|
+
*
|
|
57
|
+
* @param method - Remote method to call
|
|
58
|
+
* @param params - Parameters for the method
|
|
59
|
+
* @param callback - Name of method on actor to call with result
|
|
60
|
+
* @param timeout - Optional timeout override (ms)
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* // Make the call
|
|
65
|
+
* peer.callWithCallback('executeOrder', { market, side }, 'onOrderExecuted');
|
|
66
|
+
*
|
|
67
|
+
* // Define the callback on your actor
|
|
68
|
+
* onOrderExecuted(result: OrderResult, context: CallContext) {
|
|
69
|
+
* console.log('Order executed:', result);
|
|
70
|
+
* console.log('Latency:', context.latencyMs, 'ms');
|
|
71
|
+
* }
|
|
72
|
+
* ```
|
|
73
|
+
*/
|
|
74
|
+
callWithCallback<K extends keyof TRemoteSchema["methods"] & string>(method: K, params: unknown, callback: keyof TActor & string, timeout?: number): void;
|
|
75
|
+
/**
|
|
76
|
+
* Handle an incoming WebSocket message
|
|
77
|
+
*
|
|
78
|
+
* Checks durable storage for continuation-based calls before
|
|
79
|
+
* delegating to the base class for promise-based calls.
|
|
80
|
+
*
|
|
81
|
+
* @param data - Raw WebSocket message data
|
|
82
|
+
*/
|
|
83
|
+
handleMessage(data: WireInput): void;
|
|
84
|
+
/**
|
|
85
|
+
* Get all pending durable calls (for debugging/monitoring)
|
|
86
|
+
*
|
|
87
|
+
* @returns Array of all pending calls
|
|
88
|
+
*/
|
|
89
|
+
getPendingCalls(): PendingCall[];
|
|
90
|
+
/**
|
|
91
|
+
* Get expired calls that have exceeded their timeout
|
|
92
|
+
*
|
|
93
|
+
* @returns Array of calls that have exceeded their timeout
|
|
94
|
+
*/
|
|
95
|
+
getExpiredCalls(): PendingCall[];
|
|
96
|
+
/**
|
|
97
|
+
* Clean up expired calls
|
|
98
|
+
*
|
|
99
|
+
* Call this periodically (e.g., on alarm) to remove stale calls.
|
|
100
|
+
*
|
|
101
|
+
* @returns The expired calls that were removed (for optional error handling)
|
|
102
|
+
*/
|
|
103
|
+
cleanupExpired(): PendingCall[];
|
|
104
|
+
/**
|
|
105
|
+
* Clear all pending durable calls
|
|
106
|
+
*/
|
|
107
|
+
clearPendingCalls(): void;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Create a factory function for DurableRpcPeer instances
|
|
111
|
+
*
|
|
112
|
+
* Pre-configures the durable storage and actor, returning a function
|
|
113
|
+
* that only needs RPC options to create a new peer.
|
|
114
|
+
*
|
|
115
|
+
* @param durableOptions - Durable configuration (storage, actor, timeout)
|
|
116
|
+
* @returns Factory function that creates DurableRpcPeer instances
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```ts
|
|
120
|
+
* const createPeer = createDurableRpcPeerFactory({
|
|
121
|
+
* storage: new SqlPendingCallStorage(sql),
|
|
122
|
+
* actor: this,
|
|
123
|
+
* });
|
|
124
|
+
*
|
|
125
|
+
* // Later, create peers for each connection
|
|
126
|
+
* const peer = createPeer({
|
|
127
|
+
* ws,
|
|
128
|
+
* localSchema,
|
|
129
|
+
* remoteSchema,
|
|
130
|
+
* provider,
|
|
131
|
+
* });
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
134
|
+
declare const createDurableRpcPeerFactory: <TActor>(durableOptions: DurableRpcPeerOptions<TActor>) => <TLocalSchema extends RpcSchema, TRemoteSchema extends RpcSchema>(rpcOptions: RpcPeerOptions<TLocalSchema, TRemoteSchema>) => DurableRpcPeer<TLocalSchema, TRemoteSchema, TActor>;
|
|
135
|
+
//#endregion
|
|
136
|
+
export { CallContext, DurableRpcPeer, DurableRpcPeerOptions, createDurableRpcPeerFactory };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import "../factory-3ziwTuZe.js";
|
|
2
|
+
import "../json-Bshec-bZ.js";
|
|
3
|
+
import "../codecs-BmYG2d_U.js";
|
|
4
|
+
import "../protocol-_mpoOPp6.js";
|
|
5
|
+
import "../errors-5BfreE63.js";
|
|
6
|
+
import "../default-BkrMd28n.js";
|
|
7
|
+
import { n as createDurableRpcPeerFactory, t as DurableRpcPeer } from "../durable-MZjkvyS6.js";
|
|
8
|
+
|
|
9
|
+
export { DurableRpcPeer, createDurableRpcPeerFactory };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import "../schema-CN5HHHku.js";
|
|
2
|
+
import "../factory-C1v0AEHY.js";
|
|
3
|
+
import "../json-54Z2bIIs.js";
|
|
4
|
+
import "../index-Be7jjS77.js";
|
|
5
|
+
import "../protocol-DA84zrc2.js";
|
|
6
|
+
import "../types-Be-qmQu0.js";
|
|
7
|
+
import { n as RpcPeerOptions, t as RpcPeer } from "../default-xDNNMrg0.js";
|
|
8
|
+
import "../interface-C4S-WCqW.js";
|
|
9
|
+
import { CallContext, DurableRpcPeer, DurableRpcPeerOptions } from "./durable.js";
|
|
10
|
+
export { type CallContext, DurableRpcPeer, type DurableRpcPeerOptions, RpcPeer, type RpcPeerOptions };
|