@procedures/store 0.0.0-stage → 0.2.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 +30 -0
- package/LICENSE +21 -0
- package/README.md +65 -2
- package/dist/.tsbuildinfo +1 -0
- package/dist/errors.d.ts +16 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/module.d.ts +23 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +26 -0
- package/dist/module.js.map +1 -0
- package/dist/procedures.d.ts +13 -0
- package/dist/procedures.d.ts.map +1 -0
- package/dist/procedures.js +14 -0
- package/dist/procedures.js.map +1 -0
- package/dist/program-store/attach.d.ts +20 -0
- package/dist/program-store/attach.d.ts.map +1 -0
- package/dist/program-store/attach.js +36 -0
- package/dist/program-store/attach.js.map +1 -0
- package/dist/program-store/collection.d.ts +19 -0
- package/dist/program-store/collection.d.ts.map +1 -0
- package/dist/program-store/collection.js +34 -0
- package/dist/program-store/collection.js.map +1 -0
- package/dist/program-store/file.d.ts +29 -0
- package/dist/program-store/file.d.ts.map +1 -0
- package/dist/program-store/file.js +156 -0
- package/dist/program-store/file.js.map +1 -0
- package/dist/program-store/index.d.ts +11 -0
- package/dist/program-store/index.d.ts.map +1 -0
- package/dist/program-store/index.js +10 -0
- package/dist/program-store/index.js.map +1 -0
- package/dist/program-store/memory.d.ts +15 -0
- package/dist/program-store/memory.d.ts.map +1 -0
- package/dist/program-store/memory.js +25 -0
- package/dist/program-store/memory.js.map +1 -0
- package/dist/program-store/procedures.d.ts +23 -0
- package/dist/program-store/procedures.d.ts.map +1 -0
- package/dist/program-store/procedures.js +228 -0
- package/dist/program-store/procedures.js.map +1 -0
- package/dist/program-store/types.d.ts +50 -0
- package/dist/program-store/types.d.ts.map +1 -0
- package/dist/program-store/types.js +41 -0
- package/dist/program-store/types.js.map +1 -0
- package/dist/register.d.ts +7 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +9 -0
- package/dist/register.js.map +1 -0
- package/dist/storage/adapter.d.ts +148 -0
- package/dist/storage/adapter.d.ts.map +1 -0
- package/dist/storage/adapter.js +214 -0
- package/dist/storage/adapter.js.map +1 -0
- package/dist/storage/api.d.ts +125 -0
- package/dist/storage/api.d.ts.map +1 -0
- package/dist/storage/api.js +220 -0
- package/dist/storage/api.js.map +1 -0
- package/dist/storage/factory.d.ts +134 -0
- package/dist/storage/factory.d.ts.map +1 -0
- package/dist/storage/factory.js +166 -0
- package/dist/storage/factory.js.map +1 -0
- package/dist/storage/hybrid.d.ts +193 -0
- package/dist/storage/hybrid.d.ts.map +1 -0
- package/dist/storage/hybrid.js +519 -0
- package/dist/storage/hybrid.js.map +1 -0
- package/dist/storage/index.d.ts +21 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +20 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/procedures.d.ts +14 -0
- package/dist/storage/procedures.d.ts.map +1 -0
- package/dist/storage/procedures.js +101 -0
- package/dist/storage/procedures.js.map +1 -0
- package/dist/storage/serialization.d.ts +123 -0
- package/dist/storage/serialization.d.ts.map +1 -0
- package/dist/storage/serialization.js +216 -0
- package/dist/storage/serialization.js.map +1 -0
- package/dist/storage/synced-registry.d.ts +246 -0
- package/dist/storage/synced-registry.d.ts.map +1 -0
- package/dist/storage/synced-registry.js +575 -0
- package/dist/storage/synced-registry.js.map +1 -0
- package/dist/storage/types.d.ts +130 -0
- package/dist/storage/types.d.ts.map +1 -0
- package/dist/storage/types.js +8 -0
- package/dist/storage/types.js.map +1 -0
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/package.json +78 -3
- package/src/errors.ts +20 -0
- package/src/index.ts +19 -0
- package/src/module.ts +30 -0
- package/src/procedures.ts +17 -0
- package/src/program-store/attach.ts +46 -0
- package/src/program-store/collection.ts +39 -0
- package/src/program-store/file.ts +157 -0
- package/src/program-store/index.ts +17 -0
- package/src/program-store/memory.ts +30 -0
- package/src/program-store/procedures.ts +351 -0
- package/src/program-store/types.ts +78 -0
- package/src/register.ts +11 -0
- package/src/storage/adapter.ts +273 -0
- package/src/storage/api.ts +335 -0
- package/src/storage/factory.ts +268 -0
- package/src/storage/hybrid.ts +676 -0
- package/src/storage/index.ts +61 -0
- package/src/storage/procedures.ts +149 -0
- package/src/storage/serialization.ts +283 -0
- package/src/storage/synced-registry.ts +698 -0
- package/src/storage/types.ts +180 -0
- package/src/types.ts +7 -0
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The procedures of the program store (roadmap 3.2):
|
|
3
|
+
*
|
|
4
|
+
* - `procedure.store`: write the program of a runtime-defined procedure to the store.
|
|
5
|
+
* - `procedure.load`: define the stored programs again, through `procedure.define`.
|
|
6
|
+
* - `procedure.sync`: push the programs of the registry, pull the stored ones, or both.
|
|
7
|
+
* - `procedure.remote`: the status of the store. A store's backend is fixed when a host makes
|
|
8
|
+
* it, so `connect` and `disconnect` are NOT_IMPLEMENTED: a procedure that could point the
|
|
9
|
+
* store at a new endpoint would give a caller network reach.
|
|
10
|
+
*
|
|
11
|
+
* They use the program store of the caller's registry (`attachProgramStore`). A loaded program
|
|
12
|
+
* is a runtime-defined procedure again: data-driven, so a server with an expose rule lets it
|
|
13
|
+
* call only the exposed procedures. Load programs only from a store that you trust: a host with
|
|
14
|
+
* no expose rule runs whatever a program calls.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { z } from "zod";
|
|
18
|
+
import {
|
|
19
|
+
createProcedure,
|
|
20
|
+
getAllRuntimeProcedures,
|
|
21
|
+
keyToPath,
|
|
22
|
+
outputSchema,
|
|
23
|
+
pathToKey,
|
|
24
|
+
runtimeProgramOf,
|
|
25
|
+
zodAdapter,
|
|
26
|
+
type AnyProcedure,
|
|
27
|
+
type ProcedureContext,
|
|
28
|
+
type ProcedurePath,
|
|
29
|
+
} from "@procedures/client";
|
|
30
|
+
import { ProcedureStoreError } from "../errors.js";
|
|
31
|
+
import { callerRegistry, getProgramStore, requireProgramStore } from "./attach.js";
|
|
32
|
+
import type { ProgramStore, StoredProgram } from "./types.js";
|
|
33
|
+
|
|
34
|
+
// =============================================================================
|
|
35
|
+
// Shared
|
|
36
|
+
// =============================================================================
|
|
37
|
+
|
|
38
|
+
const pathField = z
|
|
39
|
+
.union([z.array(z.string().min(1)).min(1), z.string().min(1)])
|
|
40
|
+
.describe('The path of the procedure: a list of segments, or a dotted key such as "reports.weekly"');
|
|
41
|
+
|
|
42
|
+
function toPath(path: ProcedurePath | string): ProcedurePath {
|
|
43
|
+
return typeof path === "string" ? keyToPath(path) : path;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The metadata to store: the procedure's metadata without the program (the store keeps that). */
|
|
47
|
+
function storedMetadata(metadata: Record<string, unknown> | undefined): Record<string, unknown> | undefined {
|
|
48
|
+
if (!metadata) return undefined;
|
|
49
|
+
const { aggregation: _aggregation, generatedFrom: _generatedFrom, ...rest } = metadata;
|
|
50
|
+
return Object.keys(rest).length > 0 ? rest : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The record of a runtime-defined procedure, or a NOT_A_PROGRAM error for a code procedure. */
|
|
54
|
+
function recordOf(ctx: ProcedureContext, path: ProcedurePath): StoredProgram {
|
|
55
|
+
const key = pathToKey(path);
|
|
56
|
+
const procedure = callerRegistry(ctx).get(path);
|
|
57
|
+
if (!procedure) {
|
|
58
|
+
throw new Error(`Procedure not found: ${key}`);
|
|
59
|
+
}
|
|
60
|
+
const program = runtimeProgramOf(procedure);
|
|
61
|
+
if (!program) {
|
|
62
|
+
throw new ProcedureStoreError(
|
|
63
|
+
"NOT_A_PROGRAM",
|
|
64
|
+
`${key} is a code procedure. Only a procedure that procedure.define made is a program: code lives in a package.`
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
return { key, path: [...procedure.path], program, metadata: storedMetadata(procedure.metadata), storedAt: Date.now() };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface LoadResult {
|
|
71
|
+
loaded: number;
|
|
72
|
+
conflicts: Array<{ path: ProcedurePath; reason: string }>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Define each program again in the caller's registry, through procedure.define. */
|
|
76
|
+
async function definePrograms(ctx: ProcedureContext, programs: StoredProgram[]): Promise<LoadResult> {
|
|
77
|
+
const result: LoadResult = { loaded: 0, conflicts: [] };
|
|
78
|
+
for (const stored of programs) {
|
|
79
|
+
try {
|
|
80
|
+
await ctx.client.call(["procedure", "define"], {
|
|
81
|
+
path: stored.path,
|
|
82
|
+
aggregation: stored.program,
|
|
83
|
+
...(stored.metadata ? { metadata: stored.metadata } : {}),
|
|
84
|
+
// A stored program replaces a runtime-defined procedure. procedure.define never replaces
|
|
85
|
+
// a code procedure: that is a conflict.
|
|
86
|
+
replace: true,
|
|
87
|
+
});
|
|
88
|
+
result.loaded++;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
result.conflicts.push({ path: stored.path, reason: error instanceof Error ? error.message : String(error) });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// =============================================================================
|
|
97
|
+
// procedure.store
|
|
98
|
+
// =============================================================================
|
|
99
|
+
|
|
100
|
+
interface StoreInput {
|
|
101
|
+
path: ProcedurePath | string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
interface StoreOutput {
|
|
105
|
+
stored: true;
|
|
106
|
+
path: ProcedurePath;
|
|
107
|
+
storedAt: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const procedureStoreProcedure: AnyProcedure = createProcedure()
|
|
111
|
+
.path(["procedure", "store"])
|
|
112
|
+
.input(zodAdapter<StoreInput>(z.object({ path: pathField })))
|
|
113
|
+
.output(
|
|
114
|
+
zodAdapter<StoreOutput>(
|
|
115
|
+
z.object({
|
|
116
|
+
stored: z.literal(true).describe("True: the store has the program"),
|
|
117
|
+
path: z.array(z.string()).describe("The path of the procedure"),
|
|
118
|
+
storedAt: z.number().describe("When the store got the program, in milliseconds since 1970"),
|
|
119
|
+
})
|
|
120
|
+
)
|
|
121
|
+
)
|
|
122
|
+
.meta({
|
|
123
|
+
description: "Write the program of a procedure that procedure.define made to the program store",
|
|
124
|
+
tags: ["procedure", "registry", "storage"],
|
|
125
|
+
// It replaces a stored program of the same key. A FileProgramStore keeps the programs in a file.
|
|
126
|
+
effects: { reads: ["registry"], writes: ["fs", "programs"] },
|
|
127
|
+
args: ["path"],
|
|
128
|
+
})
|
|
129
|
+
.handler(async (input: StoreInput, ctx: ProcedureContext): Promise<StoreOutput> => {
|
|
130
|
+
const store = requireProgramStore(ctx, "procedure.store");
|
|
131
|
+
const record = recordOf(ctx, toPath(input.path));
|
|
132
|
+
await store.put(record);
|
|
133
|
+
return { stored: true, path: record.path, storedAt: record.storedAt };
|
|
134
|
+
})
|
|
135
|
+
.build();
|
|
136
|
+
|
|
137
|
+
// =============================================================================
|
|
138
|
+
// procedure.load
|
|
139
|
+
// =============================================================================
|
|
140
|
+
|
|
141
|
+
interface LoadInput {
|
|
142
|
+
path?: ProcedurePath | string | undefined;
|
|
143
|
+
prefix?: ProcedurePath | string | undefined;
|
|
144
|
+
all?: boolean | undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
interface LoadOutput {
|
|
148
|
+
/** The number of programs that the load defined */
|
|
149
|
+
loaded: number;
|
|
150
|
+
/** The paths of the stored programs that matched */
|
|
151
|
+
paths: ProcedurePath[];
|
|
152
|
+
/** The programs that the load could not define (a code procedure at the path, an invalid program) */
|
|
153
|
+
conflicts: Array<{ path: ProcedurePath; reason: string }>;
|
|
154
|
+
message: string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const conflictsField = z
|
|
158
|
+
.array(z.object({ path: z.array(z.string()), reason: z.string() }))
|
|
159
|
+
.describe("The programs that could not be defined, with the reason");
|
|
160
|
+
|
|
161
|
+
export const procedureLoadProcedure: AnyProcedure = createProcedure()
|
|
162
|
+
.path(["procedure", "load"])
|
|
163
|
+
.input(
|
|
164
|
+
zodAdapter<LoadInput>(
|
|
165
|
+
z.object({
|
|
166
|
+
path: pathField.optional(),
|
|
167
|
+
prefix: pathField.optional().describe("Load the programs of this path and under it"),
|
|
168
|
+
all: z.boolean().optional().describe("Load every stored program"),
|
|
169
|
+
})
|
|
170
|
+
)
|
|
171
|
+
)
|
|
172
|
+
.output(
|
|
173
|
+
zodAdapter<LoadOutput>(
|
|
174
|
+
z.object({
|
|
175
|
+
loaded: z.number().describe("The number of programs that the load defined"),
|
|
176
|
+
paths: z.array(z.array(z.string())).describe("The paths of the stored programs that matched"),
|
|
177
|
+
conflicts: conflictsField,
|
|
178
|
+
message: z.string(),
|
|
179
|
+
})
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
.meta({
|
|
183
|
+
description: "Define the stored programs again (procedure.define), after a restart",
|
|
184
|
+
tags: ["procedure", "registry", "storage"],
|
|
185
|
+
calls: ["procedure.define"],
|
|
186
|
+
// A loaded program replaces a runtime-defined procedure of the same path
|
|
187
|
+
effects: { reads: ["fs", "programs", "registry"], writes: ["registry"] },
|
|
188
|
+
})
|
|
189
|
+
.handler(async (input: LoadInput, ctx: ProcedureContext): Promise<LoadOutput> => {
|
|
190
|
+
const chosen = [input.path !== undefined, input.prefix !== undefined, input.all === true].filter(Boolean).length;
|
|
191
|
+
if (chosen !== 1) {
|
|
192
|
+
throw new Error("procedure.load needs exactly one of `path`, `prefix` or `all: true`");
|
|
193
|
+
}
|
|
194
|
+
const store = requireProgramStore(ctx, "procedure.load");
|
|
195
|
+
let programs: StoredProgram[];
|
|
196
|
+
let what: string;
|
|
197
|
+
if (input.path !== undefined) {
|
|
198
|
+
what = pathToKey(toPath(input.path));
|
|
199
|
+
const program = await store.get(what);
|
|
200
|
+
programs = program ? [program] : [];
|
|
201
|
+
} else if (input.prefix !== undefined) {
|
|
202
|
+
const prefix = pathToKey(toPath(input.prefix));
|
|
203
|
+
what = `${prefix}.*`;
|
|
204
|
+
programs = await store.list(prefix);
|
|
205
|
+
} else {
|
|
206
|
+
what = "all programs";
|
|
207
|
+
programs = await store.list();
|
|
208
|
+
}
|
|
209
|
+
const result = await definePrograms(ctx, programs);
|
|
210
|
+
return {
|
|
211
|
+
loaded: result.loaded,
|
|
212
|
+
paths: programs.map((p) => p.path),
|
|
213
|
+
conflicts: result.conflicts,
|
|
214
|
+
message: `Loaded ${result.loaded} of ${programs.length} stored program(s) for ${what}`,
|
|
215
|
+
};
|
|
216
|
+
})
|
|
217
|
+
.build();
|
|
218
|
+
|
|
219
|
+
// =============================================================================
|
|
220
|
+
// procedure.sync
|
|
221
|
+
// =============================================================================
|
|
222
|
+
|
|
223
|
+
interface SyncInput {
|
|
224
|
+
direction?: "push" | "pull" | "both" | undefined;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
interface SyncOutput {
|
|
228
|
+
pushed: number;
|
|
229
|
+
pulled: number;
|
|
230
|
+
conflicts: Array<{ path: ProcedurePath; reason: string }>;
|
|
231
|
+
message: string;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Write each runtime-defined procedure of the caller's registry to the store. */
|
|
235
|
+
async function push(ctx: ProcedureContext, store: ProgramStore): Promise<number> {
|
|
236
|
+
let pushed = 0;
|
|
237
|
+
for (const procedure of getAllRuntimeProcedures(callerRegistry(ctx))) {
|
|
238
|
+
await store.put(recordOf(ctx, procedure.path));
|
|
239
|
+
pushed++;
|
|
240
|
+
}
|
|
241
|
+
return pushed;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export const procedureSyncProcedure: AnyProcedure = createProcedure()
|
|
245
|
+
.path(["procedure", "sync"])
|
|
246
|
+
.input(
|
|
247
|
+
zodAdapter<SyncInput>(
|
|
248
|
+
z.object({
|
|
249
|
+
direction: z
|
|
250
|
+
.enum(["push", "pull", "both"])
|
|
251
|
+
.optional()
|
|
252
|
+
.describe(
|
|
253
|
+
"push: write the programs of the registry to the store. pull: define the stored programs. " +
|
|
254
|
+
"both (the default): push, then pull the stored programs that the registry does not have"
|
|
255
|
+
),
|
|
256
|
+
})
|
|
257
|
+
)
|
|
258
|
+
)
|
|
259
|
+
.output(
|
|
260
|
+
zodAdapter<SyncOutput>(
|
|
261
|
+
z.object({
|
|
262
|
+
pushed: z.number().describe("The number of programs written to the store"),
|
|
263
|
+
pulled: z.number().describe("The number of stored programs defined in the registry"),
|
|
264
|
+
conflicts: conflictsField,
|
|
265
|
+
message: z.string(),
|
|
266
|
+
})
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
.meta({
|
|
270
|
+
description: "Synchronize the runtime-defined procedures of the registry with the program store",
|
|
271
|
+
tags: ["procedure", "registry", "storage"],
|
|
272
|
+
calls: ["procedure.define"],
|
|
273
|
+
effects: { reads: ["fs", "programs", "registry"], writes: ["fs", "programs", "registry"] },
|
|
274
|
+
})
|
|
275
|
+
.handler(async (input: SyncInput, ctx: ProcedureContext): Promise<SyncOutput> => {
|
|
276
|
+
const direction = input.direction ?? "both";
|
|
277
|
+
const store = requireProgramStore(ctx, "procedure.sync");
|
|
278
|
+
const pushed = direction === "pull" ? 0 : await push(ctx, store);
|
|
279
|
+
let pulled = 0;
|
|
280
|
+
let conflicts: SyncOutput["conflicts"] = [];
|
|
281
|
+
if (direction !== "push") {
|
|
282
|
+
const registry = callerRegistry(ctx);
|
|
283
|
+
const stored = await store.list();
|
|
284
|
+
// With "both", the registry wins: a program that it has was pushed, and is not pulled back
|
|
285
|
+
const toPull = direction === "pull" ? stored : stored.filter((p) => !registry.get(p.path));
|
|
286
|
+
const result = await definePrograms(ctx, toPull);
|
|
287
|
+
pulled = result.loaded;
|
|
288
|
+
conflicts = result.conflicts;
|
|
289
|
+
}
|
|
290
|
+
return { pushed, pulled, conflicts, message: `Sync ${direction}: pushed ${pushed}, pulled ${pulled}` };
|
|
291
|
+
})
|
|
292
|
+
.build();
|
|
293
|
+
|
|
294
|
+
// =============================================================================
|
|
295
|
+
// procedure.remote
|
|
296
|
+
// =============================================================================
|
|
297
|
+
|
|
298
|
+
interface RemoteInput {
|
|
299
|
+
action: "status" | "connect" | "disconnect";
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
interface RemoteOutput {
|
|
303
|
+
/** True when the caller's registry has a program store */
|
|
304
|
+
configured: boolean;
|
|
305
|
+
/** The kind of the store: memory, file, collection */
|
|
306
|
+
kind?: string | undefined;
|
|
307
|
+
message: string;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export const procedureRemoteProcedure: AnyProcedure = createProcedure()
|
|
311
|
+
.path(["procedure", "remote"])
|
|
312
|
+
.input(
|
|
313
|
+
zodAdapter<RemoteInput>(
|
|
314
|
+
z.object({
|
|
315
|
+
action: z
|
|
316
|
+
.enum(["status", "connect", "disconnect"])
|
|
317
|
+
.describe("status: the store of the registry. connect and disconnect: not implemented (the host makes the store)"),
|
|
318
|
+
})
|
|
319
|
+
)
|
|
320
|
+
)
|
|
321
|
+
.output(outputSchema<RemoteOutput>())
|
|
322
|
+
.meta({
|
|
323
|
+
description: "The status of the program store of the registry",
|
|
324
|
+
tags: ["procedure", "registry", "storage"],
|
|
325
|
+
// It gives the kind of the store. `connect` and `disconnect` change nothing: they fail.
|
|
326
|
+
effects: { reads: ["programs"], writes: [] },
|
|
327
|
+
args: ["action"],
|
|
328
|
+
})
|
|
329
|
+
.handler(async (input: RemoteInput, ctx: ProcedureContext): Promise<RemoteOutput> => {
|
|
330
|
+
if (input.action === "status") {
|
|
331
|
+
const store = getProgramStore(callerRegistry(ctx));
|
|
332
|
+
return store
|
|
333
|
+
? { configured: true, kind: store.kind, message: `The registry has a ${store.kind} program store` }
|
|
334
|
+
: { configured: false, message: "The registry has no program store" };
|
|
335
|
+
}
|
|
336
|
+
throw new ProcedureStoreError(
|
|
337
|
+
"NOT_IMPLEMENTED",
|
|
338
|
+
`procedure.remote ${input.action} cannot change the store of a registry at run time. A host makes the store, ` +
|
|
339
|
+
"for example new CollectionProgramStore(new ApiStorage(client, { collection: \"programs\" })), and gives it " +
|
|
340
|
+
"to the registry with attachProgramStore()."
|
|
341
|
+
);
|
|
342
|
+
})
|
|
343
|
+
.build();
|
|
344
|
+
|
|
345
|
+
/** The procedures of the program store. */
|
|
346
|
+
export const programStoreProcedures: AnyProcedure[] = [
|
|
347
|
+
procedureStoreProcedure,
|
|
348
|
+
procedureLoadProcedure,
|
|
349
|
+
procedureSyncProcedure,
|
|
350
|
+
procedureRemoteProcedure,
|
|
351
|
+
];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The types of the program store.
|
|
3
|
+
*
|
|
4
|
+
* A program is procedure-as-data: the aggregation that `procedure.define` turns into a
|
|
5
|
+
* procedure. A program store keeps programs after the process ends. `procedure.load` defines
|
|
6
|
+
* them again, so a stored program is a runtime-defined procedure again: a data-driven one, which
|
|
7
|
+
* a server with an expose rule lets call only the exposed procedures.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { AggregationDefinition, ProcedurePath } from "@procedures/client";
|
|
11
|
+
|
|
12
|
+
/** One stored program. */
|
|
13
|
+
export interface StoredProgram {
|
|
14
|
+
/** The dotted key of the path: `reports.weekly`. */
|
|
15
|
+
key: string;
|
|
16
|
+
/** The path of the procedure. */
|
|
17
|
+
path: ProcedurePath;
|
|
18
|
+
/** The program: the aggregation of `procedure.define`. */
|
|
19
|
+
program: AggregationDefinition;
|
|
20
|
+
/** The metadata of the procedure (its description, its tags), without the program. */
|
|
21
|
+
metadata?: Record<string, unknown> | undefined;
|
|
22
|
+
/** When the store got this version, in milliseconds since 1970. */
|
|
23
|
+
storedAt: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A store of programs. Each method gives a Promise: a caller cannot tell a local store from a
|
|
28
|
+
* remote one.
|
|
29
|
+
*/
|
|
30
|
+
export interface ProgramStore {
|
|
31
|
+
/** The kind of the backend: "memory", "file", "collection" or another name. */
|
|
32
|
+
readonly kind: string;
|
|
33
|
+
/** The program of a key, or undefined. */
|
|
34
|
+
get(key: string): Promise<StoredProgram | undefined>;
|
|
35
|
+
/** The programs, sorted by key. With a prefix, only the key and the keys under it (`prefix.x`). */
|
|
36
|
+
list(prefix?: string): Promise<StoredProgram[]>;
|
|
37
|
+
/** Write a program. A program of the same key is replaced. */
|
|
38
|
+
put(program: StoredProgram): Promise<void>;
|
|
39
|
+
/** Delete the program of a key. True when it existed. */
|
|
40
|
+
delete(key: string): Promise<boolean>;
|
|
41
|
+
/** Release the resources of the store. */
|
|
42
|
+
close?(): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** True when a key is the prefix or under it (`a.b` for the prefix `a`). */
|
|
46
|
+
export function keyMatches(key: string, prefix: string | undefined): boolean {
|
|
47
|
+
return prefix === undefined || prefix === "" || key === prefix || key.startsWith(`${prefix}.`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Programs sorted by key, so a listing is the same for each backend. */
|
|
51
|
+
export function byKey(programs: StoredProgram[]): StoredProgram[] {
|
|
52
|
+
return programs.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The record to store, checked: a stored program comes back from a file or a remote service, so
|
|
57
|
+
* its shape is not trusted.
|
|
58
|
+
*/
|
|
59
|
+
export function checkStoredProgram(value: unknown): StoredProgram {
|
|
60
|
+
if (typeof value !== "object" || value === null) {
|
|
61
|
+
throw new TypeError("A stored program must be an object");
|
|
62
|
+
}
|
|
63
|
+
const record = value as Partial<StoredProgram>;
|
|
64
|
+
if (!Array.isArray(record.path) || record.path.length === 0 || !record.path.every((s) => typeof s === "string" && s !== "")) {
|
|
65
|
+
throw new TypeError("A stored program needs a path: a list of non-empty strings");
|
|
66
|
+
}
|
|
67
|
+
if (record.key !== record.path.join(".")) {
|
|
68
|
+
throw new TypeError(`The key of a stored program must be its path with dots: ${record.path.join(".")}`);
|
|
69
|
+
}
|
|
70
|
+
const program = record.program as { $proc?: unknown } | undefined;
|
|
71
|
+
if (typeof program !== "object" || program === null || !Array.isArray(program.$proc)) {
|
|
72
|
+
throw new TypeError(`The program of ${record.key} must be a procedure ref: { "$proc": [...], "input": ... }`);
|
|
73
|
+
}
|
|
74
|
+
if (typeof record.storedAt !== "number" || !Number.isFinite(record.storedAt)) {
|
|
75
|
+
throw new TypeError(`The program of ${record.key} needs storedAt, a time in milliseconds`);
|
|
76
|
+
}
|
|
77
|
+
return record as StoredProgram;
|
|
78
|
+
}
|
package/src/register.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Importing this file registers the procedures of @procedures/store, and of the modules that it
|
|
3
|
+
* requires, in the global registry: `import "@procedures/store/register"`. The package root and the
|
|
4
|
+
* `./module` entry register nothing (ARCHITECTURE-PROPOSALS-2026-10, P9).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { registerStoreProcedures } from "./module.js";
|
|
8
|
+
|
|
9
|
+
export * from "./module.js";
|
|
10
|
+
|
|
11
|
+
registerStoreProcedures();
|