@metreeca/keep 0.9.0 → 0.11.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/README.md +228 -24
- package/dist/clients/batching.core.d.ts +49 -0
- package/dist/clients/batching.core.d.ts.map +1 -0
- package/dist/clients/batching.core.js +159 -0
- package/dist/clients/batching.core.js.map +1 -0
- package/dist/clients/batching.d.ts +218 -0
- package/dist/clients/batching.d.ts.map +1 -0
- package/dist/clients/batching.js +109 -0
- package/dist/clients/batching.js.map +1 -0
- package/dist/clients/validating.d.ts +41 -0
- package/dist/clients/validating.d.ts.map +1 -0
- package/dist/clients/validating.js +150 -0
- package/dist/clients/validating.js.map +1 -0
- package/dist/index.d.ts +464 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/{states/index.js → index.js} +1 -1
- package/dist/index.js.map +1 -0
- package/dist/stores/caching.d.ts +93 -0
- package/dist/stores/caching.d.ts.map +1 -0
- package/dist/stores/caching.js +320 -0
- package/dist/stores/caching.js.map +1 -0
- package/dist/stores/managing.d.ts +57 -0
- package/dist/stores/managing.d.ts.map +1 -0
- package/dist/stores/managing.js +147 -0
- package/dist/stores/managing.js.map +1 -0
- package/package.json +34 -18
- package/dist/index/index.d.ts +0 -2
- package/dist/index/index.d.ts.map +0 -1
- package/dist/index/index.js +0 -17
- package/dist/index/index.js.map +0 -1
- package/dist/states/index.d.ts +0 -2
- package/dist/states/index.d.ts.map +0 -1
- package/dist/states/index.js.map +0 -1
- package/dist/stores/index.d.ts +0 -2
- package/dist/stores/index.d.ts.map +0 -1
- package/dist/stores/index.js +0 -17
- package/dist/stores/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://www.npmjs.com/package/@metreeca/keep)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
Core model-driven storage API for the [@metreeca/keep](https://github.com/metreeca/keep) linked data storage framework.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Provides one backend-agnostic API for persisting and retrieving linked data resources, validated against their models
|
|
8
|
+
and retrieved through client-defined templates and queries. Resource models are defined with
|
|
9
|
+
[@metreeca/blue](https://github.com/metreeca/blue), and resource states and retrieval templates follow the data and
|
|
10
|
+
query models of [@metreeca/qest](https://github.com/metreeca/qest). Actual storage is delegated to backend
|
|
11
|
+
[connector packages](https://github.com/metreeca/keep#installation).
|
|
8
12
|
|
|
9
13
|
# Installation
|
|
10
14
|
|
|
@@ -12,37 +16,237 @@
|
|
|
12
16
|
npm install @metreeca/keep
|
|
13
17
|
```
|
|
14
18
|
|
|
15
|
-
> [!WARNING]
|
|
16
|
-
>
|
|
17
|
-
> TypeScript consumers must use `"moduleResolution": "nodenext"/"node16"/"bundler"` in `tsconfig.json`.
|
|
18
|
-
> The legacy `"node"` resolver is not supported.
|
|
19
|
-
|
|
20
19
|
# Usage
|
|
21
20
|
|
|
22
21
|
> [!NOTE]
|
|
23
22
|
>
|
|
24
|
-
> This section introduces essential concepts; for complete coverage, see the
|
|
25
|
-
>
|
|
26
|
-
> | Module | Description |
|
|
27
|
-
> |--------|-------------|
|
|
28
|
-
> | {TBD} | {TBD} |
|
|
23
|
+
> This section introduces essential concepts; for complete coverage, see the
|
|
24
|
+
> [API reference](https://metreeca.github.io/keep/modules/_metreeca_keep.html).
|
|
29
25
|
|
|
30
|
-
|
|
26
|
+
| Module | Description |
|
|
27
|
+
|-----------------------------------------|--------------------------|
|
|
28
|
+
| [@metreeca/keep][keep] | Model-driven storage API |
|
|
29
|
+
| **Stores** | |
|
|
30
|
+
| [@metreeca/keep/caching][caching] | Caching store wrapper |
|
|
31
|
+
| [@metreeca/keep/managing][managing] | Managing store wrapper |
|
|
32
|
+
| **Store Clients** | |
|
|
33
|
+
| [@metreeca/keep/validating][validating] | Validating store wrapper |
|
|
34
|
+
| [@metreeca/keep/batching][batching] | Batching store client |
|
|
31
35
|
|
|
32
|
-
|
|
36
|
+
[keep]: https://metreeca.github.io/keep/modules/_metreeca_keep.html
|
|
33
37
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
38
|
+
[validating]: https://metreeca.github.io/keep/modules/_metreeca_keep.validating.html
|
|
39
|
+
|
|
40
|
+
[batching]: https://metreeca.github.io/keep/modules/_metreeca_keep.batching.html
|
|
41
|
+
|
|
42
|
+
[caching]: https://metreeca.github.io/keep/modules/_metreeca_keep.caching.html
|
|
43
|
+
|
|
44
|
+
[managing]: https://metreeca.github.io/keep/modules/_metreeca_keep.managing.html
|
|
45
|
+
|
|
46
|
+
All store methods accept an `entry` that MUST be an absolute IRI without query string or fragment: non-conforming
|
|
47
|
+
entries reject with `RangeError`. `create` takes as `entry` the resource holding the collection, and as `model` a slice
|
|
48
|
+
naming the multi-valued property that holds it. An `id` stated in a `create` state MUST be nested under `entry`.
|
|
49
|
+
`model` and `state` are validated against the supplied shape, and validation failures reject with a `TraceError`.
|
|
50
|
+
Network, storage, and other processing errors reject with a structured `Problem`.
|
|
51
|
+
|
|
52
|
+
## Retrieving Resources
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
// single resource retrieval
|
|
56
|
+
|
|
57
|
+
const product = await store.lookup({
|
|
58
|
+
entry: "http://example.com/products/1",
|
|
59
|
+
shape: ProductShape,
|
|
60
|
+
model: {
|
|
61
|
+
name: {},
|
|
62
|
+
price: {},
|
|
63
|
+
vendor: {
|
|
64
|
+
id: {},
|
|
65
|
+
name: {}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
// collection retrieval with filtering, ordering, and pagination
|
|
71
|
+
|
|
72
|
+
const catalogue = await store.lookup({
|
|
73
|
+
entry: "http://example.com/products/",
|
|
74
|
+
shape: CatalogueShape,
|
|
75
|
+
model: {
|
|
76
|
+
products: {
|
|
77
|
+
id: {},
|
|
78
|
+
name: {},
|
|
79
|
+
price: {},
|
|
80
|
+
">=price": 50, // price ≥ 50
|
|
81
|
+
"~name": "widget", // name contains "widget"
|
|
82
|
+
"^price": 1, // sort by price ascending
|
|
83
|
+
"@": 0, // offset
|
|
84
|
+
"#": 25 // limit
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Creating and Updating Resources
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// creation within the collection holding the new resource, its identifier minted by the store unless stated
|
|
94
|
+
|
|
95
|
+
const product = await store.create({
|
|
96
|
+
entry: "http://example.com/products/", shape: CatalogueShape, model: { products: {} }, state: {
|
|
97
|
+
name: "Widget",
|
|
98
|
+
price: 29.99,
|
|
99
|
+
vendor: "http://example.com/vendors/acme"
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
await store.update({
|
|
104
|
+
entry: "http://example.com/products/42", shape: ProductShape, state: {
|
|
105
|
+
id: "http://example.com/products/42",
|
|
106
|
+
name: "Widget",
|
|
107
|
+
price: 39.99,
|
|
108
|
+
vendor: "http://example.com/vendors/acme"
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Deleting Resources
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
await store.delete({ entry: "http://example.com/products/42", shape: ProductShape });
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Loading Data
|
|
120
|
+
|
|
121
|
+
```typescript
|
|
122
|
+
// unconditionally upsert (create or replace)
|
|
123
|
+
|
|
124
|
+
await store.insert({
|
|
125
|
+
entry: "http://example.com/products/42", shape: ProductShape, state: {
|
|
126
|
+
id: "http://example.com/products/42",
|
|
127
|
+
name: "Widget",
|
|
128
|
+
price: 39.99,
|
|
129
|
+
vendor: "http://example.com/vendors/acme"
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// unconditionally remove (silently succeeds if absent)
|
|
134
|
+
|
|
135
|
+
await store.remove({ entry: "http://example.com/products/42", shape: ProductShape });
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Observing Mutations
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
const unsubscribe = store.observe(mutations => {
|
|
142
|
+
for (const [id, exists] of Object.entries(mutations)) {
|
|
143
|
+
console.log(exists ? "upserted" : "removed", id);
|
|
144
|
+
}
|
|
145
|
+
}, "http://example.com/products/");
|
|
146
|
+
|
|
147
|
+
unsubscribe(); // stop receiving events
|
|
148
|
+
```
|
|
37
149
|
|
|
38
|
-
|
|
150
|
+
The second argument scopes the observer to the given resources and their descendants: pass a single reference or any
|
|
151
|
+
collection of them, omit it to receive all mutations, or pass an empty collection to skip registration altogether.
|
|
152
|
+
|
|
153
|
+
## Executing Transactions
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
await store.execute(async store => {
|
|
157
|
+
await store.create({ entry: catalogue, shape: CatalogueShape, model: { products: {} }, state: product });
|
|
158
|
+
await store.update({ entry: inventory.id, shape: InventoryShape, state: inventory });
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Cross-backend atomicity and isolation semantics, concurrency models, and the buffer-and-flush emulation pattern are
|
|
163
|
+
documented in the [Transaction Design](https://metreeca.github.io/keep/documents/_metreeca_keep.Transaction_Design.html)
|
|
164
|
+
companion document.
|
|
165
|
+
|
|
166
|
+
# Implementing Connectors
|
|
167
|
+
|
|
168
|
+
A backend connector implements the bare `StoreClient` interface against its native primitives, then wraps it with the
|
|
169
|
+
`createValidatingStore` helper from
|
|
170
|
+
[@metreeca/keep/validating](https://metreeca.github.io/keep/modules/_metreeca_keep.validating.html) for shape-driven
|
|
171
|
+
input validation and `createManagingStore` from
|
|
172
|
+
[@metreeca/keep/managing](https://metreeca.github.io/keep/modules/_metreeca_keep.managing.html) for mutation events,
|
|
173
|
+
transactional execution, and lifecycle:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import type { StoreClient, Store } from "@metreeca/keep";
|
|
177
|
+
import { createManagingStore } from "@metreeca/keep/managing";
|
|
178
|
+
import { createValidatingStore } from "@metreeca/keep/validating";
|
|
179
|
+
|
|
180
|
+
function createMyStore(): Store {
|
|
181
|
+
|
|
182
|
+
const backend: StoreClient = {
|
|
183
|
+
|
|
184
|
+
lookup({ entry, shape, model }, scope) { /* query the backend, return matching data or undefined */ },
|
|
185
|
+
|
|
186
|
+
create({ entry, shape, model, state }) { /* create under the collection, return the new entry or undefined */ },
|
|
187
|
+
update({ entry, shape, state }) { /* update if present, return entry or undefined */ },
|
|
188
|
+
delete({ entry, shape }) { /* delete if present, return entry or undefined */ },
|
|
189
|
+
|
|
190
|
+
insert({ entry, shape, state }) { /* upsert unconditionally, return entry */ },
|
|
191
|
+
remove({ entry, shape }) { /* remove unconditionally, return entry */ }
|
|
192
|
+
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
const client = createValidatingStore(backend, { trusted: true });
|
|
196
|
+
|
|
197
|
+
return createManagingStore({
|
|
198
|
+
|
|
199
|
+
execute: task => { /* run task(client) within a backend transaction */ },
|
|
200
|
+
close: () => { /* release connections */ }
|
|
201
|
+
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Pass `trusted: true` to `createValidatingStore` when the backend is trusted to deliver shape-conforming data, so
|
|
208
|
+
`lookup` responses skip the redundant validation pass. The `execute` option of `createManagingStore` is required: it runs
|
|
209
|
+
every standalone call and the entire `execute` task body within the backend's transaction primitive, and MUST call the
|
|
210
|
+
task with a store client dedicated to that transaction. A non-transactional backend supplies an `execute` applying the
|
|
211
|
+
task directly to the client, and documents that a failing task does not roll back. `close` defaults to a no-op.
|
|
212
|
+
|
|
213
|
+
All errors reach the caller as promise rejections through the unified store error channel: `RangeError` for malformed
|
|
214
|
+
entries, `TraceError` for shape validation failures, `Problem` for network, storage, or other processing failures.
|
|
215
|
+
Connectors MUST preserve this contract: convert backend-specific exceptions into `Problem` rejections and let validation
|
|
216
|
+
rejections propagate untouched.
|
|
217
|
+
|
|
218
|
+
Each connector MUST document its supported transaction isolation level and mutation event scope per the conventions
|
|
219
|
+
defined in the [Transaction Design](https://metreeca.github.io/keep/documents/_metreeca_keep.Transaction_Design.html)
|
|
220
|
+
companion document.
|
|
221
|
+
|
|
222
|
+
## Testing
|
|
223
|
+
|
|
224
|
+
Use [@metreeca/keep-suite](https://github.com/metreeca/keep/tree/main/packages/components/keep-suite) to run the full
|
|
225
|
+
conformance suite against your connector:
|
|
226
|
+
|
|
227
|
+
```typescript
|
|
228
|
+
import { testStore } from "@metreeca/keep-suite";
|
|
229
|
+
import { describe } from "vitest";
|
|
230
|
+
|
|
231
|
+
describe("my-store", () => testStore({
|
|
232
|
+
|
|
233
|
+
open: () => createMyStore(), // open a store with schema but no data
|
|
234
|
+
|
|
235
|
+
contains: entry => { /* true if the resource has any stored data */ },
|
|
236
|
+
includes: (probe, shape) => { /* true if every fact described by `probe` is present */ },
|
|
237
|
+
excludes: (probe, shape) => { /* true if every fact described by `probe` is absent */ },
|
|
238
|
+
|
|
239
|
+
populate: () => { /* clear and reload the sample dataset */ },
|
|
240
|
+
generate: (sample, shape) => { /* insert an isolated copy of `sample` with a unique id */ },
|
|
241
|
+
|
|
242
|
+
close: () => { /* optional: release store resources after all tests */ }
|
|
243
|
+
|
|
244
|
+
}));
|
|
245
|
+
```
|
|
39
246
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
| @metreeca/blue _(upcoming)_ | Shape-based validation for resources, patches, and queries |
|
|
44
|
-
| **@metreeca/keep** | Shape-driven storage framework with pluggable adapters |
|
|
45
|
-
| @metreeca/gate _(upcoming)_ | Shape-driven REST/JSON API publishing |
|
|
247
|
+
Narrow the run to a subset of sub-suites or tests with the `target` and `ignore` options (for example,
|
|
248
|
+
`target: ["Retrieve", "PersistCreate"]`); see the
|
|
249
|
+
[API reference](https://metreeca.github.io/keep/modules/_metreeca_keep-suite.html) for the full list.
|
|
46
250
|
|
|
47
251
|
# Support
|
|
48
252
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { ResourceShape } from "@metreeca/blue/resource";
|
|
2
|
+
import { type Lazy } from "@metreeca/core";
|
|
3
|
+
import type { Reference, Resource } from "@metreeca/qest/state";
|
|
4
|
+
import type { Broker, Detail, Detect, Handler, Modify, Select } from "./batching.js";
|
|
5
|
+
/**
|
|
6
|
+
* Creates a batching request {@link Broker}.
|
|
7
|
+
*
|
|
8
|
+
* Backs {@link createBatchingStore}: the broker queues each submitted request by type and serves the queues in rounds
|
|
9
|
+
* against the supplied handlers, running all four concurrently, until every queue is empty. Requests issued
|
|
10
|
+
* mid-round, whether forwarded by a handler or freshly submitted, are served in a later round, so handlers must not
|
|
11
|
+
* assume any ordering between handlers.
|
|
12
|
+
*
|
|
13
|
+
* A handler throw fails the whole run: every pending {@link Deferred} is rejected with the originating error, so
|
|
14
|
+
* callers see a deterministic failure rather than a hang.
|
|
15
|
+
*
|
|
16
|
+
* @param handlers The batch handlers, one per request type. Each settles every {@link Deferred} in
|
|
17
|
+
* its batch and may issue nested {@link Broker.detail} / {@link Broker.select} calls while
|
|
18
|
+
* processing; its returned promise must settle only after every owned request, including
|
|
19
|
+
* transitively nested ones, has settled.
|
|
20
|
+
*
|
|
21
|
+
* @returns An immutable {@link Broker} surface for enqueuing requests and receiving their
|
|
22
|
+
* materialised results; each call returns a promise that settles when the relevant batch
|
|
23
|
+
* completes
|
|
24
|
+
*/
|
|
25
|
+
export declare function createBroker(handlers: {
|
|
26
|
+
detect: Handler<Detect>;
|
|
27
|
+
detail: Handler<Detail>;
|
|
28
|
+
select: Handler<Select>;
|
|
29
|
+
modify: Handler<Modify>;
|
|
30
|
+
}): Broker;
|
|
31
|
+
/**
|
|
32
|
+
* Mints the identifier of a resource created under a collection.
|
|
33
|
+
*
|
|
34
|
+
* Returns an identifier nested under `entry`, matching the identifier {@link ResourceShape.pattern | pattern} declared
|
|
35
|
+
* by the shape, if any. Each `{name}` slot is filled with the like-named member of `state`, or with an opaque segment
|
|
36
|
+
* if the state has no such member. A trailing `/*` slot is always filled with an opaque segment. A root-relative
|
|
37
|
+
* pattern is resolved against `entry`. If the shape declares no pattern, the identifier is an opaque segment under
|
|
38
|
+
* `entry`.
|
|
39
|
+
*
|
|
40
|
+
* @param entry The absolute identifier of the resource collecting the new one
|
|
41
|
+
* @param shape The shape describing the new resource, possibly deferred to break definition cycles
|
|
42
|
+
* @param state The initial state of the new resource
|
|
43
|
+
*
|
|
44
|
+
* @returns The absolute identifier of the new resource
|
|
45
|
+
*
|
|
46
|
+
* @throws {@link !RangeError RangeError} If a member filling a slot is not a single non-empty path segment
|
|
47
|
+
*/
|
|
48
|
+
export declare function mint(entry: Reference, shape: Lazy<ResourceShape>, state: Resource): Reference;
|
|
49
|
+
//# sourceMappingURL=batching.core.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batching.core.d.ts","sourceRoot":"","sources":["../../src/clients/batching.core.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAE7D,OAAO,EAA8C,KAAK,IAAI,EAAE,MAAM,gBAAgB,CAAC;AAIvF,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAS,MAAM,sBAAsB,CAAC;AACvE,OAAO,KAAK,EAAE,MAAM,EAAY,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAW,MAAM,EAAE,MAAM,eAAe,CAAC;AAYxG;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE;IAEtC,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IACvB,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IACvB,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;IACvB,MAAM,EAAE,OAAO,CAAC,MAAM,CAAC,CAAA;CAEvB,GAAG,MAAM,CA+IT;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,IAAI,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,IAAI,CAAC,aAAa,CAAC,EAAE,KAAK,EAAE,QAAQ,GAAG,SAAS,CAwB7F"}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright © 2025-2026 Metreeca srl
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
import { eager } from "@metreeca/blue/value";
|
|
17
|
+
import { error, isNumber, isString } from "@metreeca/core";
|
|
18
|
+
import { resolve } from "@metreeca/core/resource";
|
|
19
|
+
import { immutable } from "@metreeca/core/values";
|
|
20
|
+
/**
|
|
21
|
+
* The form a value read off a state has to take to stand as a path segment: non-empty, with no delimiter or
|
|
22
|
+
* whitespace.
|
|
23
|
+
*/
|
|
24
|
+
const SegmentFormat = /^[^\s/?#]+$/;
|
|
25
|
+
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
26
|
+
/**
|
|
27
|
+
* Creates a batching request {@link Broker}.
|
|
28
|
+
*
|
|
29
|
+
* Backs {@link createBatchingStore}: the broker queues each submitted request by type and serves the queues in rounds
|
|
30
|
+
* against the supplied handlers, running all four concurrently, until every queue is empty. Requests issued
|
|
31
|
+
* mid-round, whether forwarded by a handler or freshly submitted, are served in a later round, so handlers must not
|
|
32
|
+
* assume any ordering between handlers.
|
|
33
|
+
*
|
|
34
|
+
* A handler throw fails the whole run: every pending {@link Deferred} is rejected with the originating error, so
|
|
35
|
+
* callers see a deterministic failure rather than a hang.
|
|
36
|
+
*
|
|
37
|
+
* @param handlers The batch handlers, one per request type. Each settles every {@link Deferred} in
|
|
38
|
+
* its batch and may issue nested {@link Broker.detail} / {@link Broker.select} calls while
|
|
39
|
+
* processing; its returned promise must settle only after every owned request, including
|
|
40
|
+
* transitively nested ones, has settled.
|
|
41
|
+
*
|
|
42
|
+
* @returns An immutable {@link Broker} surface for enqueuing requests and receiving their
|
|
43
|
+
* materialised results; each call returns a promise that settles when the relevant batch
|
|
44
|
+
* completes
|
|
45
|
+
*/
|
|
46
|
+
export function createBroker(handlers) {
|
|
47
|
+
const queues = Object.fromEntries(Object.entries(handlers).map(([key, handler]) => {
|
|
48
|
+
const items = [];
|
|
49
|
+
return [key, {
|
|
50
|
+
items,
|
|
51
|
+
// snapshots the queued requests and hands them to the handler, keeping them queued until the round
|
|
52
|
+
// is settled by the drain loop, which removes exactly the snapshot in the same step it re-checks the
|
|
53
|
+
// queues: a queue emptied any earlier would let a submission landing in between kick a second drain
|
|
54
|
+
// over the same snapshot, whose own removal would then drop whatever was queued behind it
|
|
55
|
+
round: () => {
|
|
56
|
+
const batch = items.slice();
|
|
57
|
+
return {
|
|
58
|
+
done: batch.length > 0 ? handler(batch, broker) : Promise.resolve(),
|
|
59
|
+
settle: () => { items.splice(0, batch.length); }
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
purge: error => {
|
|
63
|
+
items.forEach(deferred => deferred.reject(error));
|
|
64
|
+
items.length = 0;
|
|
65
|
+
}
|
|
66
|
+
}];
|
|
67
|
+
}));
|
|
68
|
+
// Inner surface handed to the handlers: nested calls enqueue without kicking a drain.
|
|
69
|
+
const broker = immutable({
|
|
70
|
+
detect(request) {
|
|
71
|
+
return new Promise((resolve, reject) => queues.detect.items.push({ request, resolve, reject }));
|
|
72
|
+
},
|
|
73
|
+
detail(request) {
|
|
74
|
+
return new Promise((resolve, reject) => queues.detail.items.push({ request, resolve, reject }));
|
|
75
|
+
},
|
|
76
|
+
select(request) {
|
|
77
|
+
return new Promise((resolve, reject) => queues.select.items.push({ request, resolve, reject }));
|
|
78
|
+
},
|
|
79
|
+
modify(request) {
|
|
80
|
+
return new Promise((resolve, reject) => queues.modify.items.push({ request, resolve, reject }));
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
// Public surface: every call enqueues through the inner broker and kicks a drain via `process`.
|
|
84
|
+
return immutable({
|
|
85
|
+
detect(request) {
|
|
86
|
+
return process(() => broker.detect(request));
|
|
87
|
+
},
|
|
88
|
+
detail(request) {
|
|
89
|
+
return process(() => broker.detail(request));
|
|
90
|
+
},
|
|
91
|
+
select(request) {
|
|
92
|
+
return process(() => broker.select(request));
|
|
93
|
+
},
|
|
94
|
+
modify(request) {
|
|
95
|
+
return process(() => broker.modify(request));
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
// Kicks a drain only when every queue is idle: a running drain keeps its snapshots queued until the
|
|
99
|
+
// step that re-checks the queues, so "all empty" means "no drain in flight". The kick is a microtask,
|
|
100
|
+
// so a synchronous burst of submissions coalesces into one drain. The check runs before `submit`
|
|
101
|
+
// enqueues, or it would always see the request it just pushed.
|
|
102
|
+
function process(submit) {
|
|
103
|
+
if (Object.values(queues).every(queue => queue.items.length === 0)) {
|
|
104
|
+
Promise.resolve().then(async () => {
|
|
105
|
+
// Drain in rounds until every queue is quiescent, all four handlers concurrently per
|
|
106
|
+
// round. Every snapshot stays queued across the `await` and is removed in the same
|
|
107
|
+
// synchronous step as the next idle check, so no submission can ever find the queues empty
|
|
108
|
+
// while the loop is alive; anything pushed mid-round is served next round. A throw leaves
|
|
109
|
+
// the snapshots queued to purge.
|
|
110
|
+
try {
|
|
111
|
+
while (Object.values(queues).some(queue => queue.items.length > 0)) {
|
|
112
|
+
const rounds = Object.values(queues).map(queue => queue.round());
|
|
113
|
+
await Promise.all(rounds.map(round => round.done));
|
|
114
|
+
rounds.forEach(round => round.settle());
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
// A handler throw poisons the run: purge every queue so pending callers reject
|
|
119
|
+
// instead of hanging; already-settled entries no-op.
|
|
120
|
+
Object.values(queues).forEach(queue => queue.purge(error));
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return submit();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Mints the identifier of a resource created under a collection.
|
|
129
|
+
*
|
|
130
|
+
* Returns an identifier nested under `entry`, matching the identifier {@link ResourceShape.pattern | pattern} declared
|
|
131
|
+
* by the shape, if any. Each `{name}` slot is filled with the like-named member of `state`, or with an opaque segment
|
|
132
|
+
* if the state has no such member. A trailing `/*` slot is always filled with an opaque segment. A root-relative
|
|
133
|
+
* pattern is resolved against `entry`. If the shape declares no pattern, the identifier is an opaque segment under
|
|
134
|
+
* `entry`.
|
|
135
|
+
*
|
|
136
|
+
* @param entry The absolute identifier of the resource collecting the new one
|
|
137
|
+
* @param shape The shape describing the new resource, possibly deferred to break definition cycles
|
|
138
|
+
* @param state The initial state of the new resource
|
|
139
|
+
*
|
|
140
|
+
* @returns The absolute identifier of the new resource
|
|
141
|
+
*
|
|
142
|
+
* @throws {@link !RangeError RangeError} If a member filling a slot is not a single non-empty path segment
|
|
143
|
+
*/
|
|
144
|
+
export function mint(entry, shape, state) {
|
|
145
|
+
const { pattern } = eager(shape);
|
|
146
|
+
return pattern === undefined
|
|
147
|
+
? resolve(entry.endsWith("/") ? entry : `${entry}/`, crypto.randomUUID())
|
|
148
|
+
: resolve(entry, pattern.replace(/\{(\w*)}|(?<=\/)\*$/g, (_, name) => name === undefined ? crypto.randomUUID() : segment(name)));
|
|
149
|
+
/**
|
|
150
|
+
* Reads a pattern slot off the like-named member of the state, or fills it with an opaque segment.
|
|
151
|
+
*/
|
|
152
|
+
function segment(name) {
|
|
153
|
+
const value = state[name];
|
|
154
|
+
return value === undefined ? crypto.randomUUID()
|
|
155
|
+
: (isString(value) || isNumber(value)) && SegmentFormat.test(String(value)) ? String(value)
|
|
156
|
+
: error(new RangeError(`illegal identifier segment <${String(value)}> for slot <${name}>`));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
//# sourceMappingURL=batching.core.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"batching.core.js","sourceRoot":"","sources":["../../src/clients/batching.core.ts"],"names":[],"mappings":"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"}
|