@procedures/client 0.14.1 → 0.15.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 +33 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/transport.d.ts.map +1 -1
- package/dist/adapters/http/client/transport.js +4 -0
- package/dist/adapters/http/client/transport.js.map +1 -1
- package/dist/adapters/http/server/handler.js +3 -0
- package/dist/adapters/http/server/handler.js.map +1 -1
- package/dist/adapters/local/client/transport.d.ts.map +1 -1
- package/dist/adapters/local/client/transport.js +5 -0
- package/dist/adapters/local/client/transport.js.map +1 -1
- package/dist/browser.d.ts +2 -2
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +2 -0
- package/dist/browser.js.map +1 -1
- package/dist/client/client.js +1 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/errors/registry.d.ts.map +1 -1
- package/dist/client/errors/registry.js +1 -0
- package/dist/client/errors/registry.js.map +1 -1
- package/dist/client/types.d.ts +6 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/types.js +7 -1
- package/dist/client/types.js.map +1 -1
- package/dist/procedures/collection/index.d.ts +3 -1
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js +2 -0
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/precondition.d.ts +111 -0
- package/dist/procedures/collection/precondition.d.ts.map +1 -0
- package/dist/procedures/collection/precondition.js +153 -0
- package/dist/procedures/collection/precondition.js.map +1 -0
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +16 -3
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/collection/query.d.ts +2 -0
- package/dist/procedures/collection/query.d.ts.map +1 -1
- package/dist/procedures/collection/query.js +3 -0
- package/dist/procedures/collection/query.js.map +1 -1
- package/dist/procedures/collection/schemas.d.ts +16 -0
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +26 -5
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +34 -6
- package/dist/procedures/collection/storage.d.ts.map +1 -1
- package/dist/procedures/collection/storage.js +3 -0
- package/dist/procedures/collection/storage.js.map +1 -1
- package/dist/procedures/index.d.ts +2 -2
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +1 -1
- package/dist/procedures/index.js.map +1 -1
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -0
- package/dist/protocol.js.map +1 -1
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +5 -0
- package/dist/server/procedure-server.js.map +1 -1
- package/package.json +1 -1
- package/src/adapters/http/client/transport.ts +3 -0
- package/src/adapters/http/server/handler.ts +3 -0
- package/src/adapters/local/client/transport.ts +4 -0
- package/src/browser.ts +16 -0
- package/src/client/client.ts +1 -1
- package/src/client/errors/registry.ts +1 -0
- package/src/client/types.ts +8 -0
- package/src/procedures/collection/index.ts +17 -0
- package/src/procedures/collection/precondition.ts +180 -0
- package/src/procedures/collection/procedures.ts +17 -4
- package/src/procedures/collection/query.ts +16 -0
- package/src/procedures/collection/schemas.ts +43 -6
- package/src/procedures/collection/storage.ts +36 -6
- package/src/procedures/index.ts +15 -0
- package/src/protocol.ts +3 -1
- package/src/server/procedure-server.ts +4 -0
|
@@ -31,6 +31,22 @@
|
|
|
31
31
|
* regular expressions and the limit. The default policy does not permit `$regex`.
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
+
// The precondition of the collection writes is a part of the wire contract too (roadmap CLI-16).
|
|
35
|
+
// This entry gives it to the storage packages: the module has no imports.
|
|
36
|
+
export {
|
|
37
|
+
MAX_EXPECTED_VERSION_LENGTH,
|
|
38
|
+
PRECONDITION_NOT_SUPPORTED,
|
|
39
|
+
VERSION_CONFLICTS_METADATA_KEY,
|
|
40
|
+
VersionConflictError,
|
|
41
|
+
asVersionConflictError,
|
|
42
|
+
isExpectedVersion,
|
|
43
|
+
preconditionNotSupported,
|
|
44
|
+
versionConflict,
|
|
45
|
+
versionConflicts,
|
|
46
|
+
versionConflictsOf,
|
|
47
|
+
} from "./precondition.js";
|
|
48
|
+
export type { CurrentItem, VersionConflict, VersionOf } from "./precondition.js";
|
|
49
|
+
|
|
34
50
|
//
|
|
35
51
|
// ═══ The types ═══
|
|
36
52
|
//
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
type StorageSelector,
|
|
17
17
|
type StorageSort,
|
|
18
18
|
} from "./query.js";
|
|
19
|
+
import { MAX_EXPECTED_VERSION_LENGTH, isExpectedVersion } from "./precondition.js";
|
|
19
20
|
|
|
20
21
|
// =============================================================================
|
|
21
22
|
// Generic Schema Builder
|
|
@@ -83,6 +84,25 @@ function withOriginOf<I extends object>(data: Record<string, unknown>, input: I)
|
|
|
83
84
|
return (isString(origin) ? { ...input, origin } : input) as I & { origin?: string };
|
|
84
85
|
}
|
|
85
86
|
|
|
87
|
+
/** The message of an expected version that is not valid (roadmap CLI-16). */
|
|
88
|
+
const EXPECTED_VERSION_MESSAGE = `expectedVersion must be a string of at most ${MAX_EXPECTED_VERSION_LENGTH} characters, or null`;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The error of the field `expectedVersion` of a write input (roadmap CLI-16), or undefined. The
|
|
92
|
+
* field is absent, a string of at most 1024 characters, or `null`.
|
|
93
|
+
*/
|
|
94
|
+
function expectedVersionError(data: Record<string, unknown>, path: (string | number)[] = []): ZodErrorLike | undefined {
|
|
95
|
+
if (!("expectedVersion" in data) || data["expectedVersion"] === undefined || isExpectedVersion(data["expectedVersion"])) return undefined;
|
|
96
|
+
const message = path.length === 0 ? EXPECTED_VERSION_MESSAGE : `${path.join(".")}.${EXPECTED_VERSION_MESSAGE}`;
|
|
97
|
+
return { message, errors: [{ path: [...path, "expectedVersion"], message }] };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** The input with the expected version of the data, when the data has one. */
|
|
101
|
+
function withExpectedVersionOf<I extends object>(data: Record<string, unknown>, input: I): I & { expectedVersion?: string | null } {
|
|
102
|
+
const expected = data["expectedVersion"];
|
|
103
|
+
return (isExpectedVersion(expected) ? { ...input, expectedVersion: expected } : input) as I & { expectedVersion?: string | null };
|
|
104
|
+
}
|
|
105
|
+
|
|
86
106
|
// =============================================================================
|
|
87
107
|
// Common Schemas
|
|
88
108
|
// =============================================================================
|
|
@@ -217,6 +237,11 @@ export interface SetInput {
|
|
|
217
237
|
value: unknown;
|
|
218
238
|
/** The origin of the write, for the observers of the storage and the change event (roadmap CLI-15). */
|
|
219
239
|
origin?: string;
|
|
240
|
+
/**
|
|
241
|
+
* The version that the write expects for the current item (roadmap CLI-16). `null` expects no
|
|
242
|
+
* item. A conflict changes nothing and answers `VERSION_CONFLICT` with the current item.
|
|
243
|
+
*/
|
|
244
|
+
expectedVersion?: string | null;
|
|
220
245
|
}
|
|
221
246
|
|
|
222
247
|
export const setInputSchema : ZodLike<SetInput> = createSchema<SetInput>((data) => {
|
|
@@ -247,9 +272,9 @@ export const setInputSchema : ZodLike<SetInput> = createSchema<SetInput>((data)
|
|
|
247
272
|
},
|
|
248
273
|
};
|
|
249
274
|
}
|
|
250
|
-
const error = originError(data);
|
|
275
|
+
const error = originError(data) ?? expectedVersionError(data);
|
|
251
276
|
if (error) return { success: false, error };
|
|
252
|
-
return { success: true, data: withOriginOf(data, { id: data["id"], value: data["value"] }) };
|
|
277
|
+
return { success: true, data: withExpectedVersionOf(data, withOriginOf(data, { id: data["id"], value: data["value"] })) };
|
|
253
278
|
});
|
|
254
279
|
|
|
255
280
|
/**
|
|
@@ -270,6 +295,11 @@ export interface DeleteInput {
|
|
|
270
295
|
id: string;
|
|
271
296
|
/** The origin of the write, for the observers of the storage and the change event (roadmap CLI-15). */
|
|
272
297
|
origin?: string;
|
|
298
|
+
/**
|
|
299
|
+
* The version that the delete expects for the current item (roadmap CLI-16). `null` expects no
|
|
300
|
+
* item. A conflict changes nothing and answers `VERSION_CONFLICT` with the current item.
|
|
301
|
+
*/
|
|
302
|
+
expectedVersion?: string | null;
|
|
273
303
|
}
|
|
274
304
|
|
|
275
305
|
export const deleteInputSchema : ZodLike<DeleteInput> = createSchema<DeleteInput>((data) => {
|
|
@@ -291,9 +321,9 @@ export const deleteInputSchema : ZodLike<DeleteInput> = createSchema<DeleteInput
|
|
|
291
321
|
},
|
|
292
322
|
};
|
|
293
323
|
}
|
|
294
|
-
const error = originError(data);
|
|
324
|
+
const error = originError(data) ?? expectedVersionError(data);
|
|
295
325
|
if (error) return { success: false, error };
|
|
296
|
-
return { success: true, data: withOriginOf(data, { id: data["id"] }) };
|
|
326
|
+
return { success: true, data: withExpectedVersionOf(data, withOriginOf(data, { id: data["id"] })) };
|
|
297
327
|
});
|
|
298
328
|
|
|
299
329
|
/**
|
|
@@ -810,7 +840,12 @@ export const getBatchOutputSchema : ZodLike<Record<string, unknown>> = createSch
|
|
|
810
840
|
* Input for setBatch operation.
|
|
811
841
|
*/
|
|
812
842
|
export interface SetBatchInput {
|
|
813
|
-
|
|
843
|
+
/**
|
|
844
|
+
* The items. An item can have `expectedVersion`: the version that the write expects for the
|
|
845
|
+
* current item of its id (roadmap CLI-16). When one item has a conflict, the batch changes no
|
|
846
|
+
* item and answers `VERSION_CONFLICT`.
|
|
847
|
+
*/
|
|
848
|
+
items: Array<{ id: string; value: unknown; expectedVersion?: string | null }>;
|
|
814
849
|
/** The origin of the write, for the observers of the storage and the change event (roadmap CLI-15). */
|
|
815
850
|
origin?: string;
|
|
816
851
|
}
|
|
@@ -845,13 +880,15 @@ export const setBatchInputSchema : ZodLike<SetBatchInput> = createSchema<SetBatc
|
|
|
845
880
|
},
|
|
846
881
|
};
|
|
847
882
|
}
|
|
883
|
+
const versionError = expectedVersionError(item, ["items", i]);
|
|
884
|
+
if (versionError) return { success: false, error: versionError };
|
|
848
885
|
}
|
|
849
886
|
const error = originError(data);
|
|
850
887
|
if (error) return { success: false, error };
|
|
851
888
|
return {
|
|
852
889
|
success: true,
|
|
853
890
|
data: withOriginOf(data, {
|
|
854
|
-
items: data["items"].map((item: any) => ({ id: item["id"], value: item["value"] })),
|
|
891
|
+
items: data["items"].map((item: any) => withExpectedVersionOf(item, { id: item["id"], value: item["value"] })),
|
|
855
892
|
}),
|
|
856
893
|
};
|
|
857
894
|
});
|
|
@@ -20,6 +20,9 @@
|
|
|
20
20
|
* - `query()` gives the result of `queryEntries()` of `@procedures/collections` for the entries.
|
|
21
21
|
* - `observe()` gives one batch for each write that changes the storage, before the write resolves.
|
|
22
22
|
* - `patch()` merges the fields into the item in one step.
|
|
23
|
+
* - A write with `expectedVersion` (`set`, `delete`) or `expectedVersions` (`setBatch`) tests the
|
|
24
|
+
* version of the current item and writes in one step. A conflict changes nothing and rejects
|
|
25
|
+
* with `VersionConflictError` (roadmap CLI-16).
|
|
23
26
|
*/
|
|
24
27
|
|
|
25
28
|
import type { StoragePatch, StorageQueryOptions, StorageQueryPolicy, StorageSelector } from "./query.js";
|
|
@@ -76,10 +79,13 @@ export interface CollectionStorage<T> {
|
|
|
76
79
|
has(id: string): Promise<boolean>;
|
|
77
80
|
/** The number of items. */
|
|
78
81
|
size(): Promise<number>;
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
82
|
+
/**
|
|
83
|
+
* Set the item of an id. The options give the origin of the write to the observers, and the
|
|
84
|
+
* expected version of the current item (roadmap CLI-16).
|
|
85
|
+
*/
|
|
86
|
+
set(id: string, value: T, options?: StorageItemWriteOptions): Promise<void>;
|
|
87
|
+
/** Delete the item of an id. True when it existed. The options can expect a version, as for `set`. */
|
|
88
|
+
delete(id: string, options?: StorageItemWriteOptions): Promise<boolean>;
|
|
83
89
|
/** Delete every item. */
|
|
84
90
|
clear(options?: StorageWriteOptions): Promise<void>;
|
|
85
91
|
/**
|
|
@@ -88,8 +94,8 @@ export interface CollectionStorage<T> {
|
|
|
88
94
|
* procedure `patch` uses it. A storage without it gets a read and a write of the item.
|
|
89
95
|
*/
|
|
90
96
|
patch?(id: string, fields: StoragePatch, options?: StorageWriteOptions): Promise<T | undefined>;
|
|
91
|
-
/** Set several items. */
|
|
92
|
-
setBatch(items: Array<[string, T]>, options?:
|
|
97
|
+
/** Set several items. The options can expect a version for each id (roadmap CLI-16). */
|
|
98
|
+
setBatch(items: Array<[string, T]>, options?: StorageBatchWriteOptions): Promise<void>;
|
|
93
99
|
/** Delete several items. The number of items that existed. */
|
|
94
100
|
deleteBatch(ids: string[], options?: StorageWriteOptions): Promise<number>;
|
|
95
101
|
/** The items of several ids. An id with no item is not in the map. */
|
|
@@ -114,6 +120,30 @@ export interface StorageWriteOptions {
|
|
|
114
120
|
readonly origin?: string | undefined;
|
|
115
121
|
}
|
|
116
122
|
|
|
123
|
+
/**
|
|
124
|
+
* The options of `set` and `delete`: the origin, and the precondition of the write (roadmap
|
|
125
|
+
* CLI-16). `precondition.ts` gives the rules.
|
|
126
|
+
*/
|
|
127
|
+
export interface StorageItemWriteOptions extends StorageWriteOptions {
|
|
128
|
+
/**
|
|
129
|
+
* The version that the write expects for the current item, from `versionOf` of the storage.
|
|
130
|
+
* `null` expects no item. When the current item has another version, the write changes nothing
|
|
131
|
+
* and rejects with `VersionConflictError`. A storage without `versionOf` rejects it with a
|
|
132
|
+
* `TypeError`. Absent: no precondition.
|
|
133
|
+
*/
|
|
134
|
+
readonly expectedVersion?: string | null | undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The options of `setBatch`: the origin, and the precondition of each id (roadmap CLI-16). */
|
|
138
|
+
export interface StorageBatchWriteOptions extends StorageWriteOptions {
|
|
139
|
+
/**
|
|
140
|
+
* The version that the write expects for the current item of each id, as for `expectedVersion`.
|
|
141
|
+
* When one id has a conflict, the batch changes no item. An id without an entry has no
|
|
142
|
+
* precondition.
|
|
143
|
+
*/
|
|
144
|
+
readonly expectedVersions?: ReadonlyArray<readonly [string, string | null]> | undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
117
147
|
/** A change of `observe()`: an id got a value. */
|
|
118
148
|
export interface StorageSetChange<T> {
|
|
119
149
|
readonly type: "set";
|
package/src/procedures/index.ts
CHANGED
|
@@ -150,8 +150,23 @@ export {
|
|
|
150
150
|
queryEntries,
|
|
151
151
|
queryOptionsProblems,
|
|
152
152
|
selectorProblems,
|
|
153
|
+
MAX_EXPECTED_VERSION_LENGTH,
|
|
154
|
+
PRECONDITION_NOT_SUPPORTED,
|
|
155
|
+
VERSION_CONFLICTS_METADATA_KEY,
|
|
156
|
+
VersionConflictError,
|
|
157
|
+
asVersionConflictError,
|
|
158
|
+
isExpectedVersion,
|
|
159
|
+
preconditionNotSupported,
|
|
160
|
+
versionConflict,
|
|
161
|
+
versionConflicts,
|
|
162
|
+
versionConflictsOf,
|
|
153
163
|
} from "./collection/index.js";
|
|
154
164
|
export type {
|
|
165
|
+
CurrentItem,
|
|
166
|
+
VersionConflict,
|
|
167
|
+
VersionOf,
|
|
168
|
+
StorageItemWriteOptions,
|
|
169
|
+
StorageBatchWriteOptions,
|
|
155
170
|
CollectionProceduresOptions,
|
|
156
171
|
CollectionChangeEventsOptions,
|
|
157
172
|
CollectionChangeEvent,
|
package/src/protocol.ts
CHANGED
|
@@ -48,7 +48,8 @@ export type ProtocolErrorCode =
|
|
|
48
48
|
| "CONNECTION_CLOSED"
|
|
49
49
|
| "CONNECTION_NOT_FOUND"
|
|
50
50
|
| "TOPIC_NOT_FOUND"
|
|
51
|
-
| "TOPIC_CLEARED"
|
|
51
|
+
| "TOPIC_CLEARED"
|
|
52
|
+
| "VERSION_CONFLICT";
|
|
52
53
|
|
|
53
54
|
/**
|
|
54
55
|
* The error codes of the protocol, on each face (HTTP, WebSocket, MCP, local). An error that a
|
|
@@ -80,6 +81,7 @@ export const ERROR_CODES: Readonly<Record<ProtocolErrorCode, ErrorCodeInfo>> = {
|
|
|
80
81
|
CONNECTION_NOT_FOUND: { http: 404, retryable: false, meaning: "The server has no connection with this id" },
|
|
81
82
|
TOPIC_NOT_FOUND: { http: 404, retryable: false, meaning: "The topic rule of the host has no topic with this name" },
|
|
82
83
|
TOPIC_CLEARED: { http: 410, retryable: false, meaning: "The host cleared a topic of the subscription: clear() of its bus removed the listener" },
|
|
84
|
+
VERSION_CONFLICT: { http: 409, retryable: false, meaning: "The current item does not have the version that the write expects. The metadata key conflicts has the current items" },
|
|
83
85
|
};
|
|
84
86
|
|
|
85
87
|
/** True when a code is an error code of the protocol. */
|
|
@@ -20,6 +20,7 @@ import { createCollectionProcedures } from "../procedures/collection/procedures.
|
|
|
20
20
|
import { InvocationError, invokeProcedure, type AdmitItem, type Interceptor, type Invocation } from "../procedures/invoke.js";
|
|
21
21
|
import type { Capability, ClearanceOrder, ProcedureContext, ResourcesOf } from "../procedures/types.js";
|
|
22
22
|
import type { CollectionPartitions } from "../procedures/collection/partition.js";
|
|
23
|
+
import { VERSION_CONFLICTS_METADATA_KEY, versionConflictsOf } from "../procedures/collection/precondition.js";
|
|
23
24
|
import { TIMEOUT_METADATA_KEY, effectsOf, joinNarrowing, narrowCapability } from "../procedures/capability.js";
|
|
24
25
|
import { IdempotencyCache, idempotencyKeyOf, isRepeatable, type IdempotencyOptions } from "../procedures/idempotency.js";
|
|
25
26
|
import { optionalMs, type Duration } from "../duration.js";
|
|
@@ -335,6 +336,9 @@ export function errorResponse(id: string, error: unknown): ServerResponse<unknow
|
|
|
335
336
|
const retryAfterMs = (error as { retryAfterMs?: unknown } | undefined)?.retryAfterMs;
|
|
336
337
|
const metadata: Record<string, unknown> = reason ? { reason } : {};
|
|
337
338
|
if (typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0) metadata["retry-after"] = retryAfterMs;
|
|
339
|
+
// A VERSION_CONFLICT error tells the current item of each conflict (roadmap CLI-16)
|
|
340
|
+
const conflicts = versionConflictsOf(error);
|
|
341
|
+
if (conflicts !== undefined) metadata[VERSION_CONFLICTS_METADATA_KEY] = conflicts;
|
|
338
342
|
const code = errorCodeOf(error);
|
|
339
343
|
return {
|
|
340
344
|
id,
|