@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,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Procedure Storage Adapter
|
|
3
|
+
*
|
|
4
|
+
* Wraps CollectionStorage<SerializedProcedure> to provide procedure-specific
|
|
5
|
+
* storage operations with serialization/deserialization.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CollectionStorage } from "@procedures/collections";
|
|
9
|
+
import type { AnyProcedure, ProcedurePath } from "@procedures/client";
|
|
10
|
+
import { pathToKey } from "@procedures/client";
|
|
11
|
+
import type { SerializedProcedure, HandlerLoader } from "./types.js";
|
|
12
|
+
import {
|
|
13
|
+
serializeProcedure,
|
|
14
|
+
deserializeProcedure,
|
|
15
|
+
getProcedureKey,
|
|
16
|
+
type SerializeOptions,
|
|
17
|
+
} from "./serialization.js";
|
|
18
|
+
|
|
19
|
+
// =============================================================================
|
|
20
|
+
// Procedure Storage Adapter
|
|
21
|
+
// =============================================================================
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Options for ProcedureStorageAdapter.
|
|
25
|
+
*/
|
|
26
|
+
export interface ProcedureStorageAdapterOptions {
|
|
27
|
+
/** Handler loader for restoring handlers from references */
|
|
28
|
+
handlerLoader?: HandlerLoader | undefined;
|
|
29
|
+
|
|
30
|
+
/** Default serialization options */
|
|
31
|
+
serializeOptions?: SerializeOptions | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Adapter that wraps CollectionStorage for procedure-specific operations.
|
|
36
|
+
*
|
|
37
|
+
* Handles serialization/deserialization of procedures and provides
|
|
38
|
+
* convenience methods for common operations.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* const storage = new InMemoryStorage<SerializedProcedure>();
|
|
43
|
+
* const adapter = new ProcedureStorageAdapter(storage, {
|
|
44
|
+
* handlerLoader: createDynamicHandlerLoader(),
|
|
45
|
+
* });
|
|
46
|
+
*
|
|
47
|
+
* // Store a procedure
|
|
48
|
+
* await adapter.store(myProcedure);
|
|
49
|
+
*
|
|
50
|
+
* // Load a procedure
|
|
51
|
+
* const proc = await adapter.load(['lib', 'scan']);
|
|
52
|
+
* ```
|
|
53
|
+
*/
|
|
54
|
+
export class ProcedureStorageAdapter {
|
|
55
|
+
private readonly storage: CollectionStorage<SerializedProcedure>;
|
|
56
|
+
private readonly handlerLoader: HandlerLoader | undefined;
|
|
57
|
+
private readonly serializeOptions: SerializeOptions;
|
|
58
|
+
|
|
59
|
+
constructor(
|
|
60
|
+
storage: CollectionStorage<SerializedProcedure>,
|
|
61
|
+
options: ProcedureStorageAdapterOptions = {}
|
|
62
|
+
) {
|
|
63
|
+
this.storage = storage;
|
|
64
|
+
this.handlerLoader = options.handlerLoader;
|
|
65
|
+
this.serializeOptions = options.serializeOptions ?? {};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ===========================================================================
|
|
69
|
+
// Single Item Operations
|
|
70
|
+
// ===========================================================================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Store a procedure.
|
|
74
|
+
*
|
|
75
|
+
* @param procedure - Procedure to store
|
|
76
|
+
* @param options - Override default serialization options
|
|
77
|
+
* @returns The record that the storage got
|
|
78
|
+
*/
|
|
79
|
+
async store(procedure: AnyProcedure, options?: SerializeOptions): Promise<SerializedProcedure> {
|
|
80
|
+
const key = getProcedureKey(procedure);
|
|
81
|
+
const serialized = serializeProcedure(procedure, {
|
|
82
|
+
...this.serializeOptions,
|
|
83
|
+
...options,
|
|
84
|
+
});
|
|
85
|
+
await this.storage.set(key, serialized);
|
|
86
|
+
return serialized;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Load a procedure by path.
|
|
91
|
+
*
|
|
92
|
+
* @param path - Procedure path
|
|
93
|
+
* @returns Procedure or undefined if not found
|
|
94
|
+
*/
|
|
95
|
+
async load(path: ProcedurePath): Promise<AnyProcedure | undefined> {
|
|
96
|
+
const key = pathToKey(path);
|
|
97
|
+
const data = await this.storage.get(key);
|
|
98
|
+
if (!data) return undefined;
|
|
99
|
+
|
|
100
|
+
return deserializeProcedure(data, {
|
|
101
|
+
handlerLoader: this.handlerLoader,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Check if a procedure exists in storage.
|
|
107
|
+
*
|
|
108
|
+
* @param path - Procedure path
|
|
109
|
+
* @returns True if procedure exists
|
|
110
|
+
*/
|
|
111
|
+
async has(path: ProcedurePath): Promise<boolean> {
|
|
112
|
+
const key = pathToKey(path);
|
|
113
|
+
return this.storage.has(key);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Remove a procedure from storage.
|
|
118
|
+
*
|
|
119
|
+
* @param path - Procedure path
|
|
120
|
+
* @returns True if procedure was removed
|
|
121
|
+
*/
|
|
122
|
+
async remove(path: ProcedurePath): Promise<boolean> {
|
|
123
|
+
const key = pathToKey(path);
|
|
124
|
+
return this.storage.delete(key);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ===========================================================================
|
|
128
|
+
// Bulk Operations
|
|
129
|
+
// ===========================================================================
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Load all procedures from storage.
|
|
133
|
+
*
|
|
134
|
+
* @returns Array of all procedures
|
|
135
|
+
*/
|
|
136
|
+
async loadAll(): Promise<AnyProcedure[]> {
|
|
137
|
+
const items = await this.storage.getAll();
|
|
138
|
+
return Promise.all(
|
|
139
|
+
items.map((item) =>
|
|
140
|
+
deserializeProcedure(item, { handlerLoader: this.handlerLoader })
|
|
141
|
+
)
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Load procedures matching a path prefix.
|
|
147
|
+
*
|
|
148
|
+
* @param prefix - Path prefix to match
|
|
149
|
+
* @returns Array of matching procedures
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* ```typescript
|
|
153
|
+
* // Load all lib.* procedures
|
|
154
|
+
* const libProcedures = await adapter.loadByPrefix(['lib']);
|
|
155
|
+
* ```
|
|
156
|
+
*/
|
|
157
|
+
async loadByPrefix(prefix: ProcedurePath): Promise<AnyProcedure[]> {
|
|
158
|
+
const prefixKey = pathToKey(prefix);
|
|
159
|
+
const items = await this.storage.find((item) => {
|
|
160
|
+
const itemKey = pathToKey(item.path);
|
|
161
|
+
return itemKey === prefixKey || itemKey.startsWith(prefixKey + ".");
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
return Promise.all(
|
|
165
|
+
items.map((item) =>
|
|
166
|
+
deserializeProcedure(item, { handlerLoader: this.handlerLoader })
|
|
167
|
+
)
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Store multiple procedures.
|
|
173
|
+
*
|
|
174
|
+
* @param procedures - Procedures to store
|
|
175
|
+
* @param options - Override default serialization options, or a function that gives the
|
|
176
|
+
* options of each procedure (for example, its handler reference)
|
|
177
|
+
* @returns The records that the storage got
|
|
178
|
+
*/
|
|
179
|
+
async storeAll(
|
|
180
|
+
procedures: AnyProcedure[],
|
|
181
|
+
options?: SerializeOptions | ((procedure: AnyProcedure) => SerializeOptions | undefined)
|
|
182
|
+
): Promise<SerializedProcedure[]> {
|
|
183
|
+
const items: Array<[string, SerializedProcedure]> = procedures.map((proc) => [
|
|
184
|
+
getProcedureKey(proc),
|
|
185
|
+
serializeProcedure(proc, {
|
|
186
|
+
...this.serializeOptions,
|
|
187
|
+
...(typeof options === "function" ? options(proc) : options),
|
|
188
|
+
}),
|
|
189
|
+
]);
|
|
190
|
+
await this.storage.setBatch(items);
|
|
191
|
+
return items.map(([, serialized]) => serialized);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Remove multiple procedures.
|
|
196
|
+
*
|
|
197
|
+
* @param paths - Procedure paths to remove
|
|
198
|
+
* @returns Number of procedures removed
|
|
199
|
+
*/
|
|
200
|
+
async removeAll(paths: ProcedurePath[]): Promise<number> {
|
|
201
|
+
const keys = paths.map(pathToKey);
|
|
202
|
+
return this.storage.deleteBatch(keys);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Clear all procedures from storage.
|
|
207
|
+
*/
|
|
208
|
+
async clear(): Promise<void> {
|
|
209
|
+
await this.storage.clear();
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ===========================================================================
|
|
213
|
+
// Info Operations
|
|
214
|
+
// ===========================================================================
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get the number of stored procedures.
|
|
218
|
+
*/
|
|
219
|
+
async size(): Promise<number> {
|
|
220
|
+
return this.storage.size();
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* List all stored procedure paths.
|
|
225
|
+
*
|
|
226
|
+
* @returns Array of procedure paths
|
|
227
|
+
*/
|
|
228
|
+
async listPaths(): Promise<ProcedurePath[]> {
|
|
229
|
+
const items = await this.storage.getAll();
|
|
230
|
+
return items.map((item) => item.path);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Get the raw serialized data for a procedure.
|
|
235
|
+
* Useful for debugging or custom processing.
|
|
236
|
+
*
|
|
237
|
+
* @param path - Procedure path
|
|
238
|
+
* @returns Serialized procedure data or undefined
|
|
239
|
+
*/
|
|
240
|
+
async getRaw(path: ProcedurePath): Promise<SerializedProcedure | undefined> {
|
|
241
|
+
const key = pathToKey(path);
|
|
242
|
+
return this.storage.get(key);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get all raw serialized data.
|
|
247
|
+
* Useful for debugging or backup.
|
|
248
|
+
*
|
|
249
|
+
* @returns Array of serialized procedures
|
|
250
|
+
*/
|
|
251
|
+
async getAllRaw(): Promise<SerializedProcedure[]> {
|
|
252
|
+
return this.storage.getAll();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ===========================================================================
|
|
256
|
+
// Lifecycle
|
|
257
|
+
// ===========================================================================
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Close the underlying storage.
|
|
261
|
+
*/
|
|
262
|
+
async close(): Promise<void> {
|
|
263
|
+
await this.storage.close();
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Get the underlying storage instance.
|
|
268
|
+
* Use for advanced operations or testing.
|
|
269
|
+
*/
|
|
270
|
+
getStorage(): CollectionStorage<SerializedProcedure> {
|
|
271
|
+
return this.storage;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Storage Implementation
|
|
3
|
+
*
|
|
4
|
+
* Remote storage backed by universal client.
|
|
5
|
+
* Enables collections to be persisted on a server.
|
|
6
|
+
*
|
|
7
|
+
* This is the KEY integration between Collections and Universal Client!
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { CollectionStorage, StorageMetadata } from "@procedures/collections";
|
|
11
|
+
import type { Client } from "@procedures/client";
|
|
12
|
+
import type { Method } from "@procedures/client";
|
|
13
|
+
import type { CallOptions } from "@procedures/client/client";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* API storage configuration options.
|
|
17
|
+
*/
|
|
18
|
+
export interface ApiStorageOptions {
|
|
19
|
+
/**
|
|
20
|
+
* The collection of the core collection procedures. The calls go to
|
|
21
|
+
* `collections.<collection>.<operation>`, as `createCollectionProcedures(collection)` registers them.
|
|
22
|
+
* @example "users", "procedures"
|
|
23
|
+
*/
|
|
24
|
+
collection?: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Service name for RPC calls. The default is `collections.<collection>`. Give a service only
|
|
28
|
+
* for a server with other paths.
|
|
29
|
+
* @example "users", "orders", "products"
|
|
30
|
+
*/
|
|
31
|
+
service?: string;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Optional API version
|
|
35
|
+
* @example "v1", "v2"
|
|
36
|
+
*/
|
|
37
|
+
version?: string;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Custom operation names (override defaults)
|
|
41
|
+
*/
|
|
42
|
+
operations?: {
|
|
43
|
+
get?: string;
|
|
44
|
+
getAll?: string;
|
|
45
|
+
find?: string;
|
|
46
|
+
has?: string;
|
|
47
|
+
size?: string;
|
|
48
|
+
set?: string;
|
|
49
|
+
delete?: string;
|
|
50
|
+
clear?: string;
|
|
51
|
+
setBatch?: string;
|
|
52
|
+
deleteBatch?: string;
|
|
53
|
+
getBatch?: string;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Request timeout in milliseconds. A call that takes longer fails. 0: no timeout.
|
|
58
|
+
* @default 30000 (30 seconds)
|
|
59
|
+
*/
|
|
60
|
+
timeout?: number;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Retry a call that fails with a retryable error: true (3 attempts), false (1 attempt), or the
|
|
64
|
+
* number of attempts.
|
|
65
|
+
* @default true
|
|
66
|
+
*/
|
|
67
|
+
retry?: boolean | number;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The first delay between attempts in milliseconds. Each later delay is two times longer.
|
|
71
|
+
* @default 100
|
|
72
|
+
*/
|
|
73
|
+
retryDelay?: number;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* AbortSignal for cancelling requests
|
|
77
|
+
*/
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* True: `close()` closes the client. The default is false: the client belongs to the caller.
|
|
82
|
+
* @default false
|
|
83
|
+
*/
|
|
84
|
+
closeClient?: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** An error that says whether another attempt can succeed (a ClientError does). */
|
|
88
|
+
function isRetryable(error: unknown): boolean {
|
|
89
|
+
return (
|
|
90
|
+
typeof error === "object" &&
|
|
91
|
+
error !== null &&
|
|
92
|
+
(error as { retryable?: unknown }).retryable === true
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** A promise that rejects when the signal aborts first. */
|
|
97
|
+
function untilAborted<T>(promise: Promise<T>, signal: AbortSignal | undefined, describe: () => Error): Promise<T> {
|
|
98
|
+
if (!signal) return promise;
|
|
99
|
+
if (signal.aborted) return Promise.reject(describe());
|
|
100
|
+
return new Promise<T>((resolve, reject) => {
|
|
101
|
+
const onAbort = (): void => reject(describe());
|
|
102
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
103
|
+
promise.then(
|
|
104
|
+
(value) => {
|
|
105
|
+
signal.removeEventListener("abort", onAbort);
|
|
106
|
+
resolve(value);
|
|
107
|
+
},
|
|
108
|
+
(error: unknown) => {
|
|
109
|
+
signal.removeEventListener("abort", onAbort);
|
|
110
|
+
reject(error);
|
|
111
|
+
}
|
|
112
|
+
);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* API storage backed by universal client.
|
|
118
|
+
*
|
|
119
|
+
* Maps collection operations to the core collection procedures
|
|
120
|
+
* (`createCollectionProcedures`), which take and return raw values:
|
|
121
|
+
* - get(id) → `collections.<collection>.get` with `{ id }`
|
|
122
|
+
* - set(id, value) → `collections.<collection>.set` with `{ id, value }`
|
|
123
|
+
* - setBatch(items) → `collections.<collection>.setBatch` with `{ items: [{ id, value }] }`
|
|
124
|
+
* - etc.
|
|
125
|
+
*
|
|
126
|
+
* Before, the class expected a `{ data }` envelope and called `<service>.get`: no core
|
|
127
|
+
* procedure matched, and every read was undefined (deep dive CORE-9, DATA-9).
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```typescript
|
|
131
|
+
* // Create API storage
|
|
132
|
+
* const client = new Client({
|
|
133
|
+
* transport: new HttpTransport({ baseUrl: "https://api.example.com" })
|
|
134
|
+
* });
|
|
135
|
+
*
|
|
136
|
+
* const storage = new ApiStorage(client, { collection: "users" });
|
|
137
|
+
*
|
|
138
|
+
* // Operations are automatically sent to server
|
|
139
|
+
* await storage.set("123", { name: "John" });
|
|
140
|
+
* const user = await storage.get("123"); // Fetched from server
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export class ApiStorage<T> implements CollectionStorage<T> {
|
|
144
|
+
private client: Client;
|
|
145
|
+
private options: {
|
|
146
|
+
service: string;
|
|
147
|
+
version?: string;
|
|
148
|
+
operations: Required<NonNullable<ApiStorageOptions["operations"]>>;
|
|
149
|
+
timeout: number;
|
|
150
|
+
attempts: number;
|
|
151
|
+
retryDelay: number;
|
|
152
|
+
signal?: AbortSignal;
|
|
153
|
+
closeClient: boolean;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
constructor(client: Client, options: ApiStorageOptions) {
|
|
157
|
+
const service = options.service ?? (options.collection !== undefined ? `collections.${options.collection}` : undefined);
|
|
158
|
+
if (!service) {
|
|
159
|
+
throw new Error("ApiStorage requires a collection or a service");
|
|
160
|
+
}
|
|
161
|
+
this.client = client;
|
|
162
|
+
this.options = {
|
|
163
|
+
service,
|
|
164
|
+
...(options.version !== undefined && { version: options.version }),
|
|
165
|
+
operations: {
|
|
166
|
+
get: "get",
|
|
167
|
+
getAll: "getAll",
|
|
168
|
+
find: "find",
|
|
169
|
+
has: "has",
|
|
170
|
+
size: "size",
|
|
171
|
+
set: "set",
|
|
172
|
+
delete: "delete",
|
|
173
|
+
clear: "clear",
|
|
174
|
+
setBatch: "setBatch",
|
|
175
|
+
deleteBatch: "deleteBatch",
|
|
176
|
+
getBatch: "getBatch",
|
|
177
|
+
...options.operations,
|
|
178
|
+
},
|
|
179
|
+
timeout: options.timeout ?? 30000,
|
|
180
|
+
attempts:
|
|
181
|
+
options.retry === false ? 1 : options.retry === true || options.retry === undefined ? 3 : Math.max(1, options.retry),
|
|
182
|
+
retryDelay: options.retryDelay ?? 100,
|
|
183
|
+
...(options.signal !== undefined && { signal: options.signal }),
|
|
184
|
+
closeClient: options.closeClient ?? false,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
//
|
|
189
|
+
// ═══ Read Operations ═══
|
|
190
|
+
//
|
|
191
|
+
|
|
192
|
+
async get(id: string): Promise<T | undefined> {
|
|
193
|
+
const value = await this.call<T | null | undefined>("get", { id });
|
|
194
|
+
return value ?? undefined;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async getAll(): Promise<T[]> {
|
|
198
|
+
const values = await this.call<T[]>("getAll", {});
|
|
199
|
+
if (!Array.isArray(values)) {
|
|
200
|
+
throw new Error(`${this.options.service}.getAll did not return an array`);
|
|
201
|
+
}
|
|
202
|
+
return values;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async find(predicate: (item: T) => boolean): Promise<T[]> {
|
|
206
|
+
// A predicate cannot go over the network: read all items, then filter here
|
|
207
|
+
const all = await this.getAll();
|
|
208
|
+
return all.filter(predicate);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async has(id: string): Promise<boolean> {
|
|
212
|
+
return (await this.call<boolean>("has", { id })) === true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async size(): Promise<number> {
|
|
216
|
+
return Number(await this.call<number>("size", {}));
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
//
|
|
220
|
+
// ═══ Write Operations ═══
|
|
221
|
+
//
|
|
222
|
+
|
|
223
|
+
async set(id: string, value: T): Promise<void> {
|
|
224
|
+
await this.call("set", { id, value });
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async delete(id: string): Promise<boolean> {
|
|
228
|
+
return (await this.call<boolean>("delete", { id })) === true;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async clear(): Promise<void> {
|
|
232
|
+
await this.call("clear", {});
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
//
|
|
236
|
+
// ═══ Bulk Operations ═══
|
|
237
|
+
//
|
|
238
|
+
|
|
239
|
+
async setBatch(items: Array<[string, T]>): Promise<void> {
|
|
240
|
+
// The collection procedure takes objects, not tuples
|
|
241
|
+
await this.call("setBatch", { items: items.map(([id, value]) => ({ id, value })) });
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async deleteBatch(ids: string[]): Promise<number> {
|
|
245
|
+
return Number(await this.call<number>("deleteBatch", { ids }));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
async getBatch(ids: string[]): Promise<Map<string, T>> {
|
|
249
|
+
const values = (await this.call<Record<string, T> | null>("getBatch", { ids })) ?? {};
|
|
250
|
+
|
|
251
|
+
// Convert object to Map
|
|
252
|
+
const result = new Map<string, T>();
|
|
253
|
+
for (const [id, value] of Object.entries(values)) {
|
|
254
|
+
result.set(id, value);
|
|
255
|
+
}
|
|
256
|
+
return result;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
//
|
|
260
|
+
// ═══ Lifecycle & Metadata ═══
|
|
261
|
+
//
|
|
262
|
+
|
|
263
|
+
async close(): Promise<void> {
|
|
264
|
+
// The client belongs to the caller, unless the caller gave it to this storage
|
|
265
|
+
if (this.options.closeClient) {
|
|
266
|
+
await this.client.close();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async getMetadata(): Promise<StorageMetadata> {
|
|
271
|
+
const storageSize = await this.size();
|
|
272
|
+
|
|
273
|
+
return {
|
|
274
|
+
type: "api",
|
|
275
|
+
size: storageSize,
|
|
276
|
+
stats: {
|
|
277
|
+
service: this.options.service,
|
|
278
|
+
version: this.options.version,
|
|
279
|
+
timeout: this.options.timeout,
|
|
280
|
+
},
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
//
|
|
285
|
+
// ═══ Internal Helpers ═══
|
|
286
|
+
//
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Make an RPC call via the universal client, with the timeout, the signal and the retries.
|
|
290
|
+
*/
|
|
291
|
+
private async call<TRes>(operation: keyof Required<ApiStorageOptions>["operations"], payload: unknown): Promise<TRes> {
|
|
292
|
+
const method: Method = {
|
|
293
|
+
service: this.options.service,
|
|
294
|
+
operation: this.options.operations[operation] ?? operation,
|
|
295
|
+
...(this.options.version !== undefined && { version: this.options.version }),
|
|
296
|
+
};
|
|
297
|
+
const label = `${method.service}.${method.operation}`;
|
|
298
|
+
|
|
299
|
+
for (let attempt = 1; ; attempt++) {
|
|
300
|
+
try {
|
|
301
|
+
return await this.attempt<TRes>(method, payload, label);
|
|
302
|
+
} catch (error) {
|
|
303
|
+
if (attempt >= this.options.attempts || !isRetryable(error) || this.options.signal?.aborted) {
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
const delay = this.options.retryDelay * 2 ** (attempt - 1);
|
|
307
|
+
await untilAborted(new Promise((resolve) => setTimeout(resolve, delay)), this.options.signal, () => error as Error);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/** One attempt: the call ends at the timeout, or when the signal of the options aborts. */
|
|
313
|
+
private attempt<TRes>(method: Method, payload: unknown, label: string): Promise<TRes> {
|
|
314
|
+
const signals: AbortSignal[] = [];
|
|
315
|
+
if (this.options.signal) signals.push(this.options.signal);
|
|
316
|
+
const timer = this.options.timeout > 0 ? new AbortController() : undefined;
|
|
317
|
+
let timeoutHandle: ReturnType<typeof setTimeout> | undefined;
|
|
318
|
+
if (timer) {
|
|
319
|
+
timeoutHandle = setTimeout(() => timer.abort(), this.options.timeout);
|
|
320
|
+
signals.push(timer.signal);
|
|
321
|
+
}
|
|
322
|
+
const signal = signals.length === 0 ? undefined : signals.length === 1 ? signals[0] : AbortSignal.any(signals);
|
|
323
|
+
|
|
324
|
+
// The signal goes in the call options, so the transport can stop the request too. (Before,
|
|
325
|
+
// it went into the metadata, and the timeout was dropped: deep dive CORE-9.)
|
|
326
|
+
const callOptions: CallOptions<unknown> = {};
|
|
327
|
+
if (signal) callOptions.signal = signal;
|
|
328
|
+
const call = this.client.call<unknown, TRes>(method, payload, callOptions);
|
|
329
|
+
return untilAborted(call, signal, () =>
|
|
330
|
+
timer?.signal.aborted ? new Error(`${label} timed out after ${this.options.timeout} ms`) : new Error(`${label} was aborted`)
|
|
331
|
+
).finally(() => {
|
|
332
|
+
if (timeoutHandle !== undefined) clearTimeout(timeoutHandle);
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|