@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
|
@@ -11,15 +11,46 @@ 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
|
+
// Helpers: own keys only (roadmap R6)
|
|
16
|
+
// =============================================================================
|
|
17
|
+
|
|
18
|
+
/** The path segments that reach a prototype. The registry refuses them in paths too. */
|
|
19
|
+
const PROTOTYPE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The segments of a dotted path. A segment that reaches a prototype is an error: before,
|
|
23
|
+
* `client.set` with the path `__proto__.x` changed the prototype of its result (roadmap R6).
|
|
24
|
+
*/
|
|
25
|
+
function segmentsOf(path: string): string[] {
|
|
26
|
+
const parts = path.split(".");
|
|
27
|
+
for (const part of parts) {
|
|
28
|
+
if (PROTOTYPE_KEYS.has(part)) {
|
|
29
|
+
throw new Error(`The path "${path}" has the segment "${part}", which reaches a prototype`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return parts;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function hasOwn(obj: object, key: string): boolean {
|
|
36
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** This function sets an own data property: a "__proto__" key of the data stays data. */
|
|
40
|
+
function put(target: Record<string, unknown>, key: string, value: unknown): void {
|
|
41
|
+
Object.defineProperty(target, key, { value, writable: true, enumerable: true, configurable: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
14
44
|
// =============================================================================
|
|
15
45
|
// Helper: Get nested value by path
|
|
16
46
|
// =============================================================================
|
|
17
47
|
|
|
18
48
|
function getByPath(obj: unknown, path: string): unknown {
|
|
19
|
-
const parts = path
|
|
49
|
+
const parts = segmentsOf(path);
|
|
20
50
|
let current: unknown = obj;
|
|
21
51
|
for (const part of parts) {
|
|
22
|
-
|
|
52
|
+
// Only own fields: before, "toString" gave a function of the prototype
|
|
53
|
+
if (current === null || typeof current !== "object" || !hasOwn(current, part)) {
|
|
23
54
|
return undefined;
|
|
24
55
|
}
|
|
25
56
|
current = (current as Record<string, unknown>)[part];
|
|
@@ -33,22 +64,22 @@ function getByPath(obj: unknown, path: string): unknown {
|
|
|
33
64
|
|
|
34
65
|
function setByPath(obj: Record<string, unknown>, path: string, value: unknown): Record<string, unknown> {
|
|
35
66
|
const result = { ...obj };
|
|
36
|
-
const parts = path
|
|
67
|
+
const parts = segmentsOf(path);
|
|
37
68
|
let current: Record<string, unknown> = result;
|
|
38
69
|
|
|
39
70
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
40
71
|
const part = parts[i]!;
|
|
41
|
-
if (!(part
|
|
42
|
-
current
|
|
72
|
+
if (!hasOwn(current, part) || typeof current[part] !== "object" || current[part] === null) {
|
|
73
|
+
put(current, part, {});
|
|
43
74
|
} else {
|
|
44
|
-
current
|
|
75
|
+
put(current, part, { ...(current[part] as Record<string, unknown>) });
|
|
45
76
|
}
|
|
46
77
|
current = current[part] as Record<string, unknown>;
|
|
47
78
|
}
|
|
48
79
|
|
|
49
80
|
const lastPart = parts[parts.length - 1];
|
|
50
81
|
if (lastPart !== undefined) {
|
|
51
|
-
current
|
|
82
|
+
put(current, lastPart, value);
|
|
52
83
|
}
|
|
53
84
|
return result;
|
|
54
85
|
}
|
|
@@ -139,16 +170,17 @@ function deepMerge(target: Record<string, unknown>, source: Record<string, unkno
|
|
|
139
170
|
typeof source[key] === "object" &&
|
|
140
171
|
source[key] !== null &&
|
|
141
172
|
!Array.isArray(source[key]) &&
|
|
173
|
+
hasOwn(result, key) &&
|
|
142
174
|
typeof result[key] === "object" &&
|
|
143
175
|
result[key] !== null &&
|
|
144
176
|
!Array.isArray(result[key])
|
|
145
177
|
) {
|
|
146
|
-
result
|
|
178
|
+
put(result, key, deepMerge(
|
|
147
179
|
result[key] as Record<string, unknown>,
|
|
148
180
|
source[key] as Record<string, unknown>
|
|
149
|
-
);
|
|
181
|
+
));
|
|
150
182
|
} else {
|
|
151
|
-
result
|
|
183
|
+
put(result, key, source[key]);
|
|
152
184
|
}
|
|
153
185
|
}
|
|
154
186
|
return result;
|
|
@@ -166,7 +198,8 @@ const mergeProcedure: MergeProcedure = defineProcedure({
|
|
|
166
198
|
if (input.deep) {
|
|
167
199
|
return input.objects.reduce((acc, obj) => deepMerge(acc, obj), {});
|
|
168
200
|
}
|
|
169
|
-
|
|
201
|
+
// Object.assign sets a "__proto__" key of the data as the prototype: spread keeps it data
|
|
202
|
+
return input.objects.reduce<Record<string, unknown>>((acc, obj) => ({ ...acc, ...obj }), {});
|
|
170
203
|
},
|
|
171
204
|
});
|
|
172
205
|
|
|
@@ -307,8 +340,9 @@ const pickProcedure: PickProcedure = defineProcedure({
|
|
|
307
340
|
handler: async (input: PickInput): Promise<Record<string, unknown>> => {
|
|
308
341
|
const result: Record<string, unknown> = {};
|
|
309
342
|
for (const key of input.keys) {
|
|
310
|
-
|
|
311
|
-
|
|
343
|
+
// Only own fields: before, "toString" picked a function of the prototype
|
|
344
|
+
if (hasOwn(input.object, key)) {
|
|
345
|
+
put(result, key, input.object[key]);
|
|
312
346
|
}
|
|
313
347
|
}
|
|
314
348
|
return result;
|
|
@@ -344,7 +378,7 @@ const omitProcedure: OmitProcedure = defineProcedure({
|
|
|
344
378
|
const result: Record<string, unknown> = {};
|
|
345
379
|
for (const key of Object.keys(input.object)) {
|
|
346
380
|
if (!keysToOmit.has(key)) {
|
|
347
|
-
result
|
|
381
|
+
put(result, key, input.object[key]);
|
|
348
382
|
}
|
|
349
383
|
}
|
|
350
384
|
return result;
|
|
@@ -7,6 +7,8 @@ import type { ProcedureContext, ProcedurePath } from "../types.js";
|
|
|
7
7
|
import { PROCEDURE_REGISTRY } from "../registry.js";
|
|
8
8
|
import {
|
|
9
9
|
isAnyProcedureRef,
|
|
10
|
+
getRefWhen,
|
|
11
|
+
WHEN_NEVER,
|
|
10
12
|
normalizeRef,
|
|
11
13
|
hydrateInput,
|
|
12
14
|
createRefScope,
|
|
@@ -73,6 +75,11 @@ export async function callOperand(
|
|
|
73
75
|
ctx: ProcedureContext,
|
|
74
76
|
options: { scope?: RefScope | undefined; extra?: Record<string, unknown> | undefined } = {},
|
|
75
77
|
): Promise<unknown> {
|
|
78
|
+
// A $never operand is data with its full input, as a $never call anywhere (roadmap R4: before,
|
|
79
|
+
// only a chain step obeyed it, and a $never branch, task or fn started)
|
|
80
|
+
if (getRefWhen(ref) === WHEN_NEVER) {
|
|
81
|
+
return ref;
|
|
82
|
+
}
|
|
76
83
|
const normalized = normalizeRef(ref);
|
|
77
84
|
const raw = rawRule(ctx)(normalized.path);
|
|
78
85
|
let input: unknown = raw ? normalized.input : await hydrateOperand(normalized.input, ctx, options.scope);
|
|
@@ -132,7 +132,9 @@ const replaceProcedure: ReplaceProcedure = defineProcedure({
|
|
|
132
132
|
if (input.all) {
|
|
133
133
|
return input.value.split(input.search).join(input.replace);
|
|
134
134
|
}
|
|
135
|
-
|
|
135
|
+
// A function gives the replacement as text. Before, "$$" became "$" and "$&" became the match
|
|
136
|
+
// (catalog test, roadmap R29).
|
|
137
|
+
return input.value.replace(input.search, () => input.replace);
|
|
136
138
|
},
|
|
137
139
|
});
|
|
138
140
|
|
|
@@ -382,7 +384,7 @@ interface TemplateInput {
|
|
|
382
384
|
}
|
|
383
385
|
|
|
384
386
|
const TemplateInputSchema = z.object({
|
|
385
|
-
template: z.string().describe("The template text, with {name} placeholders"),
|
|
387
|
+
template: z.string().describe("The template text, with {{name}} placeholders"),
|
|
386
388
|
values: z.record(z.string(), z.unknown()).describe("The values"),
|
|
387
389
|
});
|
|
388
390
|
|
|
@@ -34,6 +34,7 @@ import type {
|
|
|
34
34
|
ProcedureContext,
|
|
35
35
|
AnyProcedure,
|
|
36
36
|
ProcedureRegistryLike,
|
|
37
|
+
Effects,
|
|
37
38
|
} from "./types.js";
|
|
38
39
|
import { z } from "zod";
|
|
39
40
|
import { anySchema, zodAdapter } from "./core/schemas.js";
|
|
@@ -47,7 +48,10 @@ import {
|
|
|
47
48
|
rawInputRule,
|
|
48
49
|
RUNS_REFS_TAG,
|
|
49
50
|
} from "./ref.js";
|
|
50
|
-
import { REGISTRY_READ_EFFECTS, withEffects } from "./effects.js";
|
|
51
|
+
import { INFERRED_EFFECTS_METADATA_KEY, PURE_EFFECTS, REGISTRY_READ_EFFECTS, effectsValue, withEffects } from "./effects.js";
|
|
52
|
+
import { MANIFEST_FORMAT, describeProcedure as describeEntry, describeRegistry, type ProcedureManifest } from "./manifest.js";
|
|
53
|
+
import type { JsonSchema } from "./schema.js";
|
|
54
|
+
import { isRuntimeDefined, markRuntimeProgram, runtimeProgramOfHandler } from "./runtime-programs.js";
|
|
51
55
|
|
|
52
56
|
// Input schemas of procedure.define and procedure.delete (roadmap 2.1)
|
|
53
57
|
const procedurePathField = z.array(z.string().min(1)).min(1);
|
|
@@ -151,7 +155,24 @@ const GetOutputSchema = z.looseObject({
|
|
|
151
155
|
metadata: descriptionFields.metadata,
|
|
152
156
|
hasInputSchema: z.boolean().optional().describe("True when the procedure has an input schema"),
|
|
153
157
|
hasOutputSchema: z.boolean().optional().describe("True when the procedure has an output schema"),
|
|
154
|
-
|
|
158
|
+
input: z.record(z.string(), z.unknown()).optional().describe("The JSON Schema of the input"),
|
|
159
|
+
output: z.record(z.string(), z.unknown()).optional().describe("The JSON Schema of the output"),
|
|
160
|
+
effects: z.unknown().optional().describe("The declared effects: reads, writes, idempotent, destructive"),
|
|
161
|
+
}) as z.ZodType<{ found: false } | (ProcedureDescription & GetSchemas & { found: true; hasInputSchema: boolean; hasOutputSchema: boolean })>;
|
|
162
|
+
|
|
163
|
+
/** The JSON Schemas and the effects that `procedure.get` gives (roadmap R17). */
|
|
164
|
+
interface GetSchemas {
|
|
165
|
+
input?: JsonSchema | undefined;
|
|
166
|
+
output?: JsonSchema | undefined;
|
|
167
|
+
effects?: Effects | undefined;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** The output of `procedure.manifest` */
|
|
171
|
+
const ManifestOutputSchema = z.looseObject({
|
|
172
|
+
format: z.literal(MANIFEST_FORMAT).describe("The format of the manifest"),
|
|
173
|
+
hash: z.string().describe("A hash of the entries: two manifests with the same hash describe the same procedures"),
|
|
174
|
+
procedures: z.array(z.looseObject({ path: z.array(z.string()), key: z.string() })).describe("The procedures, in the order of their keys"),
|
|
175
|
+
}) as unknown as z.ZodType<ProcedureManifest>;
|
|
155
176
|
|
|
156
177
|
/** The output of `procedure.list` */
|
|
157
178
|
const ListOutputSchema = z.looseObject({
|
|
@@ -204,26 +225,15 @@ function createAggregationHandler(
|
|
|
204
225
|
// Runtime-Defined Procedures
|
|
205
226
|
// =============================================================================
|
|
206
227
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
* registry is the only store of the procedures: a procedure is runtime-defined when its handler
|
|
210
|
-
* is a key here. (Before, a separate map kept them, and `procedure.delete` removed only the map
|
|
211
|
-
* entry: deep dive CORE-3.) The program is kept here, not only in the metadata: a caller that
|
|
212
|
-
* gives its own metadata to `procedure.define` replaces the metadata.
|
|
213
|
-
*/
|
|
214
|
-
const RUNTIME_PROGRAMS = new WeakMap<object, AggregationDefinition>();
|
|
215
|
-
|
|
216
|
-
/** True for a procedure that `procedure.define` made. */
|
|
217
|
-
export function isRuntimeDefined(procedure: { handler?: unknown }): boolean {
|
|
218
|
-
return typeof procedure.handler === "function" && RUNTIME_PROGRAMS.has(procedure.handler);
|
|
219
|
-
}
|
|
228
|
+
// The store of the programs of procedure.define (runtime-programs.ts, which imports nothing)
|
|
229
|
+
export { isRuntimeDefined } from "./runtime-programs.js";
|
|
220
230
|
|
|
221
231
|
/**
|
|
222
232
|
* The program (the aggregation) of a procedure that `procedure.define` made, or undefined for a
|
|
223
233
|
* code procedure. A program store saves it and defines it again after a restart (roadmap 3.2).
|
|
224
234
|
*/
|
|
225
235
|
export function runtimeProgramOf(procedure: { handler?: unknown }): AggregationDefinition | undefined {
|
|
226
|
-
return
|
|
236
|
+
return runtimeProgramOfHandler(procedure.handler) as AggregationDefinition | undefined;
|
|
227
237
|
}
|
|
228
238
|
|
|
229
239
|
type RegistryOf = Pick<ProcedureRegistryLike, "get" | "getAll" | "unregister">;
|
|
@@ -327,22 +337,33 @@ export const defineProcedureProcedure: Procedure<
|
|
|
327
337
|
throw new Error(`Procedure already exists at path: ${pathKey}`);
|
|
328
338
|
}
|
|
329
339
|
|
|
340
|
+
// The effects of the body that the host found (roadmap R22). Only an interceptor of the host
|
|
341
|
+
// can give them: each transport removes the key from a request.
|
|
342
|
+
const inferred = effectsValue(ctx?.metadata?.[INFERRED_EFFECTS_METADATA_KEY]);
|
|
343
|
+
|
|
330
344
|
// Create the procedure
|
|
331
345
|
const procedure: AnyProcedure = defineProcedure({
|
|
332
346
|
path,
|
|
333
347
|
input: anySchema,
|
|
334
348
|
output: anySchema,
|
|
335
|
-
metadata:
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
349
|
+
metadata: {
|
|
350
|
+
...(metadata ?? {
|
|
351
|
+
description: `Runtime-defined aggregation procedure`,
|
|
352
|
+
generatedFrom: "procedure.define",
|
|
353
|
+
aggregation,
|
|
354
|
+
}),
|
|
355
|
+
// No effects of its own, as client.chain: the admission examines each nested call of a
|
|
356
|
+
// data-driven procedure. The framework sets it, so the metadata of a caller cannot claim less
|
|
357
|
+
effects: PURE_EFFECTS,
|
|
358
|
+
// The framework sets this field too: the effects of the body, for the tool hints
|
|
359
|
+
...(inferred ? { inferredEffects: inferred } : { inferredEffects: undefined }),
|
|
339
360
|
},
|
|
340
361
|
handler: createAggregationHandler(aggregation),
|
|
341
362
|
});
|
|
342
363
|
|
|
343
364
|
// Its behavior comes from data: a server with an expose rule lets it call only exposed procedures
|
|
344
365
|
markDataDriven(procedure);
|
|
345
|
-
|
|
366
|
+
markRuntimeProgram(procedure.handler!, aggregation);
|
|
346
367
|
|
|
347
368
|
// Validate the procedure
|
|
348
369
|
validateProcedure(procedure);
|
|
@@ -430,7 +451,7 @@ const getInputSchema: z.ZodType<ProcedureGetInput> = z.object({
|
|
|
430
451
|
*/
|
|
431
452
|
export const getProcedureProcedure: Procedure<
|
|
432
453
|
ProcedureGetInput,
|
|
433
|
-
{ found: false } | (ProcedureDescription & { found: true; hasInputSchema: boolean; hasOutputSchema: boolean }),
|
|
454
|
+
{ found: false } | (ProcedureDescription & GetSchemas & { found: true; hasInputSchema: boolean; hasOutputSchema: boolean }),
|
|
434
455
|
ProcedureMetadata
|
|
435
456
|
> = defineProcedure({
|
|
436
457
|
path: ["procedure", "get"],
|
|
@@ -445,14 +466,20 @@ export const getProcedureProcedure: Procedure<
|
|
|
445
466
|
handler: async (input: ProcedureGetInput, ctx?: ProcedureContext) => {
|
|
446
467
|
const path = typeof input.path === "string" ? keyToPath(input.path) : input.path;
|
|
447
468
|
const procedure = (ctx?.registry ?? PROCEDURE_REGISTRY).get(path);
|
|
448
|
-
|
|
469
|
+
// A procedure outside the expose rule of the host is not found (R17)
|
|
470
|
+
if (!procedure || (ctx?.expose && !ctx.expose(procedure.path))) {
|
|
449
471
|
return { found: false as const };
|
|
450
472
|
}
|
|
473
|
+
const entry = describeEntry(procedure);
|
|
451
474
|
return {
|
|
452
475
|
found: true as const,
|
|
453
476
|
...describeProcedure(procedure, true),
|
|
454
477
|
hasInputSchema: !!procedure.input,
|
|
455
478
|
hasOutputSchema: !!procedure.output,
|
|
479
|
+
// The JSON Schemas and the effects of the one manifest type (R17)
|
|
480
|
+
...(entry.input ? { input: entry.input } : {}),
|
|
481
|
+
...(entry.output ? { output: entry.output } : {}),
|
|
482
|
+
...(entry.effects ? { effects: entry.effects } : {}),
|
|
456
483
|
};
|
|
457
484
|
},
|
|
458
485
|
});
|
|
@@ -479,6 +506,8 @@ export const listProceduresProcedure: Procedure<
|
|
|
479
506
|
},
|
|
480
507
|
handler: async (input: ProcedureListInput, ctx?: ProcedureContext) => {
|
|
481
508
|
let all = (ctx?.registry ?? PROCEDURE_REGISTRY).getAll();
|
|
509
|
+
// Only the procedures that the host exposes (R17)
|
|
510
|
+
if (ctx?.expose) all = all.filter((procedure) => ctx.expose!(procedure.path));
|
|
482
511
|
if (input.runtime) all = all.filter(isRuntimeDefined);
|
|
483
512
|
if (input.namespace) all = all.filter((procedure) => procedure.path[0] === input.namespace);
|
|
484
513
|
const procedures = all.map((procedure) => describeProcedure(procedure, input.includeMetadata ?? false));
|
|
@@ -486,6 +515,31 @@ export const listProceduresProcedure: Procedure<
|
|
|
486
515
|
},
|
|
487
516
|
});
|
|
488
517
|
|
|
518
|
+
/**
|
|
519
|
+
* The manifest of the caller's registry (roadmap R17): each procedure that the host exposes, with
|
|
520
|
+
* its JSON Schemas, effects and command-line metadata, and a hash. The compiler and the SDK
|
|
521
|
+
* generators read it.
|
|
522
|
+
*/
|
|
523
|
+
export const manifestProcedure: Procedure<{ namespace?: string | undefined }, ProcedureManifest, ProcedureMetadata> = defineProcedure({
|
|
524
|
+
path: ["procedure", "manifest"],
|
|
525
|
+
input: zodAdapter(
|
|
526
|
+
z.object({ namespace: z.string().optional().describe("Only the procedures under this first path segment") }) as z.ZodType<{ namespace?: string | undefined }>,
|
|
527
|
+
),
|
|
528
|
+
output: ManifestOutputSchema,
|
|
529
|
+
metadata: {
|
|
530
|
+
description: "The manifest of the registry: each exposed procedure with its schemas and effects",
|
|
531
|
+
tags: ["core", "meta"],
|
|
532
|
+
args: [],
|
|
533
|
+
shorts: { namespace: "n" },
|
|
534
|
+
output: "json",
|
|
535
|
+
},
|
|
536
|
+
handler: async (input: { namespace?: string | undefined }, ctx?: ProcedureContext) =>
|
|
537
|
+
describeRegistry(ctx?.registry ?? PROCEDURE_REGISTRY, {
|
|
538
|
+
...(ctx?.expose ? { expose: ctx.expose } : {}),
|
|
539
|
+
...(input.namespace ? { include: (procedure: AnyProcedure) => procedure.path[0] === input.namespace } : {}),
|
|
540
|
+
}),
|
|
541
|
+
});
|
|
542
|
+
|
|
489
543
|
/**
|
|
490
544
|
* Delete a runtime-defined procedure.
|
|
491
545
|
*/
|
|
@@ -527,6 +581,10 @@ export const deleteProcedureProcedure: Procedure<
|
|
|
527
581
|
export const metaProcedures: AnyProcedure[] = [
|
|
528
582
|
// They read the registry. define and delete change it (CAPABILITIES-2026-10).
|
|
529
583
|
...withEffects({ reads: ["registry"], writes: ["registry"] }, [defineProcedureProcedure as AnyProcedure]),
|
|
530
|
-
...withEffects(REGISTRY_READ_EFFECTS, [
|
|
584
|
+
...withEffects(REGISTRY_READ_EFFECTS, [
|
|
585
|
+
getProcedureProcedure as AnyProcedure,
|
|
586
|
+
listProceduresProcedure as AnyProcedure,
|
|
587
|
+
manifestProcedure as AnyProcedure,
|
|
588
|
+
]),
|
|
531
589
|
...withEffects({ reads: ["registry"], writes: ["registry"] }, [deleteProcedureProcedure as AnyProcedure]),
|
|
532
590
|
];
|
|
@@ -11,6 +11,41 @@ export const PURE_EFFECTS: Effects = Object.freeze({ reads: Object.freeze([]), w
|
|
|
11
11
|
/** The effects of a procedure that reads the procedure registry and changes nothing. */
|
|
12
12
|
export const REGISTRY_READ_EFFECTS: Effects = Object.freeze({ reads: Object.freeze(["registry"]), writes: Object.freeze([]) });
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* The metadata key of the effects that a host found for the body of `procedure.define`
|
|
16
|
+
* (`programCheckInterceptor` of `@procedures/compiler`, roadmap R22). The key starts with `__`, so
|
|
17
|
+
* each transport removes it from a request: only an interceptor of the host can set it.
|
|
18
|
+
*/
|
|
19
|
+
export const INFERRED_EFFECTS_METADATA_KEY = "__inferredEffects";
|
|
20
|
+
|
|
21
|
+
function isNameList(value: unknown): value is string[] {
|
|
22
|
+
return Array.isArray(value) && value.every((name) => typeof name === "string");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The effects that a host found for a data-driven procedure (`metadata.inferredEffects`), or
|
|
27
|
+
* undefined. They are information for the tool hints and the explanations. The admission still
|
|
28
|
+
* examines each nested call of the procedure.
|
|
29
|
+
*/
|
|
30
|
+
export function inferredEffectsOf(source: { readonly metadata?: unknown } | Record<string, unknown> | undefined): Effects | undefined {
|
|
31
|
+
const metadata = source && "metadata" in source ? (source as { metadata?: unknown }).metadata : source;
|
|
32
|
+
const value = (metadata as { inferredEffects?: unknown } | undefined)?.inferredEffects;
|
|
33
|
+
return effectsValue(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** An `Effects` value with known reads and writes, or undefined. */
|
|
37
|
+
export function effectsValue(value: unknown): Effects | undefined {
|
|
38
|
+
if (value === null || typeof value !== "object") return undefined;
|
|
39
|
+
const { reads, writes, destructive, idempotent } = value as Record<string, unknown>;
|
|
40
|
+
if (!isNameList(reads) || !isNameList(writes)) return undefined;
|
|
41
|
+
return Object.freeze({
|
|
42
|
+
reads: Object.freeze([...reads]),
|
|
43
|
+
writes: Object.freeze([...writes]),
|
|
44
|
+
...(typeof destructive === "boolean" ? { destructive } : {}),
|
|
45
|
+
...(typeof idempotent === "boolean" ? { idempotent } : {}),
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
14
49
|
/**
|
|
15
50
|
* This function gives copies of the procedures with the effects in their metadata. A procedure
|
|
16
51
|
* that declares its own effects keeps them.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotency of calls (roadmap R1).
|
|
3
|
+
*
|
|
4
|
+
* The retry middleware gives each call one idempotency key, and each try of the call sends the
|
|
5
|
+
* same key. `ProcedureServer` keeps the result of a keyed call to a procedure that is not
|
|
6
|
+
* repeatable. A second try with the same key gets that result, and the handler does not run
|
|
7
|
+
* again. So a retry cannot do a write two times.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { Method } from "../client/types.js";
|
|
11
|
+
import { methodToPath } from "../server/method.js";
|
|
12
|
+
import type { AnyProcedure, Effects, ProcedurePath } from "./types.js";
|
|
13
|
+
import { optionalMs, type Duration } from "../duration.js";
|
|
14
|
+
import { TIMER_DEFAULTS } from "../timers.js";
|
|
15
|
+
|
|
16
|
+
// This file imports no module of the invocation, so the retry middleware stays small in a browser
|
|
17
|
+
|
|
18
|
+
/** The metadata key of the idempotency key of a call. Each try of one call sends the same key. */
|
|
19
|
+
export const IDEMPOTENCY_KEY_METADATA = "idempotency-key";
|
|
20
|
+
|
|
21
|
+
/** The longest idempotency key that a server accepts. A longer key is ignored. */
|
|
22
|
+
export const MAX_IDEMPOTENCY_KEY_LENGTH = 200;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* True when a second call with the same input changes nothing more: the procedure is idempotent,
|
|
26
|
+
* or it changes nothing (`writes: []`). Undeclared effects are not repeatable.
|
|
27
|
+
*/
|
|
28
|
+
export function isRepeatable(effects: Effects | undefined): boolean {
|
|
29
|
+
if (effects === undefined) return false;
|
|
30
|
+
if (effects.idempotent === true) return true;
|
|
31
|
+
return effects.writes !== undefined && effects.writes.length === 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* This function gives the `idempotent` option of the retry middleware from the procedures that
|
|
36
|
+
* the client knows: true for a repeatable procedure, false for another one, and undefined for a
|
|
37
|
+
* path that the registry does not have.
|
|
38
|
+
*/
|
|
39
|
+
export function idempotentIn(registry: { get(path: ProcedurePath): AnyProcedure | undefined }): (method: Method) => boolean | undefined {
|
|
40
|
+
return (method) => {
|
|
41
|
+
const procedure = registry.get(methodToPath(method));
|
|
42
|
+
if (procedure === undefined) return undefined;
|
|
43
|
+
const effects = (procedure.metadata as { effects?: Effects } | undefined)?.effects;
|
|
44
|
+
return isRepeatable(effects !== null && typeof effects === "object" ? effects : undefined);
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* This function gives the effects of the procedures that the client knows, by method: the
|
|
50
|
+
* `effects` option of the cache middleware (roadmap R13). A path that the registry does not have
|
|
51
|
+
* gives undefined.
|
|
52
|
+
*/
|
|
53
|
+
export function effectsIn(registry: { get(path: ProcedurePath): AnyProcedure | undefined }): (method: Method) => Effects | undefined {
|
|
54
|
+
return (method) => {
|
|
55
|
+
const effects = (registry.get(methodToPath(method))?.metadata as { effects?: Effects } | undefined)?.effects;
|
|
56
|
+
return effects !== null && typeof effects === "object" ? effects : undefined;
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** This function makes a new idempotency key: a random UUID. */
|
|
61
|
+
export function newIdempotencyKey(): string {
|
|
62
|
+
const random = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto?.randomUUID;
|
|
63
|
+
if (random) return random.call(globalThis.crypto);
|
|
64
|
+
// An older runtime without randomUUID: 128 random bits from Math.random
|
|
65
|
+
let key = "";
|
|
66
|
+
for (let index = 0; index < 4; index++) key += Math.floor(Math.random() * 0x100000000).toString(16).padStart(8, "0");
|
|
67
|
+
return key;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** The idempotency key of a call, from its metadata, or undefined. */
|
|
71
|
+
export function idempotencyKeyOf(metadata: Record<string, unknown> | undefined): string | undefined {
|
|
72
|
+
const key = metadata?.[IDEMPOTENCY_KEY_METADATA];
|
|
73
|
+
return typeof key === "string" && key.length > 0 && key.length <= MAX_IDEMPOTENCY_KEY_LENGTH ? key : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The options of the idempotency cache of a server. */
|
|
77
|
+
export interface IdempotencyOptions {
|
|
78
|
+
/** How long the server keeps a result after the call ends. The default is 10 minutes. */
|
|
79
|
+
readonly ttl?: Duration;
|
|
80
|
+
/** The most results that the server keeps. The oldest finished result goes first. The default is 1000. */
|
|
81
|
+
readonly maxEntries?: number;
|
|
82
|
+
/** The clock of the cache, in milliseconds. The default is `Date.now`. */
|
|
83
|
+
readonly now?: () => number;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
interface Entry<T> {
|
|
87
|
+
readonly done: Promise<T>;
|
|
88
|
+
/** The end time of the call, or undefined while it runs. */
|
|
89
|
+
settledAt: number | undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* The results of keyed calls. The first call with a key runs. A call with the same key while the
|
|
94
|
+
* first runs waits for it. A later call gets the same result until the time to live ends.
|
|
95
|
+
*/
|
|
96
|
+
export class IdempotencyCache<T> {
|
|
97
|
+
private readonly entries = new Map<string, Entry<T>>();
|
|
98
|
+
private readonly ttlMs: number;
|
|
99
|
+
private readonly maxEntries: number;
|
|
100
|
+
private readonly now: () => number;
|
|
101
|
+
|
|
102
|
+
constructor(options: IdempotencyOptions = {}) {
|
|
103
|
+
this.ttlMs = optionalMs(options.ttl, "ttl") ?? TIMER_DEFAULTS.idempotency.ttlMs;
|
|
104
|
+
this.maxEntries = options.maxEntries ?? 1000;
|
|
105
|
+
this.now = options.now ?? Date.now;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The number of results and calls that the cache holds. */
|
|
109
|
+
get size(): number {
|
|
110
|
+
return this.entries.size;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The result of the call with this key. `run` starts only for the first call. `keep` tells if
|
|
115
|
+
* the cache keeps a result: a result that is not kept lets the next call with the key run again.
|
|
116
|
+
*/
|
|
117
|
+
run(key: string, run: () => Promise<T>, keep: (result: T) => boolean): { result: Promise<T>; first: boolean } {
|
|
118
|
+
this.evict();
|
|
119
|
+
const existing = this.entries.get(key);
|
|
120
|
+
if (existing) return { result: existing.done, first: false };
|
|
121
|
+
const entry: Entry<T> = {
|
|
122
|
+
done: run().then(
|
|
123
|
+
(result) => {
|
|
124
|
+
if (keep(result)) entry.settledAt = this.now();
|
|
125
|
+
else if (this.entries.get(key) === entry) this.entries.delete(key);
|
|
126
|
+
return result;
|
|
127
|
+
},
|
|
128
|
+
(error: unknown) => {
|
|
129
|
+
if (this.entries.get(key) === entry) this.entries.delete(key);
|
|
130
|
+
throw error;
|
|
131
|
+
},
|
|
132
|
+
),
|
|
133
|
+
settledAt: undefined,
|
|
134
|
+
};
|
|
135
|
+
this.entries.set(key, entry);
|
|
136
|
+
return { result: entry.done, first: true };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** This function removes the expired results, and the oldest finished results above the limit. */
|
|
140
|
+
private evict(): void {
|
|
141
|
+
const now = this.now();
|
|
142
|
+
for (const [key, entry] of this.entries) {
|
|
143
|
+
if (entry.settledAt !== undefined && now - entry.settledAt >= this.ttlMs) this.entries.delete(key);
|
|
144
|
+
}
|
|
145
|
+
if (this.entries.size < this.maxEntries) return;
|
|
146
|
+
// A Map keeps the order of insertion, so the first finished entries are the oldest
|
|
147
|
+
for (const [key, entry] of this.entries) {
|
|
148
|
+
if (this.entries.size < this.maxEntries) break;
|
|
149
|
+
if (entry.settledAt !== undefined) this.entries.delete(key);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
package/src/procedures/index.ts
CHANGED
|
@@ -54,7 +54,22 @@ export {
|
|
|
54
54
|
explainInvocation,
|
|
55
55
|
checkEffects,
|
|
56
56
|
} from "./capability.js";
|
|
57
|
-
export { PURE_EFFECTS, REGISTRY_READ_EFFECTS, withEffects } from "./effects.js";
|
|
57
|
+
export { PURE_EFFECTS, REGISTRY_READ_EFFECTS, withEffects, INFERRED_EFFECTS_METADATA_KEY, inferredEffectsOf, effectsValue } from "./effects.js";
|
|
58
|
+
// One manifest type (roadmap R17)
|
|
59
|
+
export { MANIFEST_FORMAT, describeProcedure as describeProcedureEntry, describeRegistry, exposedProcedures, hashOf } from "./manifest.js";
|
|
60
|
+
export type { ProcedureEntry, ProcedureManifest, CliDescription, DescribeOptions, DescribeRegistryOptions } from "./manifest.js";
|
|
61
|
+
// Idempotency of calls (roadmap R1)
|
|
62
|
+
export {
|
|
63
|
+
IDEMPOTENCY_KEY_METADATA,
|
|
64
|
+
MAX_IDEMPOTENCY_KEY_LENGTH,
|
|
65
|
+
IdempotencyCache,
|
|
66
|
+
idempotencyKeyOf,
|
|
67
|
+
idempotentIn,
|
|
68
|
+
effectsIn,
|
|
69
|
+
isRepeatable,
|
|
70
|
+
newIdempotencyKey,
|
|
71
|
+
} from "./idempotency.js";
|
|
72
|
+
export type { IdempotencyOptions } from "./idempotency.js";
|
|
58
73
|
export type { ToolHints, CapabilityRefusal, CapabilityRefusalReason, InvocationExplanation, EffectsViolation } from "./capability.js";
|
|
59
74
|
|
|
60
75
|
// Registry
|
|
@@ -73,7 +88,7 @@ export type { ProcedureDefinition, ProcedureStub } from "./define.js";
|
|
|
73
88
|
|
|
74
89
|
// Collection procedures
|
|
75
90
|
export { createCollectionProcedures } from "./collection/index.js";
|
|
76
|
-
export type { CollectionStorage, StorageMetadata } from "./collection/index.js";
|
|
91
|
+
export type { CollectionStorage, StorageChanges, StorageMetadata } from "./collection/index.js";
|
|
77
92
|
|
|
78
93
|
// Manual registration helpers
|
|
79
94
|
export {
|
|
@@ -211,6 +226,7 @@ export type {
|
|
|
211
226
|
export {
|
|
212
227
|
defineProcedureProcedure,
|
|
213
228
|
getProcedureProcedure,
|
|
229
|
+
manifestProcedure,
|
|
214
230
|
listProceduresProcedure,
|
|
215
231
|
deleteProcedureProcedure,
|
|
216
232
|
metaProcedures,
|