@nimbus-sh/loom 0.1.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 +163 -0
- package/dist/actor.d.ts +222 -0
- package/dist/actor.d.ts.map +1 -0
- package/dist/actor.js +474 -0
- package/dist/callable.d.ts +45 -0
- package/dist/callable.d.ts.map +1 -0
- package/dist/callable.js +60 -0
- package/dist/client.d.ts +58 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +98 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/protocol.d.ts +63 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +45 -0
- package/dist/routing.d.ts +13 -0
- package/dist/routing.d.ts.map +1 -0
- package/dist/routing.js +12 -0
- package/dist/rpc.d.ts +47 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +102 -0
- package/dist/schedules.d.ts +150 -0
- package/dist/schedules.d.ts.map +1 -0
- package/dist/schedules.js +276 -0
- package/package.json +66 -0
- package/src/actor.ts +638 -0
- package/src/callable.ts +76 -0
- package/src/client.ts +153 -0
- package/src/index.ts +19 -0
- package/src/protocol.ts +84 -0
- package/src/routing.ts +19 -0
- package/src/rpc.ts +110 -0
- package/src/schedules.ts +356 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* client.ts — the caller's half of callable RPC: a promise per call, and a
|
|
3
|
+
* typed stub proxy that makes an actor's methods look local.
|
|
4
|
+
*
|
|
5
|
+
* Dependency-free and workerd-free on purpose: the socket is anything with
|
|
6
|
+
* `send` and message listeners — a browser WebSocket, a PartySocket, a
|
|
7
|
+
* server-side WebSocket from `fetch()` — so this module runs wherever the
|
|
8
|
+
* connection was made. Frames and defaults follow the Agents SDK client
|
|
9
|
+
* (verified in `agents` 0.20.1 dist, `client.js:129-151,217-248`): ids are
|
|
10
|
+
* `crypto.randomUUID()`, plain calls time out at 30 s by default, streamed
|
|
11
|
+
* calls (an `onChunk` listener) get no timeout unless one is passed.
|
|
12
|
+
*/
|
|
13
|
+
import { isRpcResponseFrame, RPC_FRAME_TYPE } from './protocol.js';
|
|
14
|
+
export const DEFAULT_CALL_TIMEOUT_MS = 30_000;
|
|
15
|
+
/** Attach an RPC client to a socket. One message listener for all calls. */
|
|
16
|
+
export function actorClient(socket, defaults = {}) {
|
|
17
|
+
const pending = new Map();
|
|
18
|
+
const onMessage = (event) => {
|
|
19
|
+
if (typeof event.data !== 'string')
|
|
20
|
+
return;
|
|
21
|
+
let frame;
|
|
22
|
+
try {
|
|
23
|
+
frame = JSON.parse(event.data);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (!isRpcResponseFrame(frame))
|
|
29
|
+
return;
|
|
30
|
+
const call = pending.get(frame.id);
|
|
31
|
+
if (!call)
|
|
32
|
+
return;
|
|
33
|
+
if (frame.success === false) {
|
|
34
|
+
settle(frame.id)?.reject(new Error(frame.error));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (frame.done === false) {
|
|
38
|
+
call.onChunk?.(frame.result);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
settle(frame.id)?.resolve(frame.result);
|
|
42
|
+
};
|
|
43
|
+
const settle = (id) => {
|
|
44
|
+
const call = pending.get(id);
|
|
45
|
+
if (!call)
|
|
46
|
+
return undefined;
|
|
47
|
+
pending.delete(id);
|
|
48
|
+
if (call.timer !== undefined)
|
|
49
|
+
clearTimeout(call.timer);
|
|
50
|
+
return call;
|
|
51
|
+
};
|
|
52
|
+
socket.addEventListener('message', onMessage);
|
|
53
|
+
const call = (method, args = [], options = {}) => {
|
|
54
|
+
const onChunk = options.onChunk ?? defaults.onChunk;
|
|
55
|
+
const timeoutMs = options.timeoutMs ?? defaults.timeoutMs ?? (onChunk ? undefined : DEFAULT_CALL_TIMEOUT_MS);
|
|
56
|
+
const id = crypto.randomUUID();
|
|
57
|
+
return new Promise((resolve, reject) => {
|
|
58
|
+
const entry = { resolve: resolve, reject, onChunk };
|
|
59
|
+
if (timeoutMs !== undefined) {
|
|
60
|
+
entry.timer = setTimeout(() => {
|
|
61
|
+
pending.delete(id);
|
|
62
|
+
reject(new Error(`RPC call '${method}' timed out after ${timeoutMs}ms`));
|
|
63
|
+
}, timeoutMs);
|
|
64
|
+
}
|
|
65
|
+
pending.set(id, entry);
|
|
66
|
+
try {
|
|
67
|
+
socket.send(JSON.stringify({ type: RPC_FRAME_TYPE, id, method, args }));
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
settle(id);
|
|
71
|
+
reject(e instanceof Error ? e : new Error(String(e)));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
call,
|
|
77
|
+
stub(options) {
|
|
78
|
+
return new Proxy({}, {
|
|
79
|
+
get(_target, name) {
|
|
80
|
+
if (typeof name !== 'string')
|
|
81
|
+
return undefined;
|
|
82
|
+
// `await stub` (or any promise resolution of the stub itself)
|
|
83
|
+
// probes `.then`; answering with a caller would fire an RPC
|
|
84
|
+
// literally named "then".
|
|
85
|
+
if (name === 'then')
|
|
86
|
+
return undefined;
|
|
87
|
+
return (...args) => call(name, args, options);
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
},
|
|
91
|
+
close() {
|
|
92
|
+
socket.removeEventListener('message', onMessage);
|
|
93
|
+
for (const id of [...pending.keys()]) {
|
|
94
|
+
settle(id)?.reject(new Error('RPC client closed'));
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nimbus-sh/loom — the actor framework of Nimbus: partyserver's surface on
|
|
3
|
+
* top, the @nimbus-sh/fabric floor pre-wired underneath.
|
|
4
|
+
*
|
|
5
|
+
* The root export pulls `partyserver`, which imports `cloudflare:workers`,
|
|
6
|
+
* so importing this module outside workerd fails at resolution. Non-workerd
|
|
7
|
+
* consumers (tests, browser clients) import the subpath modules they need —
|
|
8
|
+
* `@nimbus-sh/loom/client.js` is workerd-free by design.
|
|
9
|
+
*/
|
|
10
|
+
export * from './actor.js';
|
|
11
|
+
export * from './callable.js';
|
|
12
|
+
export * from './client.js';
|
|
13
|
+
export * from './protocol.js';
|
|
14
|
+
export * from './routing.js';
|
|
15
|
+
export * from './rpc.js';
|
|
16
|
+
export * from './schedules.js';
|
|
17
|
+
export { Server } from 'partyserver';
|
|
18
|
+
export type { Connection, ConnectionContext, WSMessage } from 'partyserver';
|
|
19
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,aAAa,CAAC;AAC5B,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,gBAAgB,CAAC;AAC/B,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AACrC,YAAY,EAAE,UAAU,EAAE,iBAAiB,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @nimbus-sh/loom — the actor framework of Nimbus: partyserver's surface on
|
|
3
|
+
* top, the @nimbus-sh/fabric floor pre-wired underneath.
|
|
4
|
+
*
|
|
5
|
+
* The root export pulls `partyserver`, which imports `cloudflare:workers`,
|
|
6
|
+
* so importing this module outside workerd fails at resolution. Non-workerd
|
|
7
|
+
* consumers (tests, browser clients) import the subpath modules they need —
|
|
8
|
+
* `@nimbus-sh/loom/client.js` is workerd-free by design.
|
|
9
|
+
*/
|
|
10
|
+
export * from './actor.js';
|
|
11
|
+
export * from './callable.js';
|
|
12
|
+
export * from './client.js';
|
|
13
|
+
export * from './protocol.js';
|
|
14
|
+
export * from './routing.js';
|
|
15
|
+
export * from './rpc.js';
|
|
16
|
+
export * from './schedules.js';
|
|
17
|
+
export { Server } from 'partyserver';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* protocol.ts — the JSON frames loom speaks over a WebSocket connection.
|
|
3
|
+
*
|
|
4
|
+
* The frame shapes and type strings match the Cloudflare Agents SDK
|
|
5
|
+
* (`agents` 0.20.1, verified in its shipped dist: `types.js:5-15` for the
|
|
6
|
+
* type strings, `agent-tool-types` d.ts for the request/response shapes).
|
|
7
|
+
* Matching them is deliberate: a client written for an Agents server — the
|
|
8
|
+
* SDK's own `AgentClient` included — can drive a loom actor's state sync and
|
|
9
|
+
* RPC without translation. Only the frames loom implements are declared
|
|
10
|
+
* here; the Agents-product frames (MCP, identity, sessions) are not.
|
|
11
|
+
*
|
|
12
|
+
* A string message that parses as JSON and carries one of these `type`
|
|
13
|
+
* values is a protocol frame and never reaches the embedder's `onMessage`.
|
|
14
|
+
* Everything else — binary, non-JSON text, JSON with any other `type` —
|
|
15
|
+
* passes through untouched.
|
|
16
|
+
*/
|
|
17
|
+
/** State sync, both directions: `{ type, state }`. */
|
|
18
|
+
export declare const STATE_FRAME_TYPE = "cf_agent_state";
|
|
19
|
+
/** Server → client: a client state update was refused. */
|
|
20
|
+
export declare const STATE_ERROR_FRAME_TYPE = "cf_agent_state_error";
|
|
21
|
+
/** Callable RPC, both directions. */
|
|
22
|
+
export declare const RPC_FRAME_TYPE = "rpc";
|
|
23
|
+
/** Client → server: replace the actor's state. */
|
|
24
|
+
export interface StateFrame<S = unknown> {
|
|
25
|
+
type: typeof STATE_FRAME_TYPE;
|
|
26
|
+
state: S;
|
|
27
|
+
}
|
|
28
|
+
/** Server → client: the refusal a rejected state update earns. */
|
|
29
|
+
export interface StateErrorFrame {
|
|
30
|
+
type: typeof STATE_ERROR_FRAME_TYPE;
|
|
31
|
+
error: string;
|
|
32
|
+
}
|
|
33
|
+
/** Client → server: call one `callable()` method by name. */
|
|
34
|
+
export interface RpcRequestFrame {
|
|
35
|
+
type: typeof RPC_FRAME_TYPE;
|
|
36
|
+
/** Correlates the response frames to the call. The client mints it. */
|
|
37
|
+
id: string;
|
|
38
|
+
method: string;
|
|
39
|
+
args: unknown[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Server → client. A streamed reply is many `done: false` frames and one
|
|
43
|
+
* `done: true`; a plain reply is a single `done: true` frame. `done` absent
|
|
44
|
+
* means final (the Agents client treats it so; loom always sets it).
|
|
45
|
+
*/
|
|
46
|
+
export type RpcResponseFrame = {
|
|
47
|
+
type: typeof RPC_FRAME_TYPE;
|
|
48
|
+
id: string;
|
|
49
|
+
} & ({
|
|
50
|
+
success: true;
|
|
51
|
+
result: unknown;
|
|
52
|
+
done?: boolean;
|
|
53
|
+
} | {
|
|
54
|
+
success: false;
|
|
55
|
+
error: string;
|
|
56
|
+
});
|
|
57
|
+
/** Same guard the Agents server uses: `type` plus a `state` key. */
|
|
58
|
+
export declare function isStateFrame(value: unknown): value is StateFrame;
|
|
59
|
+
/** Same guard the Agents server uses: all four fields, `args` an array. */
|
|
60
|
+
export declare function isRpcRequestFrame(value: unknown): value is RpcRequestFrame;
|
|
61
|
+
/** The client-side guard for response frames. */
|
|
62
|
+
export declare function isRpcResponseFrame(value: unknown): value is RpcResponseFrame;
|
|
63
|
+
//# sourceMappingURL=protocol.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,sDAAsD;AACtD,eAAO,MAAM,gBAAgB,mBAAmB,CAAC;AAEjD,0DAA0D;AAC1D,eAAO,MAAM,sBAAsB,yBAAyB,CAAC;AAE7D,qCAAqC;AACrC,eAAO,MAAM,cAAc,QAAQ,CAAC;AAEpC,kDAAkD;AAClD,MAAM,WAAW,UAAU,CAAC,CAAC,GAAG,OAAO;IACrC,IAAI,EAAE,OAAO,gBAAgB,CAAC;IAC9B,KAAK,EAAE,CAAC,CAAC;CACV;AAED,kEAAkE;AAClE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,OAAO,sBAAsB,CAAC;IACpC,KAAK,EAAE,MAAM,CAAC;CACf;AAED,6DAA6D;AAC7D,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,OAAO,cAAc,CAAC;IAC5B,uEAAuE;IACvE,EAAE,EAAE,MAAM,CAAC;IACX,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB;AAED;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAAE,IAAI,EAAE,OAAO,cAAc,CAAC;IAAC,EAAE,EAAE,MAAM,CAAA;CAAE,GAAG,CACzE;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,OAAO,CAAC;IAAC,IAAI,CAAC,EAAE,OAAO,CAAA;CAAE,GAClD;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,CACpC,CAAC;AAEF,oEAAoE;AACpE,wBAAgB,YAAY,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,UAAU,CAMhE;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,eAAe,CAS1E;AAED,iDAAiD;AACjD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,gBAAgB,CAI5E"}
|
package/dist/protocol.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* protocol.ts — the JSON frames loom speaks over a WebSocket connection.
|
|
3
|
+
*
|
|
4
|
+
* The frame shapes and type strings match the Cloudflare Agents SDK
|
|
5
|
+
* (`agents` 0.20.1, verified in its shipped dist: `types.js:5-15` for the
|
|
6
|
+
* type strings, `agent-tool-types` d.ts for the request/response shapes).
|
|
7
|
+
* Matching them is deliberate: a client written for an Agents server — the
|
|
8
|
+
* SDK's own `AgentClient` included — can drive a loom actor's state sync and
|
|
9
|
+
* RPC without translation. Only the frames loom implements are declared
|
|
10
|
+
* here; the Agents-product frames (MCP, identity, sessions) are not.
|
|
11
|
+
*
|
|
12
|
+
* A string message that parses as JSON and carries one of these `type`
|
|
13
|
+
* values is a protocol frame and never reaches the embedder's `onMessage`.
|
|
14
|
+
* Everything else — binary, non-JSON text, JSON with any other `type` —
|
|
15
|
+
* passes through untouched.
|
|
16
|
+
*/
|
|
17
|
+
/** State sync, both directions: `{ type, state }`. */
|
|
18
|
+
export const STATE_FRAME_TYPE = 'cf_agent_state';
|
|
19
|
+
/** Server → client: a client state update was refused. */
|
|
20
|
+
export const STATE_ERROR_FRAME_TYPE = 'cf_agent_state_error';
|
|
21
|
+
/** Callable RPC, both directions. */
|
|
22
|
+
export const RPC_FRAME_TYPE = 'rpc';
|
|
23
|
+
/** Same guard the Agents server uses: `type` plus a `state` key. */
|
|
24
|
+
export function isStateFrame(value) {
|
|
25
|
+
return (typeof value === 'object' && value !== null
|
|
26
|
+
&& value.type === STATE_FRAME_TYPE
|
|
27
|
+
&& 'state' in value);
|
|
28
|
+
}
|
|
29
|
+
/** Same guard the Agents server uses: all four fields, `args` an array. */
|
|
30
|
+
export function isRpcRequestFrame(value) {
|
|
31
|
+
if (typeof value !== 'object' || value === null)
|
|
32
|
+
return false;
|
|
33
|
+
const frame = value;
|
|
34
|
+
return (frame.type === RPC_FRAME_TYPE
|
|
35
|
+
&& typeof frame.id === 'string'
|
|
36
|
+
&& typeof frame.method === 'string'
|
|
37
|
+
&& Array.isArray(frame.args));
|
|
38
|
+
}
|
|
39
|
+
/** The client-side guard for response frames. */
|
|
40
|
+
export function isRpcResponseFrame(value) {
|
|
41
|
+
if (typeof value !== 'object' || value === null)
|
|
42
|
+
return false;
|
|
43
|
+
const frame = value;
|
|
44
|
+
return frame.type === RPC_FRAME_TYPE && typeof frame.id === 'string' && typeof frame.success === 'boolean';
|
|
45
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routing.ts — URL → binding → named actor instance.
|
|
3
|
+
*
|
|
4
|
+
* partyserver's router already is the mechanism loom wants: it maps
|
|
5
|
+
* `/${prefix}/<binding-kebab>/<name>` onto the Durable Object binding, with
|
|
6
|
+
* `onBeforeConnect`/`onBeforeRequest` gates, CORS, jurisdiction, location
|
|
7
|
+
* hints, and retry over transient DO routing errors. Wrapping it in a
|
|
8
|
+
* parallel implementation would be a drift channel, so loom re-exports it
|
|
9
|
+
* under its own names. The URL convention stays `/parties/...` by default —
|
|
10
|
+
* it is the wire contract PartySocket-family clients build URLs from.
|
|
11
|
+
*/
|
|
12
|
+
export { routePartykitRequest as routeActorRequest, getServerByName as getActorByName, type PartyServerOptions as ActorRoutingOptions, type Lobby, type RoutingRetryOptions, } from 'partyserver';
|
|
13
|
+
//# sourceMappingURL=routing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routing.d.ts","sourceRoot":"","sources":["../src/routing.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EACL,oBAAoB,IAAI,iBAAiB,EACzC,eAAe,IAAI,cAAc,EACjC,KAAK,kBAAkB,IAAI,mBAAmB,EAC9C,KAAK,KAAK,EACV,KAAK,mBAAmB,GACzB,MAAM,aAAa,CAAC"}
|
package/dist/routing.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* routing.ts — URL → binding → named actor instance.
|
|
3
|
+
*
|
|
4
|
+
* partyserver's router already is the mechanism loom wants: it maps
|
|
5
|
+
* `/${prefix}/<binding-kebab>/<name>` onto the Durable Object binding, with
|
|
6
|
+
* `onBeforeConnect`/`onBeforeRequest` gates, CORS, jurisdiction, location
|
|
7
|
+
* hints, and retry over transient DO routing errors. Wrapping it in a
|
|
8
|
+
* parallel implementation would be a drift channel, so loom re-exports it
|
|
9
|
+
* under its own names. The URL convention stays `/parties/...` by default —
|
|
10
|
+
* it is the wire contract PartySocket-family clients build URLs from.
|
|
11
|
+
*/
|
|
12
|
+
export { routePartykitRequest as routeActorRequest, getServerByName as getActorByName, } from 'partyserver';
|
package/dist/rpc.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rpc.ts — the server half of callable RPC: dispatch one request frame to a
|
|
3
|
+
* marked method and answer over the connection.
|
|
4
|
+
*
|
|
5
|
+
* The dispatch contract mirrors the Agents SDK (verified in `agents` 0.20.1
|
|
6
|
+
* dist, `index.js:883-934`):
|
|
7
|
+
* - the method must exist on the target AND carry the `callable()` mark;
|
|
8
|
+
* either failure is an error frame, not a crash.
|
|
9
|
+
* - a streaming method receives a {@link StreamingResponse} PREPENDED to
|
|
10
|
+
* the caller's arguments and owns its own reply frames; if it throws
|
|
11
|
+
* with the stream still open, the error closes the stream.
|
|
12
|
+
* - a plain method's awaited return value goes back as one
|
|
13
|
+
* `{ success: true, done: true }` frame; a throw becomes
|
|
14
|
+
* `{ success: false, error }` carrying the message only, never the stack.
|
|
15
|
+
*
|
|
16
|
+
* Sends tolerate exactly one failure: workerd throws a TypeError containing
|
|
17
|
+
* "WebSocket send() after close" when the peer is gone, and a reply that
|
|
18
|
+
* cannot be delivered has no one waiting for it. Every other send error is
|
|
19
|
+
* a real bug and propagates.
|
|
20
|
+
*/
|
|
21
|
+
import { type RpcRequestFrame } from './protocol.js';
|
|
22
|
+
/** The connection as RPC writes to it. partyserver's `Connection` satisfies it. */
|
|
23
|
+
export interface RpcConnection {
|
|
24
|
+
send(message: string): void;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The reply channel a streaming callable writes through. One `end()` (or
|
|
28
|
+
* `error()`) closes it; every later write is a refused no-op that returns
|
|
29
|
+
* false, matching the Agents SDK's `StreamingResponse` (dist
|
|
30
|
+
* `index.js:7169-7233`).
|
|
31
|
+
*/
|
|
32
|
+
export declare class StreamingResponse {
|
|
33
|
+
#private;
|
|
34
|
+
private readonly connection;
|
|
35
|
+
private readonly id;
|
|
36
|
+
constructor(connection: RpcConnection, id: string);
|
|
37
|
+
get isClosed(): boolean;
|
|
38
|
+
/** One chunk. Returns false when the stream is closed or the peer is gone. */
|
|
39
|
+
send(chunk: unknown): boolean;
|
|
40
|
+
/** Close the stream, with an optional final chunk. */
|
|
41
|
+
end(finalChunk?: unknown): boolean;
|
|
42
|
+
/** Close the stream with an error the caller's promise rejects on. */
|
|
43
|
+
error(message: string): boolean;
|
|
44
|
+
}
|
|
45
|
+
/** Run one request frame against the target and reply on the connection. */
|
|
46
|
+
export declare function dispatchRpc(target: object, connection: RpcConnection, frame: RpcRequestFrame): Promise<void>;
|
|
47
|
+
//# sourceMappingURL=rpc.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rpc.d.ts","sourceRoot":"","sources":["../src/rpc.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAGH,OAAO,EAAkB,KAAK,eAAe,EAAyB,MAAM,eAAe,CAAC;AAE5F,mFAAmF;AACnF,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAYD;;;;;GAKG;AACH,qBAAa,iBAAiB;;IAI1B,OAAO,CAAC,QAAQ,CAAC,UAAU;IAC3B,OAAO,CAAC,QAAQ,CAAC,EAAE;gBADF,UAAU,EAAE,aAAa,EACzB,EAAE,EAAE,MAAM;IAG7B,IAAI,QAAQ,IAAI,OAAO,CAEtB;IAED,8EAA8E;IAC9E,IAAI,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO;IAQ7B,sDAAsD;IACtD,GAAG,CAAC,UAAU,CAAC,EAAE,OAAO,GAAG,OAAO;IAMlC,sEAAsE;IACtE,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO;CAKhC;AAED,4EAA4E;AAC5E,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,aAAa,EACzB,KAAK,EAAE,eAAe,GACrB,OAAO,CAAC,IAAI,CAAC,CAmBf"}
|
package/dist/rpc.js
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rpc.ts — the server half of callable RPC: dispatch one request frame to a
|
|
3
|
+
* marked method and answer over the connection.
|
|
4
|
+
*
|
|
5
|
+
* The dispatch contract mirrors the Agents SDK (verified in `agents` 0.20.1
|
|
6
|
+
* dist, `index.js:883-934`):
|
|
7
|
+
* - the method must exist on the target AND carry the `callable()` mark;
|
|
8
|
+
* either failure is an error frame, not a crash.
|
|
9
|
+
* - a streaming method receives a {@link StreamingResponse} PREPENDED to
|
|
10
|
+
* the caller's arguments and owns its own reply frames; if it throws
|
|
11
|
+
* with the stream still open, the error closes the stream.
|
|
12
|
+
* - a plain method's awaited return value goes back as one
|
|
13
|
+
* `{ success: true, done: true }` frame; a throw becomes
|
|
14
|
+
* `{ success: false, error }` carrying the message only, never the stack.
|
|
15
|
+
*
|
|
16
|
+
* Sends tolerate exactly one failure: workerd throws a TypeError containing
|
|
17
|
+
* "WebSocket send() after close" when the peer is gone, and a reply that
|
|
18
|
+
* cannot be delivered has no one waiting for it. Every other send error is
|
|
19
|
+
* a real bug and propagates.
|
|
20
|
+
*/
|
|
21
|
+
import { callableMetadata, isCallable } from './callable.js';
|
|
22
|
+
import { RPC_FRAME_TYPE } from './protocol.js';
|
|
23
|
+
function sendIfOpen(connection, frame) {
|
|
24
|
+
try {
|
|
25
|
+
connection.send(JSON.stringify(frame));
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch (e) {
|
|
29
|
+
if (e instanceof TypeError && e.message.includes('WebSocket send() after close'))
|
|
30
|
+
return false;
|
|
31
|
+
throw e;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* The reply channel a streaming callable writes through. One `end()` (or
|
|
36
|
+
* `error()`) closes it; every later write is a refused no-op that returns
|
|
37
|
+
* false, matching the Agents SDK's `StreamingResponse` (dist
|
|
38
|
+
* `index.js:7169-7233`).
|
|
39
|
+
*/
|
|
40
|
+
export class StreamingResponse {
|
|
41
|
+
connection;
|
|
42
|
+
id;
|
|
43
|
+
#closed = false;
|
|
44
|
+
constructor(connection, id) {
|
|
45
|
+
this.connection = connection;
|
|
46
|
+
this.id = id;
|
|
47
|
+
}
|
|
48
|
+
get isClosed() {
|
|
49
|
+
return this.#closed;
|
|
50
|
+
}
|
|
51
|
+
/** One chunk. Returns false when the stream is closed or the peer is gone. */
|
|
52
|
+
send(chunk) {
|
|
53
|
+
if (this.#closed) {
|
|
54
|
+
console.warn('loom: StreamingResponse.send() after the stream was closed — chunk not sent');
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
return sendIfOpen(this.connection, { type: RPC_FRAME_TYPE, id: this.id, success: true, result: chunk, done: false });
|
|
58
|
+
}
|
|
59
|
+
/** Close the stream, with an optional final chunk. */
|
|
60
|
+
end(finalChunk) {
|
|
61
|
+
if (this.#closed)
|
|
62
|
+
return false;
|
|
63
|
+
this.#closed = true;
|
|
64
|
+
return sendIfOpen(this.connection, { type: RPC_FRAME_TYPE, id: this.id, success: true, result: finalChunk, done: true });
|
|
65
|
+
}
|
|
66
|
+
/** Close the stream with an error the caller's promise rejects on. */
|
|
67
|
+
error(message) {
|
|
68
|
+
if (this.#closed)
|
|
69
|
+
return false;
|
|
70
|
+
this.#closed = true;
|
|
71
|
+
return sendIfOpen(this.connection, { type: RPC_FRAME_TYPE, id: this.id, success: false, error: message });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/** Run one request frame against the target and reply on the connection. */
|
|
75
|
+
export async function dispatchRpc(target, connection, frame) {
|
|
76
|
+
const method = target[frame.method];
|
|
77
|
+
try {
|
|
78
|
+
if (typeof method !== 'function')
|
|
79
|
+
throw new Error(`Method ${frame.method} does not exist`);
|
|
80
|
+
if (!isCallable(method))
|
|
81
|
+
throw new Error(`Method ${frame.method} is not callable`);
|
|
82
|
+
if (callableMetadata(method)?.streaming) {
|
|
83
|
+
const stream = new StreamingResponse(connection, frame.id);
|
|
84
|
+
try {
|
|
85
|
+
await method.apply(target, [stream, ...frame.args]);
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
if (!stream.isClosed)
|
|
89
|
+
stream.error(errorMessage(e));
|
|
90
|
+
}
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const result = await method.apply(target, frame.args);
|
|
94
|
+
sendIfOpen(connection, { type: RPC_FRAME_TYPE, id: frame.id, success: true, result, done: true });
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
sendIfOpen(connection, { type: RPC_FRAME_TYPE, id: frame.id, success: false, error: errorMessage(e) });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
function errorMessage(error) {
|
|
101
|
+
return error instanceof Error ? error.message : 'Unknown error occurred';
|
|
102
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schedules.ts — named-method schedules persisted in the actor's own SQLite,
|
|
3
|
+
* dispatched from ONE reason in fabric's timer map.
|
|
4
|
+
*
|
|
5
|
+
* The API shape follows the Agents SDK (`agents` 0.20.1, verified in its
|
|
6
|
+
* shipped dist): `when` is a delay in seconds, a Date, or a cron string;
|
|
7
|
+
* recurrence is either `cron` (recomputed per fire) or `interval` (fixed
|
|
8
|
+
* seconds); rows carry the callback METHOD NAME and a JSON payload, and
|
|
9
|
+
* dispatch resolves the method on the target at fire time. Where the two
|
|
10
|
+
* differ, fabric's mechanism is the reason:
|
|
11
|
+
*
|
|
12
|
+
* - RETRIES ARE DURABLE. The Agents SDK retries a failed callback
|
|
13
|
+
* in-process, sleeping inside the alarm turn (dist `retries.js`, "Full
|
|
14
|
+
* Jitter"); an instance reset mid-backoff loses the remaining attempts.
|
|
15
|
+
* Here a failed attempt writes the backed-off deadline into the row and
|
|
16
|
+
* the alarm re-fires on it — the attempt budget survives the reset.
|
|
17
|
+
* Backoff is `min(maxDelayMs, baseDelayMs * 2**(attempt-1))`, no jitter:
|
|
18
|
+
* the contended resource jitter exists for is a shared endpoint, and one
|
|
19
|
+
* actor's own alarm is not shared.
|
|
20
|
+
* - `alarmInfo` REACHES THE CALLBACK. The platform reports alarm retries
|
|
21
|
+
* (`isRetry`, `retryCount`); the Agents SDK drops the report (zero
|
|
22
|
+
* references in its dist). Fabric's dispatcher forwards it, and every
|
|
23
|
+
* schedule callback receives it in its invocation argument.
|
|
24
|
+
* - NO INTERVAL MUTEX. The Agents SDK guards overlapping interval runs
|
|
25
|
+
* with a `running` flag and a hung-run timeout. Fabric's timer chain
|
|
26
|
+
* already serializes dispatches on the instance, so overlap cannot
|
|
27
|
+
* happen and the flag would be dead state.
|
|
28
|
+
*
|
|
29
|
+
* Times are epoch MILLISECONDS everywhere, matching fabric's timer map (the
|
|
30
|
+
* Agents SDK stores unix seconds).
|
|
31
|
+
*
|
|
32
|
+
* This module is deliberately partyserver-free: it owns rows and dispatch,
|
|
33
|
+
* and the Actor owns the timer-reason wiring around it.
|
|
34
|
+
*/
|
|
35
|
+
import type { TimerAlarmInfo } from '@nimbus-sh/fabric/timers.js';
|
|
36
|
+
/** Synchronous DO SQLite, as the store uses it. */
|
|
37
|
+
export interface ScheduleSqlExec {
|
|
38
|
+
exec(query: string, ...bindings: Array<string | number | null>): Iterable<unknown>;
|
|
39
|
+
}
|
|
40
|
+
export interface ScheduleContext {
|
|
41
|
+
storage: {
|
|
42
|
+
sql: ScheduleSqlExec;
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Attempts and backoff for one schedule's callback. Defaults match the
|
|
47
|
+
* Agents SDK's (`maxAttempts: 3, baseDelayMs: 100, maxDelayMs: 3000`,
|
|
48
|
+
* dist `index.js:303-308`).
|
|
49
|
+
*/
|
|
50
|
+
export interface ScheduleRetryPolicy {
|
|
51
|
+
maxAttempts?: number;
|
|
52
|
+
baseDelayMs?: number;
|
|
53
|
+
maxDelayMs?: number;
|
|
54
|
+
}
|
|
55
|
+
export declare const SCHEDULE_RETRY_DEFAULTS: Required<ScheduleRetryPolicy>;
|
|
56
|
+
export interface ScheduleOptions {
|
|
57
|
+
retry?: ScheduleRetryPolicy;
|
|
58
|
+
}
|
|
59
|
+
/** One schedule, as `getScheduleById`/`listSchedules` return it. */
|
|
60
|
+
export type Schedule<T = unknown> = {
|
|
61
|
+
id: string;
|
|
62
|
+
/** The method name dispatch resolves on the actor at fire time. */
|
|
63
|
+
callback: string;
|
|
64
|
+
payload: T;
|
|
65
|
+
retry?: ScheduleRetryPolicy;
|
|
66
|
+
} & ({
|
|
67
|
+
type: 'scheduled';
|
|
68
|
+
time: number;
|
|
69
|
+
} | {
|
|
70
|
+
type: 'delayed';
|
|
71
|
+
time: number;
|
|
72
|
+
delayInSeconds: number;
|
|
73
|
+
} | {
|
|
74
|
+
type: 'cron';
|
|
75
|
+
time: number;
|
|
76
|
+
cron: string;
|
|
77
|
+
} | {
|
|
78
|
+
type: 'interval';
|
|
79
|
+
time: number;
|
|
80
|
+
intervalSeconds: number;
|
|
81
|
+
});
|
|
82
|
+
export type ScheduleType = Schedule['type'];
|
|
83
|
+
export interface ScheduleCriteria {
|
|
84
|
+
id?: string;
|
|
85
|
+
type?: ScheduleType;
|
|
86
|
+
/** Bounds on the next fire time. */
|
|
87
|
+
timeRange?: {
|
|
88
|
+
start?: Date;
|
|
89
|
+
end?: Date;
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/** The second argument every schedule callback receives. */
|
|
93
|
+
export interface ScheduleInvocation<T = unknown> {
|
|
94
|
+
schedule: Schedule<T>;
|
|
95
|
+
/** 1 on the first try; grows with the durable retry rows. */
|
|
96
|
+
attempt: number;
|
|
97
|
+
/** The platform's alarm-invocation report, when the platform gave one. */
|
|
98
|
+
alarmInfo?: TimerAlarmInfo;
|
|
99
|
+
}
|
|
100
|
+
export interface ScheduleDispatchResult {
|
|
101
|
+
ran: number;
|
|
102
|
+
/** The earliest remaining deadline, for the timer handler's re-arm. */
|
|
103
|
+
rearmAt: number | null;
|
|
104
|
+
}
|
|
105
|
+
export declare class ScheduleStore {
|
|
106
|
+
private readonly ctx;
|
|
107
|
+
private schemaReady;
|
|
108
|
+
constructor(ctx: ScheduleContext);
|
|
109
|
+
private ensureSchema;
|
|
110
|
+
/**
|
|
111
|
+
* Persist one schedule. `when` selects the type: a non-negative number is
|
|
112
|
+
* a delay in seconds, a Date is an absolute fire time, a string is a cron
|
|
113
|
+
* expression (validated here; an invalid one throws now, not at the
|
|
114
|
+
* alarm).
|
|
115
|
+
*/
|
|
116
|
+
create<T>(when: number | Date | string, callback: string, payload?: T, options?: ScheduleOptions & {
|
|
117
|
+
now?: number;
|
|
118
|
+
}): Schedule<T>;
|
|
119
|
+
/** Persist a fixed-interval schedule. First fire is one interval from now. */
|
|
120
|
+
every<T>(intervalSeconds: number, callback: string, payload?: T, options?: ScheduleOptions & {
|
|
121
|
+
now?: number;
|
|
122
|
+
}): Schedule<T>;
|
|
123
|
+
private insert;
|
|
124
|
+
byId<T = unknown>(id: string): Schedule<T> | undefined;
|
|
125
|
+
list<T = unknown>(criteria?: ScheduleCriteria): Array<Schedule<T>>;
|
|
126
|
+
/** True when the id existed and is now gone. */
|
|
127
|
+
cancel(id: string): boolean;
|
|
128
|
+
/** The earliest pending deadline, or null when nothing is scheduled. */
|
|
129
|
+
nextDue(): number | null;
|
|
130
|
+
/**
|
|
131
|
+
* Fire every due schedule against the target, in deadline order.
|
|
132
|
+
*
|
|
133
|
+
* Each callback is `target[callback](payload, invocation)`, awaited in
|
|
134
|
+
* place. The due set is snapshotted up front, and every row is re-read
|
|
135
|
+
* before it fires — a callback that cancels or reschedules a SIBLING due
|
|
136
|
+
* row must win over the snapshot, or `cancel()`'s true would be a lie.
|
|
137
|
+
* Success advances the row: a cron recomputes from its expression, an
|
|
138
|
+
* interval re-arms `intervalSeconds` from this dispatch, a one-shot row
|
|
139
|
+
* is deleted. Failure spends one attempt of the row's retry policy: with
|
|
140
|
+
* budget left, the row's deadline moves to the backed-off time and the
|
|
141
|
+
* row survives; with the budget spent — or on a failure no retry can fix
|
|
142
|
+
* (the callback is not a method on the target; the stored payload does
|
|
143
|
+
* not parse) — `onError` hears about it and the row advances as if it
|
|
144
|
+
* had run.
|
|
145
|
+
*/
|
|
146
|
+
dispatchDue(target: object, now: number, alarmInfo?: TimerAlarmInfo, onError?: (schedule: Schedule, error: unknown) => void): Promise<ScheduleDispatchResult>;
|
|
147
|
+
/** Move a row past a completed (or abandoned) fire. */
|
|
148
|
+
private advance;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=schedules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schedules.d.ts","sourceRoot":"","sources":["../src/schedules.ts"],"names":[],"mappings":"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"}
|