@procedures/store 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -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 +15 -0
- package/dist/module.d.ts.map +1 -0
- package/dist/module.js +16 -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 +221 -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 +8 -0
- package/dist/register.d.ts.map +1 -0
- package/dist/register.js +15 -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 +98 -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 +79 -3
- package/src/errors.ts +20 -0
- package/src/index.ts +19 -0
- package/src/module.ts +18 -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 +344 -0
- package/src/program-store/types.ts +78 -0
- package/src/register.ts +18 -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 +146 -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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Procedure Storage Types
|
|
3
|
+
*
|
|
4
|
+
* Types for serializing and persisting procedure definitions.
|
|
5
|
+
* Enables procedure registry to be backed by CollectionStorage.
|
|
6
|
+
*/
|
|
7
|
+
import type { ProcedurePath, ProcedureMetadata } from "@procedures/client";
|
|
8
|
+
/**
|
|
9
|
+
* Reference to a handler that can be loaded at runtime.
|
|
10
|
+
* Handlers cannot be serialized directly, so we store a reference.
|
|
11
|
+
*/
|
|
12
|
+
export interface HandlerReference {
|
|
13
|
+
/** Module name or path (e.g., "@procedures/cli-run") */
|
|
14
|
+
module: string;
|
|
15
|
+
/** Named export from the module (e.g., "libScanHandler") */
|
|
16
|
+
export: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Serialized procedure definition for storage.
|
|
20
|
+
* Contains only JSON-serializable data.
|
|
21
|
+
*/
|
|
22
|
+
export interface SerializedProcedure {
|
|
23
|
+
/** Procedure path (e.g., ['lib', 'scan']) */
|
|
24
|
+
path: ProcedurePath;
|
|
25
|
+
/** Procedure metadata */
|
|
26
|
+
metadata: ProcedureMetadata;
|
|
27
|
+
/** Whether this procedure supports streaming */
|
|
28
|
+
streaming?: boolean | undefined;
|
|
29
|
+
/** Reference to handler for runtime loading (optional) */
|
|
30
|
+
handlerRef?: HandlerReference | undefined;
|
|
31
|
+
/** Timestamp when stored */
|
|
32
|
+
storedAt?: number | undefined;
|
|
33
|
+
/** Version for conflict detection */
|
|
34
|
+
version?: number | undefined;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Function to load a handler from a reference.
|
|
38
|
+
* Used to restore handlers when loading procedures from storage.
|
|
39
|
+
*/
|
|
40
|
+
export interface HandlerLoader {
|
|
41
|
+
/**
|
|
42
|
+
* Load a handler from a reference.
|
|
43
|
+
*
|
|
44
|
+
* @param ref - Handler reference with module and export name
|
|
45
|
+
* @returns The handler function, or undefined if not found
|
|
46
|
+
*/
|
|
47
|
+
load(ref: HandlerReference): Promise<unknown | undefined>;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Sync direction for registry operations.
|
|
51
|
+
*/
|
|
52
|
+
export type SyncDirection = "push" | "pull" | "both";
|
|
53
|
+
/**
|
|
54
|
+
* Conflict resolution strategy.
|
|
55
|
+
*/
|
|
56
|
+
export type ConflictResolution = "local" | "remote" | "merge" | "error";
|
|
57
|
+
/**
|
|
58
|
+
* Write strategy for persistence.
|
|
59
|
+
*/
|
|
60
|
+
export type WriteStrategy = "write-through" | "write-back";
|
|
61
|
+
/**
|
|
62
|
+
* Status of a sync conflict.
|
|
63
|
+
*/
|
|
64
|
+
export interface SyncConflict {
|
|
65
|
+
/** Procedure path that had conflict */
|
|
66
|
+
path: ProcedurePath;
|
|
67
|
+
/** How the conflict was resolved */
|
|
68
|
+
resolution: ConflictResolution;
|
|
69
|
+
/** Local version (if any) */
|
|
70
|
+
local?: SerializedProcedure;
|
|
71
|
+
/** Remote version (if any) */
|
|
72
|
+
remote?: SerializedProcedure;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Result of a sync operation.
|
|
76
|
+
*/
|
|
77
|
+
export interface SyncResult {
|
|
78
|
+
/** Number of procedures pushed to storage */
|
|
79
|
+
pushed: number;
|
|
80
|
+
/** Number of procedures pulled from storage */
|
|
81
|
+
pulled: number;
|
|
82
|
+
/** Conflicts encountered during sync */
|
|
83
|
+
conflicts: SyncConflict[];
|
|
84
|
+
/** Timestamp of sync completion */
|
|
85
|
+
timestamp: number;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Current sync status of the registry.
|
|
89
|
+
*/
|
|
90
|
+
export interface SyncStatus {
|
|
91
|
+
/** Whether connected to remote storage */
|
|
92
|
+
connected: boolean;
|
|
93
|
+
/** Remote endpoint URL (if connected) */
|
|
94
|
+
endpoint?: string | undefined;
|
|
95
|
+
/** Last successful sync timestamp */
|
|
96
|
+
lastSync?: number | undefined;
|
|
97
|
+
/** Number of pending local changes */
|
|
98
|
+
pendingChanges: number;
|
|
99
|
+
/** Whether currently syncing */
|
|
100
|
+
syncing: boolean;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Options for SyncedProcedureRegistry.
|
|
104
|
+
*/
|
|
105
|
+
export interface SyncedRegistryOptions {
|
|
106
|
+
/** Write strategy: write-through (immediate) or write-back (queued) */
|
|
107
|
+
writeStrategy: WriteStrategy;
|
|
108
|
+
/** How to resolve conflicts when loading from storage */
|
|
109
|
+
conflictResolution: ConflictResolution;
|
|
110
|
+
/** Auto-sync interval in milliseconds (0 to disable) */
|
|
111
|
+
syncInterval?: number | undefined;
|
|
112
|
+
/** Handler loader for restoring handlers from references */
|
|
113
|
+
handlerLoader?: HandlerLoader | undefined;
|
|
114
|
+
/** Whether to sync on initialization */
|
|
115
|
+
syncOnInit?: boolean | undefined;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Configuration for creating a storage-backed registry.
|
|
119
|
+
*/
|
|
120
|
+
export interface ProcedureStorageConfig {
|
|
121
|
+
/** Storage type to use */
|
|
122
|
+
type: "memory" | "api" | "hybrid";
|
|
123
|
+
/** The collection of the core collection procedures for API storage (default: "procedures") */
|
|
124
|
+
collection?: string | undefined;
|
|
125
|
+
/** The service of the calls, for a server with other paths (default: `collections.<collection>`) */
|
|
126
|
+
service?: string | undefined;
|
|
127
|
+
/** Options for the synced registry */
|
|
128
|
+
options?: Partial<SyncedRegistryOptions> | undefined;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/storage/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,82 @@
|
|
|
1
1
|
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package",
|
|
2
3
|
"name": "@procedures/store",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
4
|
+
"version": "0.1.0",
|
|
5
|
+
"description": "A program store for procedure-as-data: save the programs that procedure.define makes, load them back after a restart, in memory, a JSON file or a remote collection storage.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"program-store",
|
|
8
|
+
"procedures",
|
|
9
|
+
"json-programs",
|
|
10
|
+
"persistence",
|
|
11
|
+
"registry",
|
|
12
|
+
"rpc"
|
|
13
|
+
],
|
|
14
|
+
"homepage": "https://mark1russell7.github.io/client/",
|
|
15
|
+
"bugs": {
|
|
16
|
+
"url": "https://github.com/mark1russell7/client/issues"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/mark1russell7/client.git",
|
|
21
|
+
"directory": "packages/client-store"
|
|
22
|
+
},
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"author": "Mark Russell (https://github.com/mark1russell7)",
|
|
25
|
+
"type": "module",
|
|
26
|
+
"main": "./dist/index.js",
|
|
27
|
+
"types": "./dist/index.d.ts",
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"import": "./dist/index.js"
|
|
32
|
+
},
|
|
33
|
+
"./module": {
|
|
34
|
+
"types": "./dist/module.d.ts",
|
|
35
|
+
"import": "./dist/module.js"
|
|
36
|
+
},
|
|
37
|
+
"./register": {
|
|
38
|
+
"types": "./dist/register.d.ts",
|
|
39
|
+
"import": "./dist/register.js"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"dist",
|
|
44
|
+
"src",
|
|
45
|
+
"!src/**/*.test.ts",
|
|
46
|
+
"!src/**/*.test.tsx",
|
|
47
|
+
"!src/**/__tests__",
|
|
48
|
+
"CHANGELOG.md"
|
|
49
|
+
],
|
|
50
|
+
"sideEffects": [
|
|
51
|
+
"./dist/index.js",
|
|
52
|
+
"./dist/register.js"
|
|
53
|
+
],
|
|
54
|
+
"engines": {
|
|
55
|
+
"node": ">=22"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public",
|
|
59
|
+
"provenance": true
|
|
60
|
+
},
|
|
61
|
+
"client": {
|
|
62
|
+
"procedures": "./dist/register.js"
|
|
63
|
+
},
|
|
64
|
+
"dependencies": {
|
|
65
|
+
"@procedures/client": "0.1.0",
|
|
66
|
+
"@procedures/collections": "0.1.0",
|
|
67
|
+
"zod": "^4.6.5"
|
|
68
|
+
},
|
|
69
|
+
"devDependencies": {
|
|
70
|
+
"typescript": "^7.0.2",
|
|
71
|
+
"@types/node": "^22.0.0",
|
|
72
|
+
"@mark1russell7/cue": "^1.0.0",
|
|
73
|
+
"vitest": "^5.0.3"
|
|
74
|
+
},
|
|
75
|
+
"scripts": {
|
|
76
|
+
"build": "tsc -b",
|
|
77
|
+
"typecheck": "tsc --noEmit",
|
|
78
|
+
"clean": "rm -rf dist .tsbuildinfo",
|
|
79
|
+
"test": "vitest run",
|
|
80
|
+
"test:watch": "vitest"
|
|
81
|
+
}
|
|
6
82
|
}
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The error of a store procedure that cannot do its work.
|
|
3
|
+
*
|
|
4
|
+
* - NOT_CONFIGURED: the caller's registry has no store.
|
|
5
|
+
* - NOT_IMPLEMENTED: the action has no safe implementation (`procedure.remote connect`).
|
|
6
|
+
* - NO_HANDLER: `procedure.register` with `persist` got no handler that the loader allows.
|
|
7
|
+
* - NOT_A_PROGRAM: `procedure.store` got a code procedure. Only a procedure that
|
|
8
|
+
* `procedure.define` made is a program: code lives in a package.
|
|
9
|
+
*/
|
|
10
|
+
export class ProcedureStoreError extends Error {
|
|
11
|
+
override readonly name = "ProcedureStoreError";
|
|
12
|
+
readonly code: ProcedureStoreErrorCode;
|
|
13
|
+
|
|
14
|
+
constructor(code: ProcedureStoreErrorCode, message: string) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ProcedureStoreErrorCode = "NOT_CONFIGURED" | "NOT_IMPLEMENTED" | "NO_HANDLER" | "NOT_A_PROGRAM";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @procedures/store: the program store of the client ecosystem (roadmap 3.2).
|
|
3
|
+
*
|
|
4
|
+
* - The program store: `ProgramStore` and its backends (`MemoryProgramStore`,
|
|
5
|
+
* `FileProgramStore`, `CollectionProgramStore`), and the procedures `procedure.store`, `load`,
|
|
6
|
+
* `sync` and `remote`. A stored program loads back as a runtime-defined procedure.
|
|
7
|
+
* - The storage layer that moved here from `@procedures/client`: the collection storages that
|
|
8
|
+
* need a Client (`ApiStorage`, `HybridStorage`), the synced registry, the serialization, the
|
|
9
|
+
* dynamic handler loader (an allowlist) and `procedure.register`.
|
|
10
|
+
*
|
|
11
|
+
* Importing the package registers its procedures.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
export * from "./storage/index.js";
|
|
15
|
+
export * from "./program-store/index.js";
|
|
16
|
+
export { ProcedureStoreError, type ProcedureStoreErrorCode } from "./errors.js";
|
|
17
|
+
export { procedureStorageModule, procedureStorageProcedures } from "./procedures.js";
|
|
18
|
+
export * from "./register.js";
|
|
19
|
+
export * from "./types.js";
|
package/src/module.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Module (no side effects): the procedures of client-store as data, for `createRegistry()`.
|
|
3
|
+
*
|
|
4
|
+
* The procedures of the store: `procedure.register` and the program store procedures
|
|
5
|
+
* `procedure.store`, `load`, `sync` and `remote`. The core registered the older versions of them
|
|
6
|
+
* before they moved here (roadmap 3.2).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { defineModule, type ProcedureModule } from "@procedures/client";
|
|
10
|
+
import { procedureStorageProcedures } from "./procedures.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The module of this package, for `createRegistry()`: its procedures as data. Importing this
|
|
14
|
+
* file registers nothing. (`register.ts` registers the same procedures in the global registry.)
|
|
15
|
+
*/
|
|
16
|
+
export const storeModule: ProcedureModule = defineModule("@procedures/store", procedureStorageProcedures);
|
|
17
|
+
|
|
18
|
+
export default storeModule;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every procedure of the package: procedure.register (the synced registry) and the procedures of
|
|
3
|
+
* the program store (procedure.store, load, sync and remote).
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { AnyProcedure } from "@procedures/client";
|
|
7
|
+
import { procedureRegisterProcedure } from "./storage/procedures.js";
|
|
8
|
+
import { programStoreProcedures } from "./program-store/procedures.js";
|
|
9
|
+
|
|
10
|
+
/** All the procedures of client-store. */
|
|
11
|
+
export const procedureStorageProcedures: AnyProcedure[] = [procedureRegisterProcedure, ...programStoreProcedures];
|
|
12
|
+
|
|
13
|
+
/** All the procedures of client-store as a module. */
|
|
14
|
+
export const procedureStorageModule: { name: string; procedures: AnyProcedure[] } = {
|
|
15
|
+
name: "procedure-storage",
|
|
16
|
+
procedures: procedureStorageProcedures,
|
|
17
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The program store of a registry.
|
|
3
|
+
*
|
|
4
|
+
* A host gives a registry its store with `attachProgramStore(registry, store)`. The procedures
|
|
5
|
+
* `procedure.store`, `load` and `sync` use the store of the caller's registry (`ctx.registry`,
|
|
6
|
+
* or the global registry). Without a store, they fail with the code NOT_CONFIGURED.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { PROCEDURE_REGISTRY, type ProcedureContext, type ProcedureRegistryLike } from "@procedures/client";
|
|
10
|
+
import type { ProgramStore } from "./types.js";
|
|
11
|
+
import { ProcedureStoreError } from "../storage/procedures.js";
|
|
12
|
+
|
|
13
|
+
const STORES = new WeakMap<object, ProgramStore>();
|
|
14
|
+
|
|
15
|
+
/** Give a registry its program store. A second call replaces the store. */
|
|
16
|
+
export function attachProgramStore(registry: object, store: ProgramStore): void {
|
|
17
|
+
STORES.set(registry, store);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Take the program store from a registry. */
|
|
21
|
+
export function detachProgramStore(registry: object): void {
|
|
22
|
+
STORES.delete(registry);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** The program store of a registry, or undefined. */
|
|
26
|
+
export function getProgramStore(registry: object): ProgramStore | undefined {
|
|
27
|
+
return STORES.get(registry);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** The registry of a caller: the context's registry, or the global registry. */
|
|
31
|
+
export function callerRegistry(ctx: ProcedureContext | undefined): ProcedureRegistryLike {
|
|
32
|
+
return ctx?.registry ?? PROCEDURE_REGISTRY;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The program store of the caller's registry, or a NOT_CONFIGURED error. */
|
|
36
|
+
export function requireProgramStore(ctx: ProcedureContext | undefined, what: string): ProgramStore {
|
|
37
|
+
const store = getProgramStore(callerRegistry(ctx));
|
|
38
|
+
if (!store) {
|
|
39
|
+
throw new ProcedureStoreError(
|
|
40
|
+
"NOT_CONFIGURED",
|
|
41
|
+
`${what} needs a program store, and this registry has none. ` +
|
|
42
|
+
"Give it one with attachProgramStore(registry, new FileProgramStore(path)) (or MemoryProgramStore, CollectionProgramStore)."
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return store;
|
|
46
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A program store on a collection storage: `InMemoryStorage` of client-collections, or a remote
|
|
3
|
+
* one, `ApiStorage` (the collection procedures of a server) or `HybridStorage` (a local cache with
|
|
4
|
+
* a remote copy). The key of each record is the key of the program.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CollectionStorage } from "@procedures/client";
|
|
8
|
+
import { byKey, checkStoredProgram, keyMatches, type ProgramStore, type StoredProgram } from "./types.js";
|
|
9
|
+
|
|
10
|
+
export class CollectionProgramStore implements ProgramStore {
|
|
11
|
+
readonly kind = "collection";
|
|
12
|
+
|
|
13
|
+
/** @param storage - The storage of the records. The store closes it in `close()`. */
|
|
14
|
+
constructor(private readonly storage: CollectionStorage<StoredProgram>) {}
|
|
15
|
+
|
|
16
|
+
async get(key: string): Promise<StoredProgram | undefined> {
|
|
17
|
+
const record = await this.storage.get(key);
|
|
18
|
+
// A copy: a local storage (InMemoryStorage) gives its own object
|
|
19
|
+
return record === undefined ? undefined : checkStoredProgram(structuredClone(record));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async list(prefix?: string): Promise<StoredProgram[]> {
|
|
23
|
+
const records = await this.storage.getAll();
|
|
24
|
+
return byKey(records.map((record) => checkStoredProgram(structuredClone(record))).filter((p) => keyMatches(p.key, prefix)));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async put(program: StoredProgram): Promise<void> {
|
|
28
|
+
const checked = checkStoredProgram(structuredClone(program));
|
|
29
|
+
await this.storage.set(checked.key, checked);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async delete(key: string): Promise<boolean> {
|
|
33
|
+
return this.storage.delete(key);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async close(): Promise<void> {
|
|
37
|
+
await this.storage.close();
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A program store in one JSON file.
|
|
3
|
+
*
|
|
4
|
+
* The file is `{ "schemaVersion": 1, "programs": { "<key>": <StoredProgram> } }`. Each operation
|
|
5
|
+
* reads the file again, so two processes see the programs of each other. A write takes a lock
|
|
6
|
+
* file (`<file>.lock`), reads, changes and writes the file through a temporary file and a rename,
|
|
7
|
+
* so a reader never sees a part of a write and two writers do not lose each other's programs.
|
|
8
|
+
* A file that is not valid JSON is an error: the store does not write over it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdir, open, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
12
|
+
import { dirname } from "node:path";
|
|
13
|
+
import { byKey, checkStoredProgram, keyMatches, type ProgramStore, type StoredProgram } from "./types.js";
|
|
14
|
+
|
|
15
|
+
interface ProgramFile {
|
|
16
|
+
schemaVersion: 1;
|
|
17
|
+
programs: Record<string, StoredProgram>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** A lock older than this belongs to a process that stopped: it is removed. */
|
|
21
|
+
const STALE_LOCK_MS = 10_000;
|
|
22
|
+
/** The longest wait for the lock. */
|
|
23
|
+
const LOCK_TIMEOUT_MS = 30_000;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* True when another process holds a file for a moment. On Windows, a file that its owner is
|
|
27
|
+
* deleting or renaming cannot be opened until that completes: EPERM, EACCES or EBUSY.
|
|
28
|
+
*/
|
|
29
|
+
function isContention(error: unknown): boolean {
|
|
30
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
31
|
+
if (code === "EEXIST") return true;
|
|
32
|
+
return process.platform === "win32" && (code === "EPERM" || code === "EACCES" || code === "EBUSY");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const pause = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
36
|
+
|
|
37
|
+
/** Run a file operation again while Windows holds the file for a moment. */
|
|
38
|
+
async function retryOnContention<T>(operation: () => Promise<T>): Promise<T> {
|
|
39
|
+
for (let attempt = 1; ; attempt++) {
|
|
40
|
+
try {
|
|
41
|
+
return await operation();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
44
|
+
if (attempt >= 20 || code === "EEXIST" || !isContention(error)) throw error;
|
|
45
|
+
await pause(10 * attempt);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class FileProgramStore implements ProgramStore {
|
|
51
|
+
readonly kind = "file";
|
|
52
|
+
/** The writes of this process, one at a time (the lock file orders the processes). */
|
|
53
|
+
private queue: Promise<unknown> = Promise.resolve();
|
|
54
|
+
|
|
55
|
+
/** @param path - The JSON file. The store makes it, and its folder, at the first write. */
|
|
56
|
+
constructor(readonly path: string) {}
|
|
57
|
+
|
|
58
|
+
async get(key: string): Promise<StoredProgram | undefined> {
|
|
59
|
+
return (await this.read()).programs[key];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async list(prefix?: string): Promise<StoredProgram[]> {
|
|
63
|
+
return byKey(Object.values((await this.read()).programs).filter((p) => keyMatches(p.key, prefix)));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async put(program: StoredProgram): Promise<void> {
|
|
67
|
+
const checked = checkStoredProgram(structuredClone(program));
|
|
68
|
+
await this.change((file) => {
|
|
69
|
+
file.programs[checked.key] = checked;
|
|
70
|
+
return true;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async delete(key: string): Promise<boolean> {
|
|
75
|
+
let deleted = false;
|
|
76
|
+
await this.change((file) => {
|
|
77
|
+
deleted = Object.hasOwn(file.programs, key);
|
|
78
|
+
if (deleted) delete file.programs[key];
|
|
79
|
+
return deleted;
|
|
80
|
+
});
|
|
81
|
+
return deleted;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The content of the file. A missing file is an empty store. */
|
|
85
|
+
private async read(): Promise<ProgramFile> {
|
|
86
|
+
let text: string;
|
|
87
|
+
try {
|
|
88
|
+
text = await retryOnContention(() => readFile(this.path, "utf8"));
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return { schemaVersion: 1, programs: {} };
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
let parsed: unknown;
|
|
94
|
+
try {
|
|
95
|
+
parsed = JSON.parse(text);
|
|
96
|
+
} catch {
|
|
97
|
+
throw new Error(`The program store file is not valid JSON: ${this.path}. The store does not write over it.`);
|
|
98
|
+
}
|
|
99
|
+
const file = parsed as Partial<ProgramFile>;
|
|
100
|
+
if (file?.schemaVersion !== 1 || typeof file.programs !== "object" || file.programs === null) {
|
|
101
|
+
throw new Error(`The program store file has an unknown format (schemaVersion 1 expected): ${this.path}`);
|
|
102
|
+
}
|
|
103
|
+
// A null-prototype object: a key such as "__proto__" stays a plain key
|
|
104
|
+
const programs: Record<string, StoredProgram> = Object.create(null);
|
|
105
|
+
for (const [key, value] of Object.entries(file.programs)) programs[key] = checkStoredProgram(value);
|
|
106
|
+
return { schemaVersion: 1, programs };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Read, change and write the file under the lock. `change` gives false when nothing changed. */
|
|
110
|
+
private change(change: (file: ProgramFile) => boolean): Promise<void> {
|
|
111
|
+
const run = this.queue.then(() => this.withLock(async () => {
|
|
112
|
+
const file = await this.read();
|
|
113
|
+
if (change(file)) await this.write(file);
|
|
114
|
+
}));
|
|
115
|
+
this.queue = run.catch(() => undefined);
|
|
116
|
+
return run;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private async write(file: ProgramFile): Promise<void> {
|
|
120
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
121
|
+
const temporary = `${this.path}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
122
|
+
try {
|
|
123
|
+
await writeFile(temporary, `${JSON.stringify(file, null, 2)}\n`, "utf8");
|
|
124
|
+
await retryOnContention(() => rename(temporary, this.path));
|
|
125
|
+
} catch (error) {
|
|
126
|
+
await rm(temporary, { force: true });
|
|
127
|
+
throw error;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private async withLock(work: () => Promise<void>): Promise<void> {
|
|
132
|
+
const lockPath = `${this.path}.lock`;
|
|
133
|
+
await mkdir(dirname(this.path), { recursive: true });
|
|
134
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
135
|
+
for (;;) {
|
|
136
|
+
try {
|
|
137
|
+
const handle = await open(lockPath, "wx");
|
|
138
|
+
await handle.close();
|
|
139
|
+
break;
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (!isContention(error) || Date.now() > deadline) throw error;
|
|
142
|
+
try {
|
|
143
|
+
const { mtimeMs } = await stat(lockPath);
|
|
144
|
+
if (Date.now() - mtimeMs > STALE_LOCK_MS) await rm(lockPath, { force: true });
|
|
145
|
+
} catch {
|
|
146
|
+
// The lock went away: try again
|
|
147
|
+
}
|
|
148
|
+
await pause(10 + Math.random() * 20);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
await work();
|
|
153
|
+
} finally {
|
|
154
|
+
await rm(lockPath, { force: true });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The program store (roadmap 3.2): programs of procedure.define that outlive the process.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type { ProgramStore, StoredProgram } from "./types.js";
|
|
6
|
+
export { checkStoredProgram } from "./types.js";
|
|
7
|
+
export { MemoryProgramStore } from "./memory.js";
|
|
8
|
+
export { FileProgramStore } from "./file.js";
|
|
9
|
+
export { CollectionProgramStore } from "./collection.js";
|
|
10
|
+
export { attachProgramStore, detachProgramStore, getProgramStore } from "./attach.js";
|
|
11
|
+
export {
|
|
12
|
+
procedureStoreProcedure,
|
|
13
|
+
procedureLoadProcedure,
|
|
14
|
+
procedureSyncProcedure,
|
|
15
|
+
procedureRemoteProcedure,
|
|
16
|
+
programStoreProcedures,
|
|
17
|
+
} from "./procedures.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A program store in memory: for tests, and for a process that keeps its programs only while it
|
|
3
|
+
* runs. Each read and write copies the record, so a caller that changes a result does not change
|
|
4
|
+
* the store.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { byKey, checkStoredProgram, keyMatches, type ProgramStore, type StoredProgram } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export class MemoryProgramStore implements ProgramStore {
|
|
10
|
+
readonly kind = "memory";
|
|
11
|
+
private readonly programs = new Map<string, StoredProgram>();
|
|
12
|
+
|
|
13
|
+
async get(key: string): Promise<StoredProgram | undefined> {
|
|
14
|
+
const program = this.programs.get(key);
|
|
15
|
+
return program ? structuredClone(program) : undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async list(prefix?: string): Promise<StoredProgram[]> {
|
|
19
|
+
return byKey([...this.programs.values()].filter((p) => keyMatches(p.key, prefix)).map((p) => structuredClone(p)));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async put(program: StoredProgram): Promise<void> {
|
|
23
|
+
const checked = checkStoredProgram(structuredClone(program));
|
|
24
|
+
this.programs.set(checked.key, checked);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async delete(key: string): Promise<boolean> {
|
|
28
|
+
return this.programs.delete(key);
|
|
29
|
+
}
|
|
30
|
+
}
|