@procedures/client 0.2.2 → 0.3.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/CHANGELOG.md +71 -0
- package/README.md +61 -38
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/transport.d.ts.map +1 -1
- package/dist/adapters/http/client/transport.js +35 -3
- package/dist/adapters/http/client/transport.js.map +1 -1
- package/dist/adapters/http/client/types.d.ts +3 -2
- package/dist/adapters/http/client/types.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.d.ts +2 -2
- package/dist/adapters/http/server/transport.d.ts.map +1 -1
- package/dist/adapters/http/server/transport.js +53 -20
- package/dist/adapters/http/server/transport.js.map +1 -1
- package/dist/adapters/http/shared/headers.d.ts +1 -1
- package/dist/adapters/http/shared/headers.d.ts.map +1 -1
- package/dist/adapters/http/shared/headers.js +7 -6
- package/dist/adapters/http/shared/headers.js.map +1 -1
- package/dist/adapters/local/client/transport.d.ts +3 -0
- package/dist/adapters/local/client/transport.d.ts.map +1 -1
- package/dist/adapters/local/client/transport.js +63 -4
- package/dist/adapters/local/client/transport.js.map +1 -1
- package/dist/adapters/local/client/types.d.ts +15 -0
- package/dist/adapters/local/client/types.d.ts.map +1 -1
- package/dist/adapters/local/client/types.js.map +1 -1
- package/dist/adapters/mock/client/transport.d.ts +3 -2
- package/dist/adapters/mock/client/transport.d.ts.map +1 -1
- package/dist/adapters/mock/client/transport.js +8 -5
- package/dist/adapters/mock/client/transport.js.map +1 -1
- package/dist/adapters/mock/client/types.d.ts +7 -6
- package/dist/adapters/mock/client/types.d.ts.map +1 -1
- package/dist/adapters/port/transport.d.ts +62 -0
- package/dist/adapters/port/transport.d.ts.map +1 -0
- package/dist/adapters/port/transport.js +94 -0
- package/dist/adapters/port/transport.js.map +1 -0
- package/dist/adapters/websocket/client/index.d.ts +1 -1
- package/dist/adapters/websocket/client/index.d.ts.map +1 -1
- package/dist/adapters/websocket/client/index.js.map +1 -1
- package/dist/adapters/websocket/client/transport.d.ts +47 -7
- package/dist/adapters/websocket/client/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/client/transport.js +213 -67
- package/dist/adapters/websocket/client/transport.js.map +1 -1
- package/dist/adapters/websocket/client/types.d.ts +70 -21
- package/dist/adapters/websocket/client/types.d.ts.map +1 -1
- package/dist/adapters/websocket/client/types.js.map +1 -1
- package/dist/adapters/websocket/server/transport.d.ts +65 -24
- package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
- package/dist/adapters/websocket/server/transport.js +192 -263
- package/dist/adapters/websocket/server/transport.js.map +1 -1
- package/dist/adapters/websocket/server/types.d.ts +20 -1
- package/dist/adapters/websocket/server/types.d.ts.map +1 -1
- package/dist/adapters/websocket/shared/rpc.d.ts +129 -0
- package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -0
- package/dist/adapters/websocket/shared/rpc.js +425 -0
- package/dist/adapters/websocket/shared/rpc.js.map +1 -0
- package/dist/browser.d.ts +29 -7
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +26 -3
- package/dist/browser.js.map +1 -1
- package/dist/client/call-types.d.ts +10 -10
- package/dist/client/call-types.d.ts.map +1 -1
- package/dist/client/call-types.js +1 -6
- package/dist/client/call-types.js.map +1 -1
- package/dist/client/client.d.ts +6 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +23 -9
- package/dist/client/client.js.map +1 -1
- package/dist/client/deadline.d.ts +38 -0
- package/dist/client/deadline.d.ts.map +1 -0
- package/dist/client/deadline.js +60 -0
- package/dist/client/deadline.js.map +1 -0
- package/dist/client/errors/factory.d.ts.map +1 -1
- package/dist/client/errors/factory.js +11 -7
- package/dist/client/errors/factory.js.map +1 -1
- package/dist/client/index.d.ts +9 -2
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +8 -2
- package/dist/client/index.js.map +1 -1
- package/dist/client/middleware/cache.d.ts +19 -5
- package/dist/client/middleware/cache.d.ts.map +1 -1
- package/dist/client/middleware/cache.js +106 -18
- package/dist/client/middleware/cache.js.map +1 -1
- package/dist/client/middleware/circuit-breaker.d.ts +28 -10
- package/dist/client/middleware/circuit-breaker.d.ts.map +1 -1
- package/dist/client/middleware/circuit-breaker.js +35 -24
- package/dist/client/middleware/circuit-breaker.js.map +1 -1
- package/dist/client/middleware/contexts.d.ts +16 -10
- package/dist/client/middleware/contexts.d.ts.map +1 -1
- package/dist/client/middleware/contexts.js.map +1 -1
- package/dist/client/middleware/items.d.ts +31 -2
- package/dist/client/middleware/items.d.ts.map +1 -1
- package/dist/client/middleware/items.js +86 -6
- package/dist/client/middleware/items.js.map +1 -1
- package/dist/client/middleware/lru.d.ts +2 -0
- package/dist/client/middleware/lru.d.ts.map +1 -1
- package/dist/client/middleware/lru.js +11 -0
- package/dist/client/middleware/lru.js.map +1 -1
- package/dist/client/middleware/rate-limit.d.ts +7 -6
- package/dist/client/middleware/rate-limit.d.ts.map +1 -1
- package/dist/client/middleware/rate-limit.js +9 -6
- package/dist/client/middleware/rate-limit.js.map +1 -1
- package/dist/client/middleware/resilience.d.ts +61 -0
- package/dist/client/middleware/resilience.d.ts.map +1 -0
- package/dist/client/middleware/resilience.js +59 -0
- package/dist/client/middleware/resilience.js.map +1 -0
- package/dist/client/middleware/retry.d.ts +45 -11
- package/dist/client/middleware/retry.d.ts.map +1 -1
- package/dist/client/middleware/retry.js +102 -22
- package/dist/client/middleware/retry.js.map +1 -1
- package/dist/client/middleware/timeout.d.ts +15 -12
- package/dist/client/middleware/timeout.d.ts.map +1 -1
- package/dist/client/middleware/timeout.js +46 -37
- package/dist/client/middleware/timeout.js.map +1 -1
- package/dist/client/middleware/tracing.d.ts.map +1 -1
- package/dist/client/middleware/tracing.js +11 -3
- package/dist/client/middleware/tracing.js.map +1 -1
- package/dist/client/types.d.ts +9 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/types.js.map +1 -1
- package/dist/clock.d.ts +19 -0
- package/dist/clock.d.ts.map +1 -0
- package/dist/clock.js +18 -0
- package/dist/clock.js.map +1 -0
- package/dist/duration.d.ts +37 -0
- package/dist/duration.d.ts.map +1 -0
- package/dist/duration.js +44 -0
- package/dist/duration.js.map +1 -0
- package/dist/machines.d.ts +90 -0
- package/dist/machines.d.ts.map +1 -0
- package/dist/machines.js +127 -0
- package/dist/machines.js.map +1 -0
- package/dist/procedures/capability.d.ts +3 -6
- package/dist/procedures/capability.d.ts.map +1 -1
- package/dist/procedures/capability.js +11 -9
- package/dist/procedures/capability.js.map +1 -1
- package/dist/procedures/collection/index.d.ts +3 -3
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js +1 -1
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +49 -1
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/collection/schemas.d.ts +33 -0
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +68 -0
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +37 -0
- package/dist/procedures/collection/storage.d.ts.map +1 -1
- package/dist/procedures/core/array.d.ts.map +1 -1
- package/dist/procedures/core/array.js +26 -15
- package/dist/procedures/core/array.js.map +1 -1
- package/dist/procedures/core/index.d.ts.map +1 -1
- package/dist/procedures/core/index.js +8 -0
- package/dist/procedures/core/index.js.map +1 -1
- package/dist/procedures/core/meta.d.ts.map +1 -1
- package/dist/procedures/core/meta.js +2 -1
- package/dist/procedures/core/meta.js.map +1 -1
- package/dist/procedures/core/object.d.ts.map +1 -1
- package/dist/procedures/core/object.js +42 -13
- package/dist/procedures/core/object.js.map +1 -1
- package/dist/procedures/core/operands.d.ts.map +1 -1
- package/dist/procedures/core/operands.js +6 -1
- package/dist/procedures/core/operands.js.map +1 -1
- package/dist/procedures/core/string.d.ts.map +1 -1
- package/dist/procedures/core/string.js +4 -2
- package/dist/procedures/core/string.js.map +1 -1
- package/dist/procedures/define-procedure.d.ts +19 -7
- package/dist/procedures/define-procedure.d.ts.map +1 -1
- package/dist/procedures/define-procedure.js +66 -21
- package/dist/procedures/define-procedure.js.map +1 -1
- package/dist/procedures/effects.d.ts +16 -0
- package/dist/procedures/effects.d.ts.map +1 -1
- package/dist/procedures/effects.js +33 -0
- package/dist/procedures/effects.js.map +1 -1
- package/dist/procedures/idempotency.d.ts +73 -0
- package/dist/procedures/idempotency.d.ts.map +1 -0
- package/dist/procedures/idempotency.js +131 -0
- package/dist/procedures/idempotency.js.map +1 -0
- package/dist/procedures/index.d.ts +7 -3
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +6 -2
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/interceptors.d.ts +17 -4
- package/dist/procedures/interceptors.d.ts.map +1 -1
- package/dist/procedures/interceptors.js +18 -7
- package/dist/procedures/interceptors.js.map +1 -1
- package/dist/procedures/invoke.d.ts +15 -3
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +8 -3
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/manifest.d.ts +83 -0
- package/dist/procedures/manifest.d.ts.map +1 -0
- package/dist/procedures/manifest.js +90 -0
- package/dist/procedures/manifest.js.map +1 -0
- package/dist/procedures/runtime-programs.d.ts +19 -0
- package/dist/procedures/runtime-programs.d.ts.map +1 -0
- package/dist/procedures/runtime-programs.js +24 -0
- package/dist/procedures/runtime-programs.js.map +1 -0
- package/dist/procedures/types.d.ts +20 -1
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/protocol.d.ts +40 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +58 -0
- package/dist/protocol.js.map +1 -0
- package/dist/server/host-policy.d.ts +4 -3
- package/dist/server/host-policy.d.ts.map +1 -1
- package/dist/server/host-policy.js +6 -3
- package/dist/server/host-policy.js.map +1 -1
- package/dist/server/host-procedures.d.ts +31 -0
- package/dist/server/host-procedures.d.ts.map +1 -0
- package/dist/server/host-procedures.js +75 -0
- package/dist/server/host-procedures.js.map +1 -0
- package/dist/server/index.d.ts +3 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/procedure-server.d.ts +110 -4
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +269 -17
- package/dist/server/procedure-server.js.map +1 -1
- package/dist/server/request-auth.d.ts +19 -0
- package/dist/server/request-auth.d.ts.map +1 -0
- package/dist/server/request-auth.js +32 -0
- package/dist/server/request-auth.js.map +1 -0
- package/dist/server/types.d.ts +32 -0
- package/dist/server/types.d.ts.map +1 -1
- package/dist/server/types.js.map +1 -1
- package/dist/timers.d.ts +60 -0
- package/dist/timers.d.ts.map +1 -0
- package/dist/timers.js +33 -0
- package/dist/timers.js.map +1 -0
- package/dist/tracing/traceparent.d.ts +26 -0
- package/dist/tracing/traceparent.d.ts.map +1 -0
- package/dist/tracing/traceparent.js +53 -0
- package/dist/tracing/traceparent.js.map +1 -0
- package/package.json +7 -1
- package/src/adapters/http/client/transport.ts +31 -3
- package/src/adapters/http/client/types.ts +3 -2
- package/src/adapters/http/server/transport.ts +60 -24
- package/src/adapters/http/shared/headers.ts +7 -6
- package/src/adapters/local/client/transport.ts +70 -5
- package/src/adapters/local/client/types.ts +18 -0
- package/src/adapters/mock/client/transport.ts +9 -6
- package/src/adapters/mock/client/types.ts +7 -6
- package/src/adapters/port/transport.ts +124 -0
- package/src/adapters/websocket/client/index.ts +1 -0
- package/src/adapters/websocket/client/transport.ts +232 -73
- package/src/adapters/websocket/client/types.ts +80 -21
- package/src/adapters/websocket/server/transport.ts +199 -301
- package/src/adapters/websocket/server/types.ts +20 -1
- package/src/adapters/websocket/shared/rpc.ts +453 -0
- package/src/browser.ts +51 -3
- package/src/client/call-types.ts +10 -14
- package/src/client/client.ts +28 -9
- package/src/client/deadline.ts +64 -0
- package/src/client/errors/factory.ts +12 -8
- package/src/client/index.ts +21 -3
- package/src/client/middleware/cache.ts +120 -23
- package/src/client/middleware/circuit-breaker.ts +65 -35
- package/src/client/middleware/contexts.ts +17 -10
- package/src/client/middleware/items.ts +101 -6
- package/src/client/middleware/lru.ts +12 -0
- package/src/client/middleware/rate-limit.ts +9 -7
- package/src/client/middleware/resilience.ts +81 -0
- package/src/client/middleware/retry.ts +146 -34
- package/src/client/middleware/timeout.ts +55 -46
- package/src/client/middleware/tracing.ts +13 -4
- package/src/client/types.ts +9 -1
- package/src/clock.ts +21 -0
- package/src/duration.ts +56 -0
- package/src/machines.ts +186 -0
- package/src/procedures/capability.ts +11 -9
- package/src/procedures/collection/index.ts +9 -1
- package/src/procedures/collection/procedures.ts +57 -0
- package/src/procedures/collection/schemas.ts +110 -0
- package/src/procedures/collection/storage.ts +38 -0
- package/src/procedures/core/array.ts +27 -16
- package/src/procedures/core/index.ts +8 -0
- package/src/procedures/core/meta.ts +2 -1
- package/src/procedures/core/object.ts +48 -14
- package/src/procedures/core/operands.ts +7 -0
- package/src/procedures/core/string.ts +4 -2
- package/src/procedures/define-procedure.ts +82 -24
- package/src/procedures/effects.ts +35 -0
- package/src/procedures/idempotency.ts +152 -0
- package/src/procedures/index.ts +18 -2
- package/src/procedures/interceptors.ts +28 -11
- package/src/procedures/invoke.ts +19 -5
- package/src/procedures/manifest.ts +158 -0
- package/src/procedures/runtime-programs.ts +27 -0
- package/src/procedures/types.ts +20 -1
- package/src/protocol.ts +97 -0
- package/src/server/host-policy.ts +8 -5
- package/src/server/host-procedures.ts +102 -0
- package/src/server/index.ts +5 -2
- package/src/server/procedure-server.ts +324 -10
- package/src/server/request-auth.ts +35 -0
- package/src/server/types.ts +37 -0
- package/src/timers.ts +82 -0
- package/src/tracing/traceparent.ts +61 -0
package/src/machines.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The state machines of the core as data (roadmap R35): the connection of the WebSocket client
|
|
3
|
+
* and the circuit breaker. The documents have the format `mark1russell7.machine/1` of
|
|
4
|
+
* `@mark1russell7/algebra`. They are the specification, and the code implements them. Their knobs
|
|
5
|
+
* are the default times of the core (`TIMER_DEFAULTS`), and their timers say which knob gives each
|
|
6
|
+
* wait.
|
|
7
|
+
*
|
|
8
|
+
* The code reports each change of its state as a `MachineTransition` (the option `onTransition`).
|
|
9
|
+
* `replayTransitions()` replays the reports of a run against a document, so a test finds each
|
|
10
|
+
* change that the document does not allow. It uses `replay` of `@mark1russell7/algebra/replay`
|
|
11
|
+
* (roadmap R37). That entry imports nothing else, and it has 418 bytes of gzip.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { replay } from "@mark1russell7/algebra/replay";
|
|
15
|
+
import { TIMER_DEFAULTS } from "./timers.js";
|
|
16
|
+
|
|
17
|
+
/** A change of state that the code reports. It has the fields of a lifecycle event of `@mark1russell7/service`. */
|
|
18
|
+
export interface MachineTransition {
|
|
19
|
+
/** The id of the document. */
|
|
20
|
+
readonly machine: string;
|
|
21
|
+
/** The instance: one connection, one breaker. */
|
|
22
|
+
readonly instance: string;
|
|
23
|
+
readonly from: string;
|
|
24
|
+
readonly on: string;
|
|
25
|
+
readonly to: string;
|
|
26
|
+
/** The time of the clock of the instance, in milliseconds. */
|
|
27
|
+
readonly at: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** A time of a timer: one knob, in a unit. */
|
|
31
|
+
export interface MachineDuration {
|
|
32
|
+
readonly knob: string;
|
|
33
|
+
readonly unit: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A timer of a document: the events that start and stop it, and the event that it gives. */
|
|
37
|
+
export interface MachineTimer {
|
|
38
|
+
readonly duration: MachineDuration;
|
|
39
|
+
readonly arms: readonly string[];
|
|
40
|
+
readonly cancels?: readonly string[] | undefined;
|
|
41
|
+
readonly fires: string;
|
|
42
|
+
/** A timer that starts again after it fires, with a longer wait each time. */
|
|
43
|
+
readonly backoff?: { readonly resets: readonly string[] } | undefined;
|
|
44
|
+
readonly note?: string | undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A transition of a document. */
|
|
48
|
+
export interface MachineTransitionDef {
|
|
49
|
+
readonly id: string;
|
|
50
|
+
readonly from: string | readonly string[];
|
|
51
|
+
readonly on: string | readonly string[];
|
|
52
|
+
readonly to: string;
|
|
53
|
+
readonly timer?: string | undefined;
|
|
54
|
+
readonly note?: string | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** A machine document: the subset of `mark1russell7.machine/1` that the core uses. */
|
|
58
|
+
export interface MachineDocument {
|
|
59
|
+
readonly format: "mark1russell7.machine/1";
|
|
60
|
+
readonly id: string;
|
|
61
|
+
readonly title: string;
|
|
62
|
+
readonly layer: "self";
|
|
63
|
+
readonly version: string;
|
|
64
|
+
readonly sources: ReadonlyArray<{ readonly ref: string; readonly rung: "implemented" }>;
|
|
65
|
+
readonly states: Readonly<Record<string, { readonly initial?: boolean; readonly final?: boolean; readonly note?: string }>>;
|
|
66
|
+
readonly events: Readonly<Record<string, { readonly kind: "rx" | "tx" | "app" | "timer" | "local"; readonly note?: string }>>;
|
|
67
|
+
readonly knobs: Readonly<Record<string, { readonly key: string; readonly scope: string; readonly unit: string | null; readonly default: number | null }>>;
|
|
68
|
+
readonly timers: Readonly<Record<string, MachineTimer>>;
|
|
69
|
+
readonly transitions: readonly MachineTransitionDef[];
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const SOURCE = [{ ref: "packages/client/src (the core implements this document)", rung: "implemented" as const }];
|
|
73
|
+
const ms = (knob: string): MachineDuration => ({ knob, unit: "ms" });
|
|
74
|
+
const client = TIMER_DEFAULTS.webSocketClient;
|
|
75
|
+
|
|
76
|
+
/** The connection of the WebSocket client (`WebSocketTransport`). */
|
|
77
|
+
export const WEBSOCKET_CONNECTION_MACHINE: MachineDocument = {
|
|
78
|
+
format: "mark1russell7.machine/1",
|
|
79
|
+
id: "procedures.machine.websocket-connection",
|
|
80
|
+
title: "The connection of the WebSocket client",
|
|
81
|
+
layer: "self",
|
|
82
|
+
version: "0.2.3",
|
|
83
|
+
sources: SOURCE,
|
|
84
|
+
states: {
|
|
85
|
+
DISCONNECTED: { initial: true, note: "no socket" },
|
|
86
|
+
CONNECTING: { note: "the handshake, with its time limit" },
|
|
87
|
+
CONNECTED: { note: "the calls and the heartbeat" },
|
|
88
|
+
RECONNECTING: { note: "the backoff before the next try" },
|
|
89
|
+
DISCONNECTING: { note: "close() ends the socket and the calls" },
|
|
90
|
+
},
|
|
91
|
+
events: {
|
|
92
|
+
connect: { kind: "app", note: "the transport starts" },
|
|
93
|
+
"reconnect-due": { kind: "timer" },
|
|
94
|
+
open: { kind: "rx", note: "the handshake ends" },
|
|
95
|
+
drop: { kind: "local", note: "the socket closes, or an error ends it" },
|
|
96
|
+
"handshake-timeout": { kind: "timer" },
|
|
97
|
+
"ping-due": { kind: "timer" },
|
|
98
|
+
pong: { kind: "rx" },
|
|
99
|
+
"pong-timeout": { kind: "timer", note: "a drop after the last allowed miss" },
|
|
100
|
+
"give-up": { kind: "local", note: "a drop without reconnect, or without tries left" },
|
|
101
|
+
close: { kind: "app", note: "close() of the application" },
|
|
102
|
+
closed: { kind: "local" },
|
|
103
|
+
},
|
|
104
|
+
knobs: {
|
|
105
|
+
connection_timeout: { key: "connectionTimeout", scope: "client", unit: "ms", default: client.connectionTimeoutMs },
|
|
106
|
+
request_timeout: { key: "requestTimeout", scope: "client", unit: "ms", default: client.requestTimeoutMs },
|
|
107
|
+
heartbeat_interval: { key: "heartbeat.interval", scope: "client", unit: "ms", default: client.heartbeatIntervalMs },
|
|
108
|
+
heartbeat_timeout: { key: "heartbeat.timeout", scope: "client", unit: "ms", default: client.heartbeatTimeoutMs },
|
|
109
|
+
heartbeat_misses: { key: "heartbeat.misses", scope: "client", unit: "count", default: client.heartbeatMisses },
|
|
110
|
+
reconnect_initial_delay: { key: "reconnect.initialDelay", scope: "client", unit: "ms", default: client.reconnectInitialDelayMs },
|
|
111
|
+
reconnect_max_delay: { key: "reconnect.maxDelay", scope: "client", unit: "ms", default: client.reconnectMaxDelayMs },
|
|
112
|
+
},
|
|
113
|
+
timers: {
|
|
114
|
+
handshake: { duration: ms("connection_timeout"), arms: ["enter:CONNECTING"], cancels: ["open", "drop", "close"], fires: "handshake-timeout" },
|
|
115
|
+
reconnect: {
|
|
116
|
+
duration: ms("reconnect_max_delay"),
|
|
117
|
+
arms: ["enter:RECONNECTING"],
|
|
118
|
+
cancels: ["close"],
|
|
119
|
+
fires: "reconnect-due",
|
|
120
|
+
backoff: { resets: ["open"] },
|
|
121
|
+
note: "the longest wait: try n waits a random time up to min(maxDelay, initialDelay × 2^(n-1))",
|
|
122
|
+
},
|
|
123
|
+
ping: { duration: ms("heartbeat_interval"), arms: ["enter:CONNECTED", "ping-due"], cancels: ["drop", "close"], fires: "ping-due" },
|
|
124
|
+
pong: { duration: ms("heartbeat_timeout"), arms: ["ping-due"], cancels: ["pong", "drop", "close"], fires: "pong-timeout" },
|
|
125
|
+
},
|
|
126
|
+
transitions: [
|
|
127
|
+
{ id: "connect", from: "DISCONNECTED", on: "connect", to: "CONNECTING" },
|
|
128
|
+
{ id: "reconnect", from: "RECONNECTING", on: "reconnect-due", to: "CONNECTING", timer: "reconnect" },
|
|
129
|
+
{ id: "open", from: "CONNECTING", on: "open", to: "CONNECTED" },
|
|
130
|
+
{
|
|
131
|
+
id: "drop",
|
|
132
|
+
from: ["CONNECTING", "CONNECTED", "RECONNECTING"],
|
|
133
|
+
on: ["drop", "handshake-timeout", "pong-timeout"],
|
|
134
|
+
to: "RECONNECTING",
|
|
135
|
+
note: "the code reports each of them as drop",
|
|
136
|
+
},
|
|
137
|
+
{ id: "give-up", from: ["CONNECTING", "CONNECTED", "RECONNECTING"], on: "give-up", to: "DISCONNECTED" },
|
|
138
|
+
{ id: "close", from: ["DISCONNECTED", "CONNECTING", "CONNECTED", "RECONNECTING"], on: "close", to: "DISCONNECTING" },
|
|
139
|
+
{ id: "closed", from: "DISCONNECTING", on: "closed", to: "DISCONNECTED" },
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** The circuit breaker (`createCircuitBreakerMiddleware`). */
|
|
144
|
+
export const CIRCUIT_BREAKER_MACHINE: MachineDocument = {
|
|
145
|
+
format: "mark1russell7.machine/1",
|
|
146
|
+
id: "procedures.machine.circuit-breaker",
|
|
147
|
+
title: "The circuit breaker",
|
|
148
|
+
layer: "self",
|
|
149
|
+
version: "0.2.3",
|
|
150
|
+
sources: SOURCE,
|
|
151
|
+
states: {
|
|
152
|
+
CLOSED: { initial: true, note: "each call goes through. The window counts the failures of availability." },
|
|
153
|
+
OPEN: { note: "each call fails at once, until the reset time" },
|
|
154
|
+
HALF_OPEN: { note: "a limited number of probes go through" },
|
|
155
|
+
},
|
|
156
|
+
events: {
|
|
157
|
+
"failure-threshold": { kind: "local", note: "the window has the threshold of failures" },
|
|
158
|
+
"reset-timeout": { kind: "timer" },
|
|
159
|
+
"probe-failure": { kind: "local" },
|
|
160
|
+
"probe-successes": { kind: "local", note: "the threshold of successes of the probes" },
|
|
161
|
+
},
|
|
162
|
+
knobs: {
|
|
163
|
+
failure_threshold: { key: "failureThreshold", scope: "client", unit: "count", default: 5 },
|
|
164
|
+
failure_window: { key: "failureWindow", scope: "client", unit: "ms", default: TIMER_DEFAULTS.circuitBreaker.failureWindowMs },
|
|
165
|
+
reset_timeout: { key: "resetTimeout", scope: "client", unit: "ms", default: TIMER_DEFAULTS.circuitBreaker.resetTimeoutMs },
|
|
166
|
+
success_threshold: { key: "successThreshold", scope: "client", unit: "count", default: 2 },
|
|
167
|
+
},
|
|
168
|
+
timers: {
|
|
169
|
+
reset: { duration: ms("reset_timeout"), arms: ["failure-threshold", "probe-failure"], cancels: ["probe-successes"], fires: "reset-timeout" },
|
|
170
|
+
},
|
|
171
|
+
transitions: [
|
|
172
|
+
{ id: "trip", from: "CLOSED", on: "failure-threshold", to: "OPEN" },
|
|
173
|
+
{ id: "reset", from: "OPEN", on: "reset-timeout", to: "HALF_OPEN", timer: "reset" },
|
|
174
|
+
{ id: "reopen", from: "HALF_OPEN", on: "probe-failure", to: "OPEN" },
|
|
175
|
+
{ id: "recover", from: "HALF_OPEN", on: "probe-successes", to: "CLOSED" },
|
|
176
|
+
],
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* This function replays the reported transitions of one document. Each instance starts in the
|
|
181
|
+
* state that its first transition leaves. Each transition must leave the state where the replay
|
|
182
|
+
* is, and the document must allow it. The function gives each disagreement.
|
|
183
|
+
*/
|
|
184
|
+
export function replayTransitions(machine: MachineDocument, events: readonly MachineTransition[]): string[] {
|
|
185
|
+
return replay(machine, events, { machine: machine.id });
|
|
186
|
+
}
|
|
@@ -25,16 +25,14 @@ import type {
|
|
|
25
25
|
} from "./types.js";
|
|
26
26
|
import { pathToKey } from "./types.js";
|
|
27
27
|
import { isDataDriven } from "./ref.js";
|
|
28
|
+
import { inferredEffectsOf } from "./effects.js";
|
|
28
29
|
import { callMatches } from "./policy.js";
|
|
29
30
|
|
|
30
31
|
/** The standard resource names. An application adds its own names. */
|
|
31
32
|
export const STANDARD_RESOURCES = ["fs", "network", "process", "registry"] as const;
|
|
32
33
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
* only to make its deadline earlier.
|
|
36
|
-
*/
|
|
37
|
-
export const TIMEOUT_METADATA_KEY = "procedure-timeout";
|
|
34
|
+
// The metadata key of the remaining time of a remote call, in milliseconds (client/deadline.ts)
|
|
35
|
+
export { TIMEOUT_METADATA_KEY } from "../client/deadline.js";
|
|
38
36
|
|
|
39
37
|
/** The capability without limits: the capability of a host that sets none. */
|
|
40
38
|
export const UNLIMITED_CAPABILITY: Capability = Object.freeze({});
|
|
@@ -97,11 +95,12 @@ export interface ToolHints {
|
|
|
97
95
|
/**
|
|
98
96
|
* The MCP hints of a procedure, from its declared effects. A procedure without a declaration,
|
|
99
97
|
* or a data-driven one (its operands decide what it does), gets no hints: then the MCP defaults
|
|
100
|
-
* apply (not read-only, destructive, open world).
|
|
98
|
+
* apply (not read-only, destructive, open world). A procedure that `procedure.define` made gets
|
|
99
|
+
* the hints of the effects that the host found for its body (`inferredEffects`, roadmap R22).
|
|
101
100
|
*/
|
|
102
101
|
export function toolHints(procedure: AnyProcedure): ToolHints {
|
|
103
|
-
const effects = effectsOf(procedure);
|
|
104
|
-
if (!effects
|
|
102
|
+
const effects = isDataDriven(procedure) ? inferredEffectsOf(procedure) : effectsOf(procedure);
|
|
103
|
+
if (!effects) return {};
|
|
105
104
|
if (isReadOnly(effects)) {
|
|
106
105
|
return { readOnlyHint: true, openWorldHint: isOpenWorld(effects) };
|
|
107
106
|
}
|
|
@@ -168,7 +167,10 @@ export function narrowCapability<C>(
|
|
|
168
167
|
const effects = narrowEffects(base.effects, request.effects);
|
|
169
168
|
if (effects !== undefined) result.effects = effects;
|
|
170
169
|
|
|
171
|
-
|
|
170
|
+
// A time that is not a finite number of milliseconds (NaN, Infinity, below zero) asks for nothing:
|
|
171
|
+
// Math.min with NaN gives NaN, and a NaN deadline is no deadline, so it would widen
|
|
172
|
+
const timeoutMs = request.timeoutMs;
|
|
173
|
+
const requestedDeadline = typeof timeoutMs === "number" && Number.isFinite(timeoutMs) && timeoutMs >= 0 ? now + timeoutMs : undefined;
|
|
172
174
|
const deadline =
|
|
173
175
|
base.deadline === undefined ? requestedDeadline : requestedDeadline === undefined ? base.deadline : Math.min(base.deadline, requestedDeadline);
|
|
174
176
|
if (deadline !== undefined) result.deadline = deadline;
|
|
@@ -19,6 +19,10 @@ export {
|
|
|
19
19
|
hasOutputSchema,
|
|
20
20
|
getAllInputSchema,
|
|
21
21
|
getAllOutputSchema,
|
|
22
|
+
entriesInputSchema,
|
|
23
|
+
entriesOutputSchema,
|
|
24
|
+
changesInputSchema,
|
|
25
|
+
changesOutputSchema,
|
|
22
26
|
sizeInputSchema,
|
|
23
27
|
sizeOutputSchema,
|
|
24
28
|
clearInputSchema,
|
|
@@ -37,6 +41,10 @@ export type {
|
|
|
37
41
|
DeleteInput,
|
|
38
42
|
HasInput,
|
|
39
43
|
GetAllInput,
|
|
44
|
+
EntriesInput,
|
|
45
|
+
CollectionEntry,
|
|
46
|
+
ChangesInput,
|
|
47
|
+
ChangesOutput,
|
|
40
48
|
SizeInput,
|
|
41
49
|
ClearInput,
|
|
42
50
|
GetBatchInput,
|
|
@@ -48,4 +56,4 @@ export type {
|
|
|
48
56
|
export { createCollectionProcedures } from "./procedures.js";
|
|
49
57
|
|
|
50
58
|
// The storage interface of the core (the shape of client-collections' CollectionStorage)
|
|
51
|
-
export type { CollectionStorage, StorageMetadata } from "./storage.js";
|
|
59
|
+
export type { CollectionStorage, StorageChanges, StorageMetadata } from "./storage.js";
|
|
@@ -18,6 +18,10 @@ import {
|
|
|
18
18
|
hasOutputSchema,
|
|
19
19
|
getAllInputSchema,
|
|
20
20
|
getAllOutputSchema,
|
|
21
|
+
entriesInputSchema,
|
|
22
|
+
entriesOutputSchema,
|
|
23
|
+
changesInputSchema,
|
|
24
|
+
changesOutputSchema,
|
|
21
25
|
sizeInputSchema,
|
|
22
26
|
sizeOutputSchema,
|
|
23
27
|
clearInputSchema,
|
|
@@ -33,6 +37,9 @@ import {
|
|
|
33
37
|
type DeleteInput,
|
|
34
38
|
type HasInput,
|
|
35
39
|
type GetAllInput,
|
|
40
|
+
type EntriesInput,
|
|
41
|
+
type ChangesInput,
|
|
42
|
+
type ChangesOutput,
|
|
36
43
|
type SizeInput,
|
|
37
44
|
type ClearInput,
|
|
38
45
|
type GetBatchInput,
|
|
@@ -168,6 +175,56 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
|
|
|
168
175
|
},
|
|
169
176
|
}),
|
|
170
177
|
|
|
178
|
+
// Entries: each item with its id, for a sync (roadmap R2)
|
|
179
|
+
defineProcedure({
|
|
180
|
+
path: [...basePath, "entries"],
|
|
181
|
+
input: entriesInputSchema,
|
|
182
|
+
output: entriesOutputSchema,
|
|
183
|
+
metadata: {
|
|
184
|
+
effects: { reads: [resource], writes: [] },
|
|
185
|
+
description: `Get all items of ${collectionName} with their ids`,
|
|
186
|
+
tags: ["collections", collectionName, "read"],
|
|
187
|
+
},
|
|
188
|
+
handler: async (_input: EntriesInput, ctx: ProcedureContext) => {
|
|
189
|
+
const storage = getStorage(ctx);
|
|
190
|
+
if (!storage.getEntries) {
|
|
191
|
+
throw new Error(`The storage of ${collectionName} cannot give the ids of its items`);
|
|
192
|
+
}
|
|
193
|
+
return (await storage.getEntries()).map(([id, value]) => ({ id, value }));
|
|
194
|
+
},
|
|
195
|
+
}),
|
|
196
|
+
|
|
197
|
+
// Changes: the items that changed after a version, for a delta sync (roadmap R28)
|
|
198
|
+
defineProcedure({
|
|
199
|
+
path: [...basePath, "changes"],
|
|
200
|
+
input: changesInputSchema,
|
|
201
|
+
output: changesOutputSchema,
|
|
202
|
+
metadata: {
|
|
203
|
+
effects: { reads: [resource], writes: [] },
|
|
204
|
+
description: `Get the items of ${collectionName} that changed after a version`,
|
|
205
|
+
tags: ["collections", collectionName, "read"],
|
|
206
|
+
},
|
|
207
|
+
handler: async (input: ChangesInput, ctx: ProcedureContext): Promise<ChangesOutput> => {
|
|
208
|
+
const storage = getStorage(ctx);
|
|
209
|
+
if (storage.getChangesSince) {
|
|
210
|
+
const changes = await storage.getChangesSince(input.since);
|
|
211
|
+
return {
|
|
212
|
+
version: changes.version,
|
|
213
|
+
full: changes.full,
|
|
214
|
+
changed: changes.changed.map(([id, value]) => ({ id, value })),
|
|
215
|
+
deleted: changes.deleted,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
// A storage without a change log gives all its items. The empty version asks for all
|
|
219
|
+
// items again at the next call.
|
|
220
|
+
if (storage.getEntries) {
|
|
221
|
+
const entries = await storage.getEntries();
|
|
222
|
+
return { version: "", full: true, changed: entries.map(([id, value]) => ({ id, value })), deleted: [] };
|
|
223
|
+
}
|
|
224
|
+
throw new Error(`The storage of ${collectionName} cannot give its changes or the ids of its items`);
|
|
225
|
+
},
|
|
226
|
+
}),
|
|
227
|
+
|
|
171
228
|
// Size
|
|
172
229
|
defineProcedure({
|
|
173
230
|
path: [...basePath, "size"],
|
|
@@ -335,6 +335,116 @@ export const getAllOutputSchema : ZodLike<unknown[]> = createSchema<unknown[]>((
|
|
|
335
335
|
};
|
|
336
336
|
});
|
|
337
337
|
|
|
338
|
+
// =============================================================================
|
|
339
|
+
// Entries Operation Schemas
|
|
340
|
+
// =============================================================================
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Input for entries operation - empty object.
|
|
344
|
+
*/
|
|
345
|
+
export interface EntriesInput {}
|
|
346
|
+
|
|
347
|
+
export const entriesInputSchema : ZodLike<EntriesInput> = createSchema<EntriesInput>((data) => {
|
|
348
|
+
if (!isObject(data)) {
|
|
349
|
+
return {
|
|
350
|
+
success: false,
|
|
351
|
+
error: {
|
|
352
|
+
message: "Input must be an object",
|
|
353
|
+
errors: [{ path: [], message: "Input must be an object" }],
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
return { success: true, data: {} };
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
/** One item of a collection with its id. */
|
|
361
|
+
export interface CollectionEntry {
|
|
362
|
+
id: string;
|
|
363
|
+
value: unknown;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Output for entries operation - each item with its id. A sync needs the ids: `getAll` gives
|
|
368
|
+
* only the values (roadmap R2).
|
|
369
|
+
*/
|
|
370
|
+
export const entriesOutputSchema : ZodLike<CollectionEntry[]> = createSchema<CollectionEntry[]>((data) => {
|
|
371
|
+
if (isArray(data) && data.every((entry) => isObject(entry) && isString(entry["id"]) && "value" in entry)) {
|
|
372
|
+
return { success: true, data: data as CollectionEntry[] };
|
|
373
|
+
}
|
|
374
|
+
return {
|
|
375
|
+
success: false,
|
|
376
|
+
error: {
|
|
377
|
+
message: "Output must be an array of { id, value } objects",
|
|
378
|
+
errors: [{ path: [], message: "Output must be an array of { id, value } objects" }],
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
// =============================================================================
|
|
384
|
+
// Changes Operation Schemas
|
|
385
|
+
// =============================================================================
|
|
386
|
+
|
|
387
|
+
/** Input for changes operation: the version of an earlier call (roadmap R28). */
|
|
388
|
+
export interface ChangesInput {
|
|
389
|
+
/** The `version` of an earlier result. Without it, the result has all items. */
|
|
390
|
+
since?: string;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export const changesInputSchema : ZodLike<ChangesInput> = createSchema<ChangesInput>((data) => {
|
|
394
|
+
if (!isObject(data)) {
|
|
395
|
+
return {
|
|
396
|
+
success: false,
|
|
397
|
+
error: {
|
|
398
|
+
message: "Input must be an object",
|
|
399
|
+
errors: [{ path: [], message: "Input must be an object" }],
|
|
400
|
+
},
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
const since = data["since"];
|
|
404
|
+
if (since !== undefined && !isString(since)) {
|
|
405
|
+
return {
|
|
406
|
+
success: false,
|
|
407
|
+
error: {
|
|
408
|
+
message: "since must be a string",
|
|
409
|
+
errors: [{ path: ["since"], message: "since must be a string" }],
|
|
410
|
+
},
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
return { success: true, data: since === undefined ? {} : { since } };
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Output for changes operation: the items that changed after the version, the deleted ids, and
|
|
418
|
+
* the new version. When `full` is true, `changed` has each item (roadmap R28).
|
|
419
|
+
*/
|
|
420
|
+
export interface ChangesOutput {
|
|
421
|
+
version: string;
|
|
422
|
+
full: boolean;
|
|
423
|
+
changed: CollectionEntry[];
|
|
424
|
+
deleted: string[];
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export const changesOutputSchema : ZodLike<ChangesOutput> = createSchema<ChangesOutput>((data) => {
|
|
428
|
+
if (
|
|
429
|
+
isObject(data) &&
|
|
430
|
+
isString(data["version"]) &&
|
|
431
|
+
typeof data["full"] === "boolean" &&
|
|
432
|
+
isArray(data["changed"]) &&
|
|
433
|
+
data["changed"].every((entry) => isObject(entry) && isString(entry["id"]) && "value" in entry) &&
|
|
434
|
+
isArray(data["deleted"]) &&
|
|
435
|
+
data["deleted"].every((id) => isString(id))
|
|
436
|
+
) {
|
|
437
|
+
return { success: true, data: data as unknown as ChangesOutput };
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
success: false,
|
|
441
|
+
error: {
|
|
442
|
+
message: "Output must be { version, full, changed: [{ id, value }], deleted: [id] }",
|
|
443
|
+
errors: [{ path: [], message: "Output must be { version, full, changed: [{ id, value }], deleted: [id] }" }],
|
|
444
|
+
},
|
|
445
|
+
};
|
|
446
|
+
});
|
|
447
|
+
|
|
338
448
|
// =============================================================================
|
|
339
449
|
// Size Operation Schemas
|
|
340
450
|
// =============================================================================
|
|
@@ -15,6 +15,18 @@ export interface CollectionStorage<T> {
|
|
|
15
15
|
get(id: string): Promise<T | undefined>;
|
|
16
16
|
/** Every item of the collection. */
|
|
17
17
|
getAll(): Promise<T[]>;
|
|
18
|
+
/**
|
|
19
|
+
* Every item with its id. A sync needs the ids (roadmap R2). A storage that cannot give
|
|
20
|
+
* them does not have this method.
|
|
21
|
+
*/
|
|
22
|
+
getEntries?(): Promise<Array<[string, T]>>;
|
|
23
|
+
/**
|
|
24
|
+
* The changes after a version (roadmap R28). A storage without a change log does not have
|
|
25
|
+
* this method, and a sync uses `getEntries()`.
|
|
26
|
+
*
|
|
27
|
+
* @param since - A `version` that an earlier call gave. Without it, the result has all items.
|
|
28
|
+
*/
|
|
29
|
+
getChangesSince?(since?: string): Promise<StorageChanges<T>>;
|
|
18
30
|
/** The items for which the predicate gives true. */
|
|
19
31
|
find(predicate: (item: T) => boolean): Promise<T[]>;
|
|
20
32
|
/** True when an item has this id. */
|
|
@@ -33,12 +45,38 @@ export interface CollectionStorage<T> {
|
|
|
33
45
|
deleteBatch(ids: string[]): Promise<number>;
|
|
34
46
|
/** The items of several ids. An id with no item is not in the map. */
|
|
35
47
|
getBatch(ids: string[]): Promise<Map<string, T>>;
|
|
48
|
+
/**
|
|
49
|
+
* This method sends the writes that wait in the storage to their destination (roadmap R28). A storage
|
|
50
|
+
* that keeps no writes does not have this method. `ProcedureServer.useStorage` calls it.
|
|
51
|
+
*/
|
|
52
|
+
flush?(): Promise<void>;
|
|
36
53
|
/** Release the resources of the storage. */
|
|
37
54
|
close(): Promise<void>;
|
|
38
55
|
/** The kind, the size and the statistics of the storage. */
|
|
39
56
|
getMetadata?(): Promise<StorageMetadata>;
|
|
40
57
|
}
|
|
41
58
|
|
|
59
|
+
/**
|
|
60
|
+
* The changes of a storage after a version (roadmap R28).
|
|
61
|
+
*
|
|
62
|
+
* - `version` is opaque. The next call gives it back as `since`.
|
|
63
|
+
* - When `full` is true, `changed` has every item, and each id that it does not have was
|
|
64
|
+
* deleted. A storage gives a full result when it cannot find the changes since the version:
|
|
65
|
+
* the first call, a version of an older run, or a change log that dropped old deletes.
|
|
66
|
+
* - When `full` is false, `changed` has the items that changed after the version, and `deleted`
|
|
67
|
+
* has the ids that were deleted after it.
|
|
68
|
+
*/
|
|
69
|
+
export interface StorageChanges<T> {
|
|
70
|
+
/** The version of the storage that includes these changes. */
|
|
71
|
+
version: string;
|
|
72
|
+
/** True when `changed` has every item of the storage. */
|
|
73
|
+
full: boolean;
|
|
74
|
+
/** The items that changed, with their ids. */
|
|
75
|
+
changed: Array<[string, T]>;
|
|
76
|
+
/** The ids that were deleted. Empty when `full` is true. */
|
|
77
|
+
deleted: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
42
80
|
/** The metadata that a storage gives about itself. */
|
|
43
81
|
export interface StorageMetadata {
|
|
44
82
|
type: "memory" | "api" | "hybrid" | "synced" | "custom";
|
|
@@ -11,6 +11,16 @@ import { defineProcedure, namespace } from "../define.js";
|
|
|
11
11
|
import type { AnyProcedure, Procedure } from "../types.js";
|
|
12
12
|
import { PURE_EFFECTS, withEffects } from "../effects.js";
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* This function gives the own field `key` of an item, or undefined. A field of the prototype
|
|
16
|
+
* (`toString`, `constructor`) is not a field of the item: before, `count` with the key
|
|
17
|
+
* `toString` counted each item (catalog test, roadmap R29, as R6 for the object procedures).
|
|
18
|
+
*/
|
|
19
|
+
function fieldOf(item: unknown, key: string): unknown {
|
|
20
|
+
if (item === null || item === undefined) return undefined;
|
|
21
|
+
return Object.hasOwn(Object(item), key) ? (item as Record<string, unknown>)[key] : undefined;
|
|
22
|
+
}
|
|
23
|
+
|
|
14
24
|
// =============================================================================
|
|
15
25
|
// First Procedure
|
|
16
26
|
// =============================================================================
|
|
@@ -209,8 +219,8 @@ const sortProcedure: SortProcedure = defineProcedure({
|
|
|
209
219
|
const direction = input.desc ? -1 : 1;
|
|
210
220
|
|
|
211
221
|
return items.sort((a, b) => {
|
|
212
|
-
let aVal = input.key ? (a
|
|
213
|
-
let bVal = input.key ? (b
|
|
222
|
+
let aVal = input.key ? fieldOf(a, input.key) : a;
|
|
223
|
+
let bVal = input.key ? fieldOf(b, input.key) : b;
|
|
214
224
|
|
|
215
225
|
if (typeof aVal === "string" && typeof bVal === "string") {
|
|
216
226
|
return aVal.localeCompare(bVal) * direction;
|
|
@@ -309,7 +319,7 @@ const uniqueProcedure: UniqueProcedure = defineProcedure({
|
|
|
309
319
|
if (input.key) {
|
|
310
320
|
const seen = new Set<unknown>();
|
|
311
321
|
return input.items.filter((item) => {
|
|
312
|
-
const val = (item
|
|
322
|
+
const val = fieldOf(item, input.key!);
|
|
313
323
|
if (seen.has(val)) return false;
|
|
314
324
|
seen.add(val);
|
|
315
325
|
return true;
|
|
@@ -346,11 +356,13 @@ const groupByProcedure: GroupByProcedure = defineProcedure({
|
|
|
346
356
|
handler: async (input: GroupByInput): Promise<Record<string, unknown[]>> => {
|
|
347
357
|
const result: Record<string, unknown[]> = {};
|
|
348
358
|
for (const item of input.items) {
|
|
349
|
-
const keyVal = String((item
|
|
350
|
-
|
|
351
|
-
|
|
359
|
+
const keyVal = String(fieldOf(item, input.key));
|
|
360
|
+
// Only an own group: before, a group "constructor" or "toString" found the function of the
|
|
361
|
+
// prototype, and push failed (catalog test, roadmap R29)
|
|
362
|
+
if (!Object.hasOwn(result, keyVal)) {
|
|
363
|
+
Object.defineProperty(result, keyVal, { value: [], writable: true, enumerable: true, configurable: true });
|
|
352
364
|
}
|
|
353
|
-
result[keyVal]
|
|
365
|
+
result[keyVal]!.push(item);
|
|
354
366
|
}
|
|
355
367
|
return result;
|
|
356
368
|
},
|
|
@@ -640,7 +652,7 @@ const filterProcedure: FilterProcedure = defineProcedure({
|
|
|
640
652
|
const { items, key, value, values, match = 'equals', invert = false } = input;
|
|
641
653
|
|
|
642
654
|
const predicate = (item: unknown): boolean => {
|
|
643
|
-
const itemValue = key ? (item
|
|
655
|
+
const itemValue = key ? fieldOf(item, key) : item;
|
|
644
656
|
|
|
645
657
|
let result: boolean;
|
|
646
658
|
switch (match) {
|
|
@@ -716,10 +728,7 @@ const whereProcedure: WhereProcedure = defineProcedure({
|
|
|
716
728
|
},
|
|
717
729
|
handler: async (input: WhereInput): Promise<unknown[]> => {
|
|
718
730
|
const { items, where } = input;
|
|
719
|
-
return items.filter((item) =>
|
|
720
|
-
const obj = item as Record<string, unknown>;
|
|
721
|
-
return Object.entries(where).every(([key, value]) => obj[key] === value);
|
|
722
|
-
});
|
|
731
|
+
return items.filter((item) => Object.entries(where).every(([key, value]) => fieldOf(item, key) === value));
|
|
723
732
|
},
|
|
724
733
|
});
|
|
725
734
|
|
|
@@ -750,7 +759,7 @@ const pluckProcedure: PluckProcedure = defineProcedure({
|
|
|
750
759
|
tags: ["core", "array", "transform"],
|
|
751
760
|
},
|
|
752
761
|
handler: async (input: PluckInput): Promise<unknown[]> => {
|
|
753
|
-
return input.items.map((item) => (item
|
|
762
|
+
return input.items.map((item) => fieldOf(item, input.key));
|
|
754
763
|
},
|
|
755
764
|
});
|
|
756
765
|
|
|
@@ -785,7 +794,9 @@ const arrPickProcedure: ArrPickProcedure = defineProcedure({
|
|
|
785
794
|
const obj = item as Record<string, unknown>;
|
|
786
795
|
const result: Record<string, unknown> = {};
|
|
787
796
|
for (const key of input.keys) {
|
|
788
|
-
|
|
797
|
+
// Only own fields, as client.pick: before, "toString" picked a function of the prototype
|
|
798
|
+
// (catalog test, roadmap R29)
|
|
799
|
+
if (Object.hasOwn(obj, key)) {
|
|
789
800
|
result[key] = obj[key];
|
|
790
801
|
}
|
|
791
802
|
}
|
|
@@ -881,7 +892,7 @@ const partitionProcedure: PartitionProcedure = defineProcedure({
|
|
|
881
892
|
const falsy: unknown[] = [];
|
|
882
893
|
|
|
883
894
|
for (const item of input.items) {
|
|
884
|
-
const itemValue = (item
|
|
895
|
+
const itemValue = fieldOf(item, input.key);
|
|
885
896
|
const matches = input.value !== undefined ? itemValue === input.value : Boolean(itemValue);
|
|
886
897
|
if (matches) {
|
|
887
898
|
truthy.push(item);
|
|
@@ -929,7 +940,7 @@ const countProcedure: CountProcedure = defineProcedure({
|
|
|
929
940
|
}
|
|
930
941
|
|
|
931
942
|
return input.items.filter((item) => {
|
|
932
|
-
const itemValue = (item
|
|
943
|
+
const itemValue = fieldOf(item, input.key!);
|
|
933
944
|
return input.value !== undefined ? itemValue === input.value : Boolean(itemValue);
|
|
934
945
|
}).length;
|
|
935
946
|
},
|
|
@@ -582,6 +582,10 @@ const mapProcedure: MapProcedure = defineProcedure({
|
|
|
582
582
|
// map is a control-flow procedure: its operands arrive raw, and it runs them here.
|
|
583
583
|
// Before, the handler only returned the raw items, so their refs never ran and no
|
|
584
584
|
// function was applied.
|
|
585
|
+
if (input.fn !== undefined && !isAnyProcedureRef(input.fn)) {
|
|
586
|
+
// Before, a fn that was not a call was ignored without a word (roadmap R4)
|
|
587
|
+
throw new Error("client.map: fn must be one call ({ $proc, input }), or absent");
|
|
588
|
+
}
|
|
585
589
|
const outer = scopeOf(input);
|
|
586
590
|
const items = await hydrateItems(input.items, ctx, outer);
|
|
587
591
|
if (!isAnyProcedureRef(input.fn) || !ctx?.client) {
|
|
@@ -625,6 +629,10 @@ const reduceProcedure: ReduceProcedure = defineProcedure({
|
|
|
625
629
|
tags: controlFlowTags("collection"),
|
|
626
630
|
},
|
|
627
631
|
handler: async (input: ReduceInput, ctx?: ProcedureContext): Promise<unknown> => {
|
|
632
|
+
if (input.fn !== undefined && !isAnyProcedureRef(input.fn)) {
|
|
633
|
+
// Before, a fn that was not a call was ignored without a word (roadmap R4)
|
|
634
|
+
throw new Error("client.reduce: fn must be one call ({ $proc, input }), or absent");
|
|
635
|
+
}
|
|
628
636
|
if (!ctx?.client) {
|
|
629
637
|
return input.accumulated ?? input.initial;
|
|
630
638
|
}
|
|
@@ -331,7 +331,8 @@ const lookupProcedure: LookupProcedure = defineProcedure({
|
|
|
331
331
|
handler: async (input: LookupInput, ctx): Promise<LookupOutput> => {
|
|
332
332
|
// The registry of the caller (a server or a client with its own registry)
|
|
333
333
|
const proc = (ctx?.registry ?? PROCEDURE_REGISTRY).get(input.path);
|
|
334
|
-
|
|
334
|
+
// A procedure outside the expose rule of the host does not exist for its callers (R17)
|
|
335
|
+
if (proc && (!ctx?.expose || ctx.expose(proc.path))) {
|
|
335
336
|
return {
|
|
336
337
|
exists: true,
|
|
337
338
|
metadata: proc.metadata,
|