@fuzdev/fuz_app 0.24.0 → 0.26.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/dist/actions/action_codegen.d.ts +25 -0
- package/dist/actions/action_codegen.d.ts.map +1 -1
- package/dist/actions/action_codegen.js +39 -0
- package/dist/actions/action_peer.d.ts +7 -0
- package/dist/actions/action_peer.d.ts.map +1 -1
- package/dist/actions/action_peer.js +1 -1
- package/dist/actions/action_types.d.ts +16 -1
- package/dist/actions/action_types.d.ts.map +1 -1
- package/dist/actions/cancel.d.ts +78 -0
- package/dist/actions/cancel.d.ts.map +1 -0
- package/dist/actions/cancel.js +79 -0
- package/dist/actions/register_action_ws.d.ts.map +1 -1
- package/dist/actions/register_action_ws.js +62 -22
- package/dist/actions/rpc_client.d.ts +10 -0
- package/dist/actions/rpc_client.d.ts.map +1 -1
- package/dist/actions/rpc_client.js +22 -7
- package/dist/actions/socket.svelte.d.ts +22 -10
- package/dist/actions/socket.svelte.d.ts.map +1 -1
- package/dist/actions/socket.svelte.js +46 -12
- package/dist/actions/transports.d.ts +14 -3
- package/dist/actions/transports.d.ts.map +1 -1
- package/dist/actions/transports_http.d.ts +3 -3
- package/dist/actions/transports_http.d.ts.map +1 -1
- package/dist/actions/transports_http.js +4 -3
- package/dist/actions/transports_ws.d.ts +33 -6
- package/dist/actions/transports_ws.d.ts.map +1 -1
- package/dist/actions/transports_ws.js +43 -46
- package/dist/actions/transports_ws_backend.d.ts +12 -3
- package/dist/actions/transports_ws_backend.d.ts.map +1 -1
- package/dist/actions/transports_ws_backend.js +12 -1
- package/dist/auth/bearer_auth.js +0 -1
- package/dist/auth/keyring.d.ts.map +1 -1
- package/dist/auth/keyring.js +0 -2
- package/dist/auth/migrations.js +4 -4
- package/dist/db/migrate.d.ts +12 -2
- package/dist/db/migrate.d.ts.map +1 -1
- package/dist/db/migrate.js +25 -16
- package/dist/db/status.d.ts.map +1 -1
- package/dist/db/status.js +0 -2
- package/dist/dev/setup.d.ts +34 -0
- package/dist/dev/setup.d.ts.map +1 -1
- package/dist/dev/setup.js +50 -2
- package/dist/http/db_routes.d.ts.map +1 -1
- package/dist/http/db_routes.js +0 -1
- package/dist/testing/admin_integration.d.ts.map +1 -1
- package/dist/testing/admin_integration.js +0 -3
- package/dist/testing/app_server.js +1 -1
- package/dist/testing/data_exposure.js +6 -8
- package/dist/testing/db.js +1 -1
- package/dist/testing/integration.js +0 -1
- package/dist/testing/rate_limiting.d.ts.map +1 -1
- package/dist/testing/rate_limiting.js +0 -6
- package/dist/testing/rpc_round_trip.js +4 -4
- package/dist/testing/sse_round_trip.d.ts.map +1 -1
- package/dist/testing/sse_round_trip.js +1 -2
- package/package.json +2 -2
|
@@ -110,5 +110,30 @@ export declare const to_action_spec_output_identifier: (method: string) => strin
|
|
|
110
110
|
*/
|
|
111
111
|
export declare const get_innermost_type: (schema: z.ZodType) => z.ZodType;
|
|
112
112
|
export declare const get_innermost_type_name: (schema: z.ZodType) => string;
|
|
113
|
+
/**
|
|
114
|
+
* Generates one method line of the typed `ActionsApi` interface for a single
|
|
115
|
+
* spec. Encapsulates the input/options/return-type signature shape so the
|
|
116
|
+
* surface evolves in one place when fields like `signal` or `transport_name`
|
|
117
|
+
* are added to per-call options.
|
|
118
|
+
*
|
|
119
|
+
* Async methods (request_response, async local_call) get an optional second
|
|
120
|
+
* `options?: RpcClientCallOptions` arg (`{signal?, transport_name?}`). Sync
|
|
121
|
+
* local_call methods omit the options arg — `signal` can't cooperatively
|
|
122
|
+
* interrupt a synchronous handler and there's no transport to select.
|
|
123
|
+
*
|
|
124
|
+
* Consumers must import `ActionInputs`, `ActionOutputs`, `Result`,
|
|
125
|
+
* `JsonrpcErrorObject`, and (for async) `RpcClientCallOptions` into the
|
|
126
|
+
* generated module — the helper only emits the type references.
|
|
127
|
+
*
|
|
128
|
+
* @param spec - the action spec to emit
|
|
129
|
+
* @param options.sync_returns_value - when true (default), sync local_call
|
|
130
|
+
* methods return the output value directly; when false they're wrapped in
|
|
131
|
+
* `Result<{value, error}>` like async methods. Set to `false` if your
|
|
132
|
+
* ActionsApi treats every method uniformly.
|
|
133
|
+
* @returns one line like `foo: (input: ActionInputs['foo'], options?: RpcClientCallOptions) => Promise<Result<...>>;`
|
|
134
|
+
*/
|
|
135
|
+
export declare const generate_actions_api_method_signature: (spec: ActionSpecUnion, options?: {
|
|
136
|
+
sync_returns_value?: boolean;
|
|
137
|
+
}) => string;
|
|
113
138
|
export {};
|
|
114
139
|
//# sourceMappingURL=action_codegen.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action_codegen.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_codegen.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,OAAO,KAAK,EAAC,eAAe,EAAE,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAOxE;;GAEG;AACH,UAAU,UAAU;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,WAAW,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,aAAa;;IACzB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAa;IAE1D;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAQrC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAI1C;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI;IAOrD;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI;IAgCtD;;;OAGG;IACH,KAAK,IAAI,MAAM;IAIf;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;OAGG;IACH,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC;IAIxB;;OAEG;IACH,KAAK,IAAI,IAAI;CAqDb;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,eAAe,EACrB,UAAU,UAAU,GAAG,SAAS,KAC9B,KAAK,CAAC,gBAAgB,CA4DxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,eAAe,EACrB,OAAO,gBAAgB,EACvB,SAAS,aAAa,EACtB,aAAa,MAAM,KACjB,MAkBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,eAAe,EACrB,UAAU,UAAU,GAAG,SAAS,EAChC,SAAS,aAAa,EACtB,UAAU;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAC,KACpC,MA4BF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,aAAa,MAAM,KAAG,MACU,CAAC;AAG/D,eAAO,MAAM,yBAAyB,GAAI,QAAQ,MAAM,KAAG,MAAiC,CAAC;AAC7F,eAAO,MAAM,+BAA+B,GAAI,QAAQ,MAAM,KAAG,MACpB,CAAC;AAC9C,eAAO,MAAM,gCAAgC,GAAI,QAAQ,MAAM,KAAG,MACpB,CAAC;AAE/C;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,CAAC,CAAC,OAwBxD,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,MAI3D,CAAC"}
|
|
1
|
+
{"version":3,"file":"action_codegen.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_codegen.ts"],"names":[],"mappings":"AACA,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAGtB,OAAO,KAAK,EAAC,eAAe,EAAE,gBAAgB,EAAC,MAAM,kBAAkB,CAAC;AAOxE;;GAEG;AACH,UAAU,UAAU;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,WAAW,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,qBAAa,aAAa;;IACzB,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAa;IAE1D;;;;OAIG;IACH,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAQrC;;;;OAIG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI;IAI1C;;OAEG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI;IAOrD;;OAEG;IACH,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,KAAK,EAAE,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI;IAgCtD;;;OAGG;IACH,KAAK,IAAI,MAAM;IAIf;;OAEG;IACH,WAAW,IAAI,OAAO;IAItB;;OAEG;IACH,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;OAGG;IACH,OAAO,IAAI,KAAK,CAAC,MAAM,CAAC;IAIxB;;OAEG;IACH,KAAK,IAAI,IAAI;CAqDb;AAED;;GAEG;AACH,eAAO,MAAM,mBAAmB,GAC/B,MAAM,eAAe,EACrB,UAAU,UAAU,GAAG,SAAS,KAC9B,KAAK,CAAC,gBAAgB,CA4DxB,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,eAAe,EACrB,OAAO,gBAAgB,EACvB,SAAS,aAAa,EACtB,aAAa,MAAM,KACjB,MAkBF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,uBAAuB,GACnC,MAAM,eAAe,EACrB,UAAU,UAAU,GAAG,SAAS,EAChC,SAAS,aAAa,EACtB,UAAU;IAAC,iBAAiB,CAAC,EAAE,MAAM,CAAA;CAAC,KACpC,MA4BF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,aAAa,MAAM,KAAG,MACU,CAAC;AAG/D,eAAO,MAAM,yBAAyB,GAAI,QAAQ,MAAM,KAAG,MAAiC,CAAC;AAC7F,eAAO,MAAM,+BAA+B,GAAI,QAAQ,MAAM,KAAG,MACpB,CAAC;AAC9C,eAAO,MAAM,gCAAgC,GAAI,QAAQ,MAAM,KAAG,MACpB,CAAC;AAE/C;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,CAAC,CAAC,OAwBxD,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,QAAQ,CAAC,CAAC,OAAO,KAAG,MAI3D,CAAC;AAEF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,eAAO,MAAM,qCAAqC,GACjD,MAAM,eAAe,EACrB,UAAU;IAAC,kBAAkB,CAAC,EAAE,OAAO,CAAA;CAAC,KACtC,MAmBF,CAAC"}
|
|
@@ -316,3 +316,42 @@ export const get_innermost_type_name = (schema) => {
|
|
|
316
316
|
const def = innermost.def;
|
|
317
317
|
return def.type;
|
|
318
318
|
};
|
|
319
|
+
/**
|
|
320
|
+
* Generates one method line of the typed `ActionsApi` interface for a single
|
|
321
|
+
* spec. Encapsulates the input/options/return-type signature shape so the
|
|
322
|
+
* surface evolves in one place when fields like `signal` or `transport_name`
|
|
323
|
+
* are added to per-call options.
|
|
324
|
+
*
|
|
325
|
+
* Async methods (request_response, async local_call) get an optional second
|
|
326
|
+
* `options?: RpcClientCallOptions` arg (`{signal?, transport_name?}`). Sync
|
|
327
|
+
* local_call methods omit the options arg — `signal` can't cooperatively
|
|
328
|
+
* interrupt a synchronous handler and there's no transport to select.
|
|
329
|
+
*
|
|
330
|
+
* Consumers must import `ActionInputs`, `ActionOutputs`, `Result`,
|
|
331
|
+
* `JsonrpcErrorObject`, and (for async) `RpcClientCallOptions` into the
|
|
332
|
+
* generated module — the helper only emits the type references.
|
|
333
|
+
*
|
|
334
|
+
* @param spec - the action spec to emit
|
|
335
|
+
* @param options.sync_returns_value - when true (default), sync local_call
|
|
336
|
+
* methods return the output value directly; when false they're wrapped in
|
|
337
|
+
* `Result<{value, error}>` like async methods. Set to `false` if your
|
|
338
|
+
* ActionsApi treats every method uniformly.
|
|
339
|
+
* @returns one line like `foo: (input: ActionInputs['foo'], options?: RpcClientCallOptions) => Promise<Result<...>>;`
|
|
340
|
+
*/
|
|
341
|
+
export const generate_actions_api_method_signature = (spec, options) => {
|
|
342
|
+
const sync_returns_value = options?.sync_returns_value ?? true;
|
|
343
|
+
const innermost_type_name = get_innermost_type_name(spec.input);
|
|
344
|
+
const has_input = innermost_type_name !== 'null' && innermost_type_name !== 'void';
|
|
345
|
+
const input_param = has_input
|
|
346
|
+
? `input${spec.input.safeParse(undefined).success ? '?' : ''}: ActionInputs['${spec.method}']`
|
|
347
|
+
: 'input?: void';
|
|
348
|
+
const is_async = spec.kind === 'request_response' || (spec.kind === 'local_call' && spec.async);
|
|
349
|
+
const options_param = is_async ? ', options?: RpcClientCallOptions' : '';
|
|
350
|
+
const result_return = `Result<{value: ActionOutputs['${spec.method}']}, {error: JsonrpcErrorObject}>`;
|
|
351
|
+
const return_type = is_async
|
|
352
|
+
? `Promise<${result_return}>`
|
|
353
|
+
: sync_returns_value
|
|
354
|
+
? `ActionOutputs['${spec.method}']`
|
|
355
|
+
: result_return;
|
|
356
|
+
return `${spec.method}: (${input_param}${options_param}) => ${return_type};`;
|
|
357
|
+
};
|
|
@@ -11,6 +11,13 @@ import { Transports, type TransportName } from './transports.js';
|
|
|
11
11
|
import type { ActionEventEnvironment } from './action_event_types.js';
|
|
12
12
|
export interface ActionPeerSendOptions {
|
|
13
13
|
transport_name?: TransportName;
|
|
14
|
+
/**
|
|
15
|
+
* Per-call `AbortSignal`. Forwarded into the chosen transport's `send`,
|
|
16
|
+
* which bottoms out at `FrontendWebsocketClient.request({signal})` for WS
|
|
17
|
+
* (sends the shared `cancel` notification on abort) and at
|
|
18
|
+
* `fetch({signal})` for HTTP. Backend transport ignores it.
|
|
19
|
+
*/
|
|
20
|
+
signal?: AbortSignal;
|
|
14
21
|
}
|
|
15
22
|
export interface ActionPeerOptions {
|
|
16
23
|
environment: ActionEventEnvironment;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action_peer.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_peer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEN,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAU5B,OAAO,EAAC,UAAU,EAAE,KAAK,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAC/D,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,yBAAyB,CAAC;AAOpE,MAAM,WAAW,qBAAqB;IACrC,cAAc,CAAC,EAAE,aAAa,CAAC;
|
|
1
|
+
{"version":3,"file":"action_peer.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_peer.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAEN,gCAAgC,EAChC,mBAAmB,EACnB,cAAc,EACd,sBAAsB,EACtB,oBAAoB,EACpB,MAAM,oBAAoB,CAAC;AAU5B,OAAO,EAAC,UAAU,EAAE,KAAK,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAC/D,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,yBAAyB,CAAC;AAOpE,MAAM,WAAW,qBAAqB;IACrC,cAAc,CAAC,EAAE,aAAa,CAAC;IAC/B;;;;;OAKG;IACH,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,iBAAiB;IACjC,WAAW,EAAE,sBAAsB,CAAC;IAGpC,UAAU,CAAC,EAAE,UAAU,CAAC;IAGxB,oBAAoB,CAAC,EAAE,OAAO,CAAC,qBAAqB,CAAC,CAAC;CACtD;AAED,qBAAa,UAAU;;IACtB,QAAQ,CAAC,WAAW,EAAE,sBAAsB,CAAC;IAC7C,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAMhC,oBAAoB,EAAE,qBAAqB,CAAC;gBAEhC,OAAO,EAAE,iBAAiB;IAOhC,IAAI,CACT,OAAO,EAAE,cAAc,EACvB,OAAO,CAAC,EAAE,qBAAqB,GAC7B,OAAO,CAAC,sBAAsB,CAAC;IAC5B,IAAI,CACT,OAAO,EAAE,mBAAmB,EAC5B,OAAO,CAAC,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,GAAG,IAAI,CAAC;IA2CjC,OAAO,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,gCAAgC,GAAG,IAAI,CAAC;CAyIjF"}
|
|
@@ -33,7 +33,7 @@ export class ActionPeer {
|
|
|
33
33
|
}
|
|
34
34
|
const message_type = is_jsonrpc_request(message) ? 'request' : 'notification';
|
|
35
35
|
this.environment.log?.debug(`[peer] send ${message_type}:`, message.method, `via ${transport.transport_name}`);
|
|
36
|
-
const result = await transport.send(message);
|
|
36
|
+
const result = await transport.send(message, { signal: options?.signal });
|
|
37
37
|
if (result && 'error' in result) {
|
|
38
38
|
this.environment.log?.error(`[peer] send ${message_type} failed:`, message.method, result.error.message);
|
|
39
39
|
}
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* @module
|
|
10
10
|
*/
|
|
11
11
|
import type { JsonrpcRequestId } from '../http/jsonrpc.js';
|
|
12
|
+
import type { Uuid } from '../uuid.js';
|
|
12
13
|
import type { ActionSpecUnion } from './action_spec.js';
|
|
13
14
|
/**
|
|
14
15
|
* Minimum per-request context every server-side WS handler receives.
|
|
@@ -20,13 +21,27 @@ import type { ActionSpecUnion } from './action_spec.js';
|
|
|
20
21
|
export interface BaseHandlerContext {
|
|
21
22
|
/** JSON-RPC envelope request id — echoed back on the response. */
|
|
22
23
|
request_id: JsonrpcRequestId;
|
|
24
|
+
/**
|
|
25
|
+
* Stable per-socket connection id assigned by
|
|
26
|
+
* `BackendWebsocketTransport.add_connection` — same reference across every
|
|
27
|
+
* message on this socket, also passed to `on_socket_open` /
|
|
28
|
+
* `on_socket_close`. Consumers key per-connection domain state on this
|
|
29
|
+
* directly instead of trying to derive it from signals (which are
|
|
30
|
+
* per-message composites of `AbortSignal.any([socket, request])`).
|
|
31
|
+
*/
|
|
32
|
+
connection_id: Uuid;
|
|
23
33
|
/**
|
|
24
34
|
* Send a request-scoped JSON-RPC notification to the originating socket.
|
|
25
35
|
* Not a broadcast — the message only reaches the client whose request
|
|
26
36
|
* triggered this handler.
|
|
27
37
|
*/
|
|
28
38
|
notify: (method: string, params: unknown) => void;
|
|
29
|
-
/**
|
|
39
|
+
/**
|
|
40
|
+
* Fires on socket close OR on a client-initiated `cancel` notification
|
|
41
|
+
* matching this request's id. Streaming handlers poll for early
|
|
42
|
+
* termination; per-message composite (`AbortSignal.any([socket, request])`)
|
|
43
|
+
* — not stable across messages.
|
|
44
|
+
*/
|
|
30
45
|
signal: AbortSignal;
|
|
31
46
|
}
|
|
32
47
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"action_types.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,kBAAkB,CAAC;AAEtD;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IAClC,kEAAkE;IAClE,UAAU,EAAE,gBAAgB,CAAC;IAC7B;;;;OAIG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD
|
|
1
|
+
{"version":3,"file":"action_types.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/action_types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAC,gBAAgB,EAAC,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,YAAY,CAAC;AACrC,OAAO,KAAK,EAAC,eAAe,EAAC,MAAM,kBAAkB,CAAC;AAEtD;;;;;;GAMG;AACH,MAAM,WAAW,kBAAkB;IAClC,kEAAkE;IAClE,UAAU,EAAE,gBAAgB,CAAC;IAC7B;;;;;;;OAOG;IACH,aAAa,EAAE,IAAI,CAAC;IACpB;;;;OAIG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD;;;;;OAKG;IACH,MAAM,EAAE,WAAW,CAAC;CACpB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,eAAe,CAAC,IAAI,SAAS,kBAAkB,GAAG,kBAAkB,IAAI,CACnF,KAAK,EAAE,OAAO,EACd,GAAG,EAAE,IAAI,KACL,OAAO,CAAC;AAEb;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,MAAM,CAAC,IAAI,SAAS,kBAAkB,GAAG,kBAAkB;IAC3E,IAAI,EAAE,eAAe,CAAC;IACtB,8EAA8E;IAC9E,OAAO,CAAC,EAAE,eAAe,CAAC,IAAI,CAAC,CAAC;CAChC"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared cancel action — the second composable fuz_app primitive, validating
|
|
3
|
+
* the spec+handler tuple pattern on a notification-kind action.
|
|
4
|
+
*
|
|
5
|
+
* Semantics: the client sends `{jsonrpc, method: 'cancel', params:
|
|
6
|
+
* {request_id}}` to abort an in-flight request on the same socket.
|
|
7
|
+
* {@link register_action_ws} intercepts this notification and aborts the
|
|
8
|
+
* matching pending request's `ctx.signal`. Unknown ids are no-ops by design —
|
|
9
|
+
* races between response arrival and cancel delivery are safe without extra
|
|
10
|
+
* coordination.
|
|
11
|
+
*
|
|
12
|
+
* The handler field is an empty stub: cancel semantics are dispatcher-owned
|
|
13
|
+
* (the dispatcher has the `{request_id → AbortController}` map, not the
|
|
14
|
+
* handler). The handler exists for symmetry with other composable primitives
|
|
15
|
+
* like {@link heartbeat_action}; the dispatcher never calls it. Consumers
|
|
16
|
+
* spread {@link cancel_action} into their server's `actions` array so
|
|
17
|
+
* `spec_by_method` knows about it (enabling input validation on incoming
|
|
18
|
+
* cancels) and so `create_rpc_client` codegen produces `app.api.cancel()`
|
|
19
|
+
* when desired — though `FrontendWebsocketClient.request({signal})` sends
|
|
20
|
+
* the cancel on abort without needing the typed API.
|
|
21
|
+
*
|
|
22
|
+
* Wire format is snake_case `cancel` with `{request_id}`, not MCP's
|
|
23
|
+
* `$/cancelRequest` with `{requestId}` — fuz_app's WS transport isn't MCP,
|
|
24
|
+
* and adopting MCP's convention would leak protocol-specific framing into
|
|
25
|
+
* the base transport. When MCP elicitation (Phase 5) lands, a translation
|
|
26
|
+
* layer at the MCP adapter is the right seam.
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
import type { Action } from './action_types.js';
|
|
32
|
+
/** Method name on the wire — shared across every fuz_app consumer. */
|
|
33
|
+
export declare const CANCEL_METHOD = "cancel";
|
|
34
|
+
/**
|
|
35
|
+
* Params for a {@link CANCEL_METHOD} notification. `request_id` is the id of
|
|
36
|
+
* the pending request to abort. Must match the id of a request sent on the
|
|
37
|
+
* same socket; cancels from other sockets (or for unknown ids) are ignored.
|
|
38
|
+
*/
|
|
39
|
+
export declare const CancelNotificationParams: z.ZodObject<{
|
|
40
|
+
request_id: z.ZodUnion<readonly [z.ZodString, z.ZodNumber]>;
|
|
41
|
+
}, z.core.$strict>;
|
|
42
|
+
export type CancelNotificationParams = z.infer<typeof CancelNotificationParams>;
|
|
43
|
+
/**
|
|
44
|
+
* `ActionSpec` for the shared cancel. `auth: null` matches every other
|
|
45
|
+
* remote-notification spec — upgrade-time auth has already admitted the
|
|
46
|
+
* socket, so per-action auth on a fire-and-forget notification is moot. The
|
|
47
|
+
* per-connection `{request_id → AbortController}` map enforces socket-scoped
|
|
48
|
+
* ownership naturally: a different socket's cancel for the same id misses
|
|
49
|
+
* in its own map.
|
|
50
|
+
*/
|
|
51
|
+
export declare const cancel_action_spec: {
|
|
52
|
+
method: string;
|
|
53
|
+
initiator: "both" | "frontend" | "backend";
|
|
54
|
+
input: z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
55
|
+
description: string;
|
|
56
|
+
kind: "remote_notification";
|
|
57
|
+
auth: null;
|
|
58
|
+
side_effects: true;
|
|
59
|
+
output: z.ZodVoid;
|
|
60
|
+
async: true;
|
|
61
|
+
streams?: string | undefined;
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Placeholder handler — cancel semantics are owned by {@link register_action_ws},
|
|
65
|
+
* not invoked per-handler. Exported for symmetry with the {@link Action}
|
|
66
|
+
* tuple shape; the dispatcher short-circuits cancel notifications before any
|
|
67
|
+
* handler lookup happens.
|
|
68
|
+
*/
|
|
69
|
+
export declare const cancel_handler: () => void;
|
|
70
|
+
/**
|
|
71
|
+
* Composable tuple — spread into the server's `actions` array so the
|
|
72
|
+
* dispatcher registers the spec for input validation and so `create_rpc_client`
|
|
73
|
+
* codegen sees the method. The client doesn't need to call it directly;
|
|
74
|
+
* `FrontendWebsocketClient.request({signal})` sends the cancel notification
|
|
75
|
+
* automatically when the signal fires.
|
|
76
|
+
*/
|
|
77
|
+
export declare const cancel_action: Action;
|
|
78
|
+
//# sourceMappingURL=cancel.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cancel.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/cancel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAC,CAAC,EAAC,MAAM,KAAK,CAAC;AAItB,OAAO,KAAK,EAAC,MAAM,EAAC,MAAM,mBAAmB,CAAC;AAE9C,sEAAsE;AACtE,eAAO,MAAM,aAAa,WAAW,CAAC;AAEtC;;;;GAIG;AACH,eAAO,MAAM,wBAAwB;;kBAEnC,CAAC;AACH,MAAM,MAAM,wBAAwB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,wBAAwB,CAAC,CAAC;AAEhF;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB;;;;;;;;;;;CAW7B,CAAC;AAEH;;;;;GAKG;AACH,eAAO,MAAM,cAAc,QAAO,IAAU,CAAC;AAE7C;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,EAAE,MAG3B,CAAC"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared cancel action — the second composable fuz_app primitive, validating
|
|
3
|
+
* the spec+handler tuple pattern on a notification-kind action.
|
|
4
|
+
*
|
|
5
|
+
* Semantics: the client sends `{jsonrpc, method: 'cancel', params:
|
|
6
|
+
* {request_id}}` to abort an in-flight request on the same socket.
|
|
7
|
+
* {@link register_action_ws} intercepts this notification and aborts the
|
|
8
|
+
* matching pending request's `ctx.signal`. Unknown ids are no-ops by design —
|
|
9
|
+
* races between response arrival and cancel delivery are safe without extra
|
|
10
|
+
* coordination.
|
|
11
|
+
*
|
|
12
|
+
* The handler field is an empty stub: cancel semantics are dispatcher-owned
|
|
13
|
+
* (the dispatcher has the `{request_id → AbortController}` map, not the
|
|
14
|
+
* handler). The handler exists for symmetry with other composable primitives
|
|
15
|
+
* like {@link heartbeat_action}; the dispatcher never calls it. Consumers
|
|
16
|
+
* spread {@link cancel_action} into their server's `actions` array so
|
|
17
|
+
* `spec_by_method` knows about it (enabling input validation on incoming
|
|
18
|
+
* cancels) and so `create_rpc_client` codegen produces `app.api.cancel()`
|
|
19
|
+
* when desired — though `FrontendWebsocketClient.request({signal})` sends
|
|
20
|
+
* the cancel on abort without needing the typed API.
|
|
21
|
+
*
|
|
22
|
+
* Wire format is snake_case `cancel` with `{request_id}`, not MCP's
|
|
23
|
+
* `$/cancelRequest` with `{requestId}` — fuz_app's WS transport isn't MCP,
|
|
24
|
+
* and adopting MCP's convention would leak protocol-specific framing into
|
|
25
|
+
* the base transport. When MCP elicitation (Phase 5) lands, a translation
|
|
26
|
+
* layer at the MCP adapter is the right seam.
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
import { z } from 'zod';
|
|
31
|
+
import { JsonrpcRequestId } from '../http/jsonrpc.js';
|
|
32
|
+
import { RemoteNotificationActionSpec } from './action_spec.js';
|
|
33
|
+
/** Method name on the wire — shared across every fuz_app consumer. */
|
|
34
|
+
export const CANCEL_METHOD = 'cancel';
|
|
35
|
+
/**
|
|
36
|
+
* Params for a {@link CANCEL_METHOD} notification. `request_id` is the id of
|
|
37
|
+
* the pending request to abort. Must match the id of a request sent on the
|
|
38
|
+
* same socket; cancels from other sockets (or for unknown ids) are ignored.
|
|
39
|
+
*/
|
|
40
|
+
export const CancelNotificationParams = z.strictObject({
|
|
41
|
+
request_id: JsonrpcRequestId,
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* `ActionSpec` for the shared cancel. `auth: null` matches every other
|
|
45
|
+
* remote-notification spec — upgrade-time auth has already admitted the
|
|
46
|
+
* socket, so per-action auth on a fire-and-forget notification is moot. The
|
|
47
|
+
* per-connection `{request_id → AbortController}` map enforces socket-scoped
|
|
48
|
+
* ownership naturally: a different socket's cancel for the same id misses
|
|
49
|
+
* in its own map.
|
|
50
|
+
*/
|
|
51
|
+
export const cancel_action_spec = RemoteNotificationActionSpec.parse({
|
|
52
|
+
method: CANCEL_METHOD,
|
|
53
|
+
kind: 'remote_notification',
|
|
54
|
+
initiator: 'frontend',
|
|
55
|
+
auth: null,
|
|
56
|
+
side_effects: true,
|
|
57
|
+
input: CancelNotificationParams,
|
|
58
|
+
output: z.void(),
|
|
59
|
+
async: true,
|
|
60
|
+
description: 'Client-initiated cancellation of an in-flight request by id. Dispatcher-handled: aborts the ctx.signal of the matching pending request on the same socket. Unknown or completed ids no-op.',
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* Placeholder handler — cancel semantics are owned by {@link register_action_ws},
|
|
64
|
+
* not invoked per-handler. Exported for symmetry with the {@link Action}
|
|
65
|
+
* tuple shape; the dispatcher short-circuits cancel notifications before any
|
|
66
|
+
* handler lookup happens.
|
|
67
|
+
*/
|
|
68
|
+
export const cancel_handler = () => { }; // eslint-disable-line @typescript-eslint/no-empty-function
|
|
69
|
+
/**
|
|
70
|
+
* Composable tuple — spread into the server's `actions` array so the
|
|
71
|
+
* dispatcher registers the spec for input validation and so `create_rpc_client`
|
|
72
|
+
* codegen sees the method. The client doesn't need to call it directly;
|
|
73
|
+
* `FrontendWebsocketClient.request({signal})` sends the cancel notification
|
|
74
|
+
* automatically when the signal fires.
|
|
75
|
+
*/
|
|
76
|
+
export const cancel_action = {
|
|
77
|
+
spec: cancel_action_spec,
|
|
78
|
+
handler: cancel_handler,
|
|
79
|
+
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAC,OAAO,EAAE,IAAI,EAAC,MAAM,MAAM,CAAC;AACxC,OAAO,KAAK,EAAC,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEzD,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAgB1E,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,YAAY,CAAC;AAErC,OAAO,EAAC,KAAK,MAAM,EAAE,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAC,MAAM,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"register_action_ws.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/register_action_ws.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAC,OAAO,EAAE,IAAI,EAAC,MAAM,MAAM,CAAC;AACxC,OAAO,KAAK,EAAC,gBAAgB,EAAE,SAAS,EAAC,MAAM,SAAS,CAAC;AAEzD,OAAO,EAAS,KAAK,MAAM,IAAI,UAAU,EAAC,MAAM,yBAAyB,CAAC;AAgB1E,OAAO,KAAK,EAAC,IAAI,EAAC,MAAM,YAAY,CAAC;AAErC,OAAO,EAAC,KAAK,MAAM,EAAE,KAAK,kBAAkB,EAAE,KAAK,eAAe,EAAC,MAAM,mBAAmB,CAAC;AAG7F,OAAO,EAAC,yBAAyB,EAAE,KAAK,kBAAkB,EAAC,MAAM,4BAA4B,CAAC;AAE9F,YAAY,EAAC,MAAM,EAAE,kBAAkB,EAAE,eAAe,EAAC,CAAC;AAE1D,0EAA0E;AAC1E,eAAO,MAAM,gCAAgC,QAAS,CAAC;AAEvD;;;;;;;GAOG;AACH,MAAM,WAAW,iBAAiB;IACjC,qFAAqF;IACrF,EAAE,EAAE,SAAS,CAAC;IACd,4EAA4E;IAC5E,aAAa,EAAE,IAAI,CAAC;IACpB,oDAAoD;IACpD,QAAQ,EAAE,kBAAkB,CAAC;IAC7B;;;OAGG;IACH,MAAM,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,KAAK,IAAI,CAAC;IAClD,wFAAwF;IACxF,MAAM,EAAE,WAAW,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,kBAAkB;IAClC,+CAA+C;IAC/C,EAAE,EAAE,SAAS,CAAC;IACd,2CAA2C;IAC3C,aAAa,EAAE,IAAI,CAAC;IACpB,kGAAkG;IAClG,QAAQ,EAAE,kBAAkB,CAAC;CAC7B;AAED,MAAM,WAAW,sBAAsB;IACtC;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wCAAwC;AACxC,MAAM,WAAW,uBAAuB,CAAC,IAAI,SAAS,kBAAkB;IACvE,oCAAoC;IACpC,IAAI,EAAE,MAAM,CAAC;IACb,gCAAgC;IAChC,GAAG,EAAE,IAAI,CAAC;IACV,iEAAiE;IACjE,gBAAgB,EAAE,gBAAgB,CAAC;IACnC;;;;;;OAMG;IACH,OAAO,EAAE,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IACrC;;;;;OAKG;IACH,cAAc,EAAE,CAAC,IAAI,EAAE,kBAAkB,EAAE,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IAC/D;;;;OAIG;IACH,SAAS,CAAC,EAAE,yBAAyB,CAAC;IACtC;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,GAAG,sBAAsB,CAAC;IAC7C,+EAA+E;IAC/E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,qDAAqD;IACrD,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB;;;;;OAKG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,iBAAiB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE;;;;;OAKG;IACH,eAAe,CAAC,EAAE,CAAC,GAAG,EAAE,kBAAkB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACpE;AAED,sCAAsC;AACtC,MAAM,WAAW,sBAAsB;IACtC,yEAAyE;IACzE,SAAS,EAAE,yBAAyB,CAAC;CACrC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,eAAO,MAAM,kBAAkB,GAAI,IAAI,SAAS,kBAAkB,EACjE,SAAS,uBAAuB,CAAC,IAAI,CAAC,KACpC,sBA8WF,CAAC"}
|
|
@@ -28,10 +28,11 @@ import { get_request_context, has_role } from '../auth/request_context.js';
|
|
|
28
28
|
import { hash_session_token } from '../auth/session_queries.js';
|
|
29
29
|
import { ROLE_KEEPER } from '../auth/role_schema.js';
|
|
30
30
|
import { JSONRPC_VERSION } from '../http/jsonrpc.js';
|
|
31
|
-
import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
|
|
31
|
+
import { jsonrpc_error_messages, ThrownJsonrpcError } from '../http/jsonrpc_errors.js';
|
|
32
32
|
import { create_jsonrpc_error_response, create_jsonrpc_error_response_from_thrown, create_jsonrpc_notification, to_jsonrpc_message_id, to_jsonrpc_params, is_jsonrpc_request, } from '../http/jsonrpc_helpers.js';
|
|
33
33
|
import { CREDENTIAL_TYPE_KEY, AUTH_API_TOKEN_ID_KEY } from '../hono_context.js';
|
|
34
34
|
import {} from './action_types.js';
|
|
35
|
+
import { CANCEL_METHOD, CancelNotificationParams } from './cancel.js';
|
|
35
36
|
import { WS_CLOSE_SERVER_HEARTBEAT_TIMEOUT } from './transports.js';
|
|
36
37
|
import { BackendWebsocketTransport } from './transports_ws_backend.js';
|
|
37
38
|
/** Default inactivity window before the server closes a silent socket. */
|
|
@@ -88,12 +89,19 @@ export const register_action_ws = (options) => {
|
|
|
88
89
|
// `close_sockets_for_token` to tear down just this socket on
|
|
89
90
|
// `token_revoke` without affecting the account's other sockets.
|
|
90
91
|
const api_token_id = c.get(AUTH_API_TOKEN_ID_KEY);
|
|
91
|
-
// Per-socket abort controller — fires on socket close,
|
|
92
|
-
// every in-flight handler's
|
|
93
|
-
//
|
|
94
|
-
//
|
|
95
|
-
//
|
|
92
|
+
// Per-socket abort controller — fires on socket close, chained into
|
|
93
|
+
// every in-flight handler's per-request controller via
|
|
94
|
+
// `AbortSignal.any`. Keeping both signals lets the client
|
|
95
|
+
// cancel-one-request-by-id (via the `cancel` notification) without
|
|
96
|
+
// tearing down the whole socket.
|
|
96
97
|
const socket_abort_controller = new AbortController();
|
|
98
|
+
// Per-request controllers keyed by JSON-RPC request id — lets an
|
|
99
|
+
// incoming `cancel` notification abort just the matching handler.
|
|
100
|
+
// Populated on request dispatch, cleared in the handler's `finally`
|
|
101
|
+
// so a late-arriving cancel for a completed id (or a reused id)
|
|
102
|
+
// can't null-abort a freshly-arrived request. Idempotent: cancel
|
|
103
|
+
// for unknown ids no-ops.
|
|
104
|
+
const pending_controllers = new Map();
|
|
97
105
|
// Identity is assembled at upgrade time so `on_socket_close` can
|
|
98
106
|
// still read it after the audit guard tears the transport record
|
|
99
107
|
// down; `BackendWebsocketTransport.#revoke_connection` clears the
|
|
@@ -126,10 +134,10 @@ export const register_action_ws = (options) => {
|
|
|
126
134
|
}
|
|
127
135
|
};
|
|
128
136
|
return {
|
|
129
|
-
onOpen: async (
|
|
137
|
+
onOpen: async (_event, ws) => {
|
|
130
138
|
const connection_id = transport.add_connection(ws, token_hash, account_id, api_token_id);
|
|
131
139
|
captured_connection_id = connection_id;
|
|
132
|
-
log.debug('ws opened', connection_id
|
|
140
|
+
log.debug('ws opened', connection_id);
|
|
133
141
|
if (heartbeat_enabled) {
|
|
134
142
|
last_receive_time = Date.now();
|
|
135
143
|
heartbeat_timer = setInterval(() => {
|
|
@@ -185,9 +193,26 @@ export const register_action_ws = (options) => {
|
|
|
185
193
|
ws.send(JSON.stringify(create_jsonrpc_error_response(null, jsonrpc_error_messages.invalid_request('batch JSON-RPC requests are not supported on WebSocket'))));
|
|
186
194
|
return;
|
|
187
195
|
}
|
|
188
|
-
//
|
|
196
|
+
// Notifications (method + no id) — `cancel` is intercepted
|
|
197
|
+
// for request-scoped cancellation; other notifications are
|
|
198
|
+
// silenced per JSON-RPC spec (consumer notification handlers
|
|
199
|
+
// are not a feature yet).
|
|
189
200
|
if (!is_jsonrpc_request(json)) {
|
|
190
201
|
if (typeof json === 'object' && json !== null && 'method' in json && !('id' in json)) {
|
|
202
|
+
if (json.method === CANCEL_METHOD) {
|
|
203
|
+
const parsed = CancelNotificationParams.safeParse(json.params);
|
|
204
|
+
if (!parsed.success) {
|
|
205
|
+
log.debug('cancel: invalid params, ignoring', parsed.error.issues);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const controller = pending_controllers.get(parsed.data.request_id);
|
|
209
|
+
if (controller) {
|
|
210
|
+
controller.abort();
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
log.debug('cancel: no pending request for id', parsed.data.request_id);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
191
216
|
return;
|
|
192
217
|
}
|
|
193
218
|
ws.send(JSON.stringify(create_jsonrpc_error_response(to_jsonrpc_message_id(json), jsonrpc_error_messages.invalid_request())));
|
|
@@ -233,22 +258,27 @@ export const register_action_ws = (options) => {
|
|
|
233
258
|
await wait(artificial_delay);
|
|
234
259
|
}
|
|
235
260
|
// Socket-scoped notification — routes to originator only, not
|
|
236
|
-
// broadcast.
|
|
261
|
+
// broadcast. Same helper used in `on_socket_open` so both
|
|
262
|
+
// paths share one code path for send-and-log-on-failure.
|
|
263
|
+
// Future work: other audiences — account-scoped,
|
|
237
264
|
// ACL-filtered, broadcast — likely via a transport-level
|
|
238
265
|
// policy hook.
|
|
239
|
-
const notify = (
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
266
|
+
const notify = notify_socket(ws);
|
|
267
|
+
// Per-request controller — fires on explicit `cancel` or on
|
|
268
|
+
// socket close (via the socket_abort_controller chain below).
|
|
269
|
+
// Registered before dispatch so a cancel arriving mid-handler
|
|
270
|
+
// finds it; cleared in `finally` so late cancels for a
|
|
271
|
+
// completed id (or a future request that reuses the id) can't
|
|
272
|
+
// null-abort the wrong handler.
|
|
273
|
+
const request_controller = new AbortController();
|
|
274
|
+
pending_controllers.set(id, request_controller);
|
|
248
275
|
const base = {
|
|
249
276
|
request_id: id,
|
|
277
|
+
// Populated in `onOpen` before any message can dispatch —
|
|
278
|
+
// non-null assertion is safe.
|
|
279
|
+
connection_id: captured_connection_id,
|
|
250
280
|
notify,
|
|
251
|
-
signal: socket_abort_controller.signal,
|
|
281
|
+
signal: AbortSignal.any([socket_abort_controller.signal, request_controller.signal]),
|
|
252
282
|
};
|
|
253
283
|
const ctx = extend_context(base, c);
|
|
254
284
|
try {
|
|
@@ -264,9 +294,19 @@ export const register_action_ws = (options) => {
|
|
|
264
294
|
ws.send(JSON.stringify({ jsonrpc: JSONRPC_VERSION, id, result: output }));
|
|
265
295
|
}
|
|
266
296
|
catch (error) {
|
|
267
|
-
|
|
297
|
+
if (error instanceof ThrownJsonrpcError) {
|
|
298
|
+
// Expected handler outcome (conflict, not_found, invalid_params, ...).
|
|
299
|
+
// Log at debug without the stack — the throw site is part of protocol, not a bug.
|
|
300
|
+
log.debug('handler error:', method, `${error.code} ${error.message}`);
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
log.error('handler error:', method, error);
|
|
304
|
+
}
|
|
268
305
|
ws.send(JSON.stringify(create_jsonrpc_error_response_from_thrown(id, error)));
|
|
269
306
|
}
|
|
307
|
+
finally {
|
|
308
|
+
pending_controllers.delete(id);
|
|
309
|
+
}
|
|
270
310
|
},
|
|
271
311
|
onClose: async (event, ws) => {
|
|
272
312
|
stop_heartbeat_timer();
|
|
@@ -284,7 +324,7 @@ export const register_action_ws = (options) => {
|
|
|
284
324
|
}
|
|
285
325
|
}
|
|
286
326
|
transport.remove_connection(ws);
|
|
287
|
-
log.debug('ws closed', event);
|
|
327
|
+
log.debug('ws closed', captured_connection_id, { code: event.code, reason: event.reason });
|
|
288
328
|
},
|
|
289
329
|
};
|
|
290
330
|
}));
|
|
@@ -57,4 +57,14 @@ export interface CreateRpcClientOptions {
|
|
|
57
57
|
* @returns a Proxy that responds to any method name found in the environment's specs
|
|
58
58
|
*/
|
|
59
59
|
export declare const create_rpc_client: (options: CreateRpcClientOptions) => Record<string, (...args: Array<any>) => any>;
|
|
60
|
+
/**
|
|
61
|
+
* Per-call options accepted by every typed Proxy method. `signal` lets the
|
|
62
|
+
* caller cancel an in-flight request (sends the shared `cancel` notification
|
|
63
|
+
* on the WS path, aborts `fetch` on HTTP). `transport_name` overrides the
|
|
64
|
+
* per-method `transport_for_method` selector for this call.
|
|
65
|
+
*/
|
|
66
|
+
export interface RpcClientCallOptions {
|
|
67
|
+
signal?: AbortSignal;
|
|
68
|
+
transport_name?: TransportName;
|
|
69
|
+
}
|
|
60
70
|
//# sourceMappingURL=rpc_client.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rpc_client.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/rpc_client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,yBAAyB,CAAC;AAOpE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"rpc_client.d.ts","sourceRoot":"../src/lib/","sources":["../../src/lib/actions/rpc_client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,OAAO,KAAK,EAAC,sBAAsB,EAAC,MAAM,yBAAyB,CAAC;AAOpE,OAAO,KAAK,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAC,oBAAoB,EAAC,MAAM,wBAAwB,CAAC;AACjE,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,iBAAiB,CAAC;AAGnD;;;;;;;GAOG;AACH,MAAM,MAAM,kBAAkB,GAAG,CAAC,MAAM,EAAE,MAAM,KAAK,aAAa,GAAG,SAAS,CAAC;AAM/E,8EAA8E;AAC9E,MAAM,WAAW,sBAAsB;IACtC,aAAa,EAAE,CAAC,IAAI,EAAE;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,oBAAoB,CAAA;KAAC,KAC5E;QACA,sBAAsB,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,IAAI,CAAC;KAC5C,GACD,SAAS,CAAC;CACb;AAED,uCAAuC;AACvC,MAAM,WAAW,sBAAsB;IACtC,IAAI,EAAE,UAAU,CAAC;IACjB,WAAW,EAAE,sBAAsB,CAAC;IACpC,kEAAkE;IAClE,OAAO,CAAC,EAAE,sBAAsB,CAAC;IACjC;;;;;OAKG;IACH,oBAAoB,CAAC,EAAE,kBAAkB,CAAC;CAC1C;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,iBAAiB,GAC7B,SAAS,sBAAsB,KAC7B,MAAM,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,GAAG,CAgB7C,CAAC;AA2DF;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACpC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,cAAc,CAAC,EAAE,aAAa,CAAC;CAC/B"}
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { create_action_event } from './action_event.js';
|
|
13
13
|
import { is_send_request, is_notification_send, extract_action_result, } from './action_event_helpers.js';
|
|
14
|
+
import { jsonrpc_error_messages } from '../http/jsonrpc_errors.js';
|
|
14
15
|
/**
|
|
15
16
|
* Creates a Proxy-based API from action specs.
|
|
16
17
|
*
|
|
@@ -78,9 +79,19 @@ const create_sync_local_call_method = (environment, spec, actions) => {
|
|
|
78
79
|
/**
|
|
79
80
|
* Creates an asynchronous local call method.
|
|
80
81
|
* Returns Result for type-safe error handling.
|
|
82
|
+
*
|
|
83
|
+
* Local calls don't traverse a transport, so `transport_name` is ignored and
|
|
84
|
+
* `signal` can only short-circuit before the synchronous handler runs (no
|
|
85
|
+
* cooperative interrupt mid-handler).
|
|
81
86
|
*/
|
|
82
87
|
const create_async_local_call_method = (environment, spec, actions) => {
|
|
83
|
-
return async (input) => {
|
|
88
|
+
return async (input, options) => {
|
|
89
|
+
if (options?.signal?.aborted) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
error: jsonrpc_error_messages.internal_error(`${spec.method} aborted before execution`),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
84
95
|
const event = create_action_event(environment, spec, input);
|
|
85
96
|
const action = actions?.add_from_json({
|
|
86
97
|
method: spec.method,
|
|
@@ -95,7 +106,7 @@ const create_async_local_call_method = (environment, spec, actions) => {
|
|
|
95
106
|
* Creates a request/response method that communicates over the network.
|
|
96
107
|
*/
|
|
97
108
|
const create_request_response_method = (peer, environment, spec, actions, transport_for_method) => {
|
|
98
|
-
return async (input) => {
|
|
109
|
+
return async (input, options) => {
|
|
99
110
|
const event = create_action_event(environment, spec, input);
|
|
100
111
|
const action = actions?.add_from_json({
|
|
101
112
|
method: spec.method,
|
|
@@ -113,8 +124,10 @@ const create_request_response_method = (peer, environment, spec, actions, transp
|
|
|
113
124
|
if (event.data.step !== 'handled') {
|
|
114
125
|
return extract_action_result(event);
|
|
115
126
|
}
|
|
116
|
-
const
|
|
117
|
-
|
|
127
|
+
const response = await peer.send(event.data.request, {
|
|
128
|
+
transport_name: options?.transport_name ?? transport_for_method?.(spec.method),
|
|
129
|
+
signal: options?.signal,
|
|
130
|
+
});
|
|
118
131
|
event.transition('receive_response');
|
|
119
132
|
// TODO @api shouldn't this happen in the peer like the other method calls?
|
|
120
133
|
event.set_response(response);
|
|
@@ -128,7 +141,7 @@ const create_request_response_method = (peer, environment, spec, actions, transp
|
|
|
128
141
|
* Returns Result<{value: void}> for consistency.
|
|
129
142
|
*/
|
|
130
143
|
const create_remote_notification_method = (peer, environment, spec, actions, transport_for_method) => {
|
|
131
|
-
return async (input) => {
|
|
144
|
+
return async (input, options) => {
|
|
132
145
|
const event = create_action_event(environment, spec, input);
|
|
133
146
|
const action = actions?.add_from_json({
|
|
134
147
|
method: spec.method,
|
|
@@ -139,8 +152,10 @@ const create_remote_notification_method = (peer, environment, spec, actions, tra
|
|
|
139
152
|
if (!is_notification_send(event.data))
|
|
140
153
|
throw Error(); // TODO @many maybe make this an assertion helper?
|
|
141
154
|
if (event.data.step === 'handled') {
|
|
142
|
-
const
|
|
143
|
-
|
|
155
|
+
const send_result = await peer.send(event.data.notification, {
|
|
156
|
+
transport_name: options?.transport_name ?? transport_for_method?.(spec.method),
|
|
157
|
+
signal: options?.signal,
|
|
158
|
+
});
|
|
144
159
|
// Check if notification failed to send
|
|
145
160
|
if (send_result !== null) {
|
|
146
161
|
environment.log?.error('notification send failed:', send_result.error);
|