@metreeca/keep 0.9.0 → 0.10.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.
Files changed (37) hide show
  1. package/README.md +221 -24
  2. package/dist/clients/batching.core.d.ts +29 -0
  3. package/dist/clients/batching.core.d.ts.map +1 -0
  4. package/dist/clients/batching.core.js +114 -0
  5. package/dist/clients/batching.core.js.map +1 -0
  6. package/dist/clients/batching.d.ts +223 -0
  7. package/dist/clients/batching.d.ts.map +1 -0
  8. package/dist/clients/batching.js +105 -0
  9. package/dist/clients/batching.js.map +1 -0
  10. package/dist/clients/validating.d.ts +38 -0
  11. package/dist/clients/validating.d.ts.map +1 -0
  12. package/dist/clients/validating.js +128 -0
  13. package/dist/clients/validating.js.map +1 -0
  14. package/dist/index.d.ts +358 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/{states/index.js → index.js} +1 -1
  17. package/dist/index.js.map +1 -0
  18. package/dist/stores/caching.d.ts +96 -0
  19. package/dist/stores/caching.d.ts.map +1 -0
  20. package/dist/stores/caching.js +311 -0
  21. package/dist/stores/caching.js.map +1 -0
  22. package/dist/stores/managing.d.ts +55 -0
  23. package/dist/stores/managing.d.ts.map +1 -0
  24. package/dist/stores/managing.js +159 -0
  25. package/dist/stores/managing.js.map +1 -0
  26. package/package.json +33 -17
  27. package/dist/index/index.d.ts +0 -2
  28. package/dist/index/index.d.ts.map +0 -1
  29. package/dist/index/index.js +0 -17
  30. package/dist/index/index.js.map +0 -1
  31. package/dist/states/index.d.ts +0 -2
  32. package/dist/states/index.d.ts.map +0 -1
  33. package/dist/states/index.js.map +0 -1
  34. package/dist/stores/index.d.ts +0 -2
  35. package/dist/stores/index.d.ts.map +0 -1
  36. package/dist/stores/index.js +0 -17
  37. package/dist/stores/index.js.map +0 -1
package/README.md CHANGED
@@ -2,9 +2,13 @@
2
2
 
3
3
  [![npm](https://img.shields.io/npm/v/@metreeca/keep)](https://www.npmjs.com/package/@metreeca/keep)
4
4
 
5
- {TBD: one-line description}
5
+ Core model-driven storage API for the [@metreeca/keep](https://github.com/metreeca/keep) linked data storage framework.
6
6
 
7
- {TBD: value proposition and key capabilities}
7
+ Defines the backend-agnostic `StoreClient` and `Store` interfaces for persisting and retrieving linked data resources as
8
+ shape-validated states and query projections. Resource shapes are defined using
9
+ [@metreeca/blue](https://github.com/metreeca/blue); resource states and retrieval templates follow the data and query
10
+ models defined by [@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,230 @@
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 API reference:
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).
25
+
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 |
35
+
36
+ [keep]: https://metreeca.github.io/keep/modules/_metreeca_keep.html
37
+
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`. `model` and `state` are validated against the supplied shape; validation failures
48
+ reject with a `TraceError`. Network, storage, and other processing errors reject with a structured `Problem`.
49
+
50
+ ## Retrieving Resources
51
+
52
+ ```typescript
53
+ // single resource retrieval
54
+
55
+ const product = await store.lookup({
56
+ entry: "http://example.com/products/1",
57
+ shape: ProductShape,
58
+ model: {
59
+ name: "",
60
+ price: 0,
61
+ vendor: {
62
+ id: "",
63
+ name: ""
64
+ }
65
+ }
66
+ });
67
+
68
+ // collection retrieval with filtering, ordering, and pagination
69
+
70
+ const catalog = await store.lookup({
71
+ entry: "http://example.com/products/",
72
+ shape: ProductShape,
73
+ model: {
74
+ products: [{
75
+ id: "",
76
+ name: "",
77
+ price: 0,
78
+ ">=price": 50, // price ≥ 50
79
+ "~name": "widget", // name contains "widget"
80
+ "^price": 1, // sort by price ascending
81
+ "@": 0, // offset
82
+ "#": 25 // limit
83
+ }]
84
+ }
85
+ });
86
+ ```
29
87
 
30
- {TBD: usage overview and examples}
88
+ ## Creating and Updating Resources
89
+
90
+ ```typescript
91
+ await store.create({
92
+ entry: "http://example.com/products/42", shape: ProductShape, state: {
93
+ id: "http://example.com/products/42",
94
+ name: "Widget",
95
+ price: 29.99,
96
+ vendor: "http://example.com/vendors/acme"
97
+ }
98
+ });
99
+
100
+ await store.update({
101
+ entry: "http://example.com/products/42", shape: ProductShape, state: {
102
+ id: "http://example.com/products/42",
103
+ name: "Widget",
104
+ price: 39.99,
105
+ vendor: "http://example.com/vendors/acme"
106
+ }
107
+ });
108
+ ```
31
109
 
32
- # Integrated Ecosystem
110
+ ## Deleting Resources
33
111
 
34
- > [!IMPORTANT]
35
- >
36
- > **@metreeca/keep** defines {TBD}; applications are free to {TBD}.
112
+ ```typescript
113
+ await store.delete({ entry: "http://example.com/products/42", shape: ProductShape });
114
+ ```
115
+
116
+ ## Loading Data
117
+
118
+ ```typescript
119
+ // unconditionally upsert (create or replace)
120
+
121
+ await store.insert({
122
+ entry: "http://example.com/products/42", shape: ProductShape, state: {
123
+ id: "http://example.com/products/42",
124
+ name: "Widget",
125
+ price: 39.99,
126
+ vendor: "http://example.com/vendors/acme"
127
+ }
128
+ });
129
+
130
+ // unconditionally remove (silently succeeds if absent)
131
+
132
+ await store.remove({ entry: "http://example.com/products/42", shape: ProductShape });
133
+ ```
134
+
135
+ ## Observing Mutations
136
+
137
+ ```typescript
138
+ const unsubscribe = store.observe(mutations => {
139
+ for (const [id, exists] of Object.entries(mutations)) {
140
+ console.log(exists ? "upserted" : "removed", id);
141
+ }
142
+ }, "http://example.com/products/");
143
+
144
+ unsubscribe(); // stop receiving events
145
+ ```
146
+
147
+ The second argument scopes the observer to the given resources and their descendants; omit it to receive all mutations,
148
+ pass an empty array to detach the observer.
149
+
150
+ ## Executing Transactions
151
+
152
+ ```typescript
153
+ await store.execute(async store => {
154
+ await store.create({ entry: product.id, shape: ProductShape, state: product });
155
+ await store.update({ entry: inventory.id, shape: InventoryShape, state: inventory });
156
+ });
157
+ ```
158
+
159
+ Cross-backend isolation semantics, concurrency models, and the buffer-and-flush emulation pattern are documented in the
160
+ [Transaction Design](https://metreeca.github.io/keep/documents/_metreeca_keep.Transaction_Design.html) companion
161
+ document.
162
+
163
+ # Implementing Connectors
164
+
165
+ A backend connector implements the bare `StoreClient` interface against its native primitives, then wraps it with the
166
+ `createValidatingStore` helper from
167
+ [@metreeca/keep/validating](https://metreeca.github.io/keep/modules/_metreeca_keep.validating.html) for shape-driven
168
+ input validation and `createManagingStore` from
169
+ [@metreeca/keep/managing](https://metreeca.github.io/keep/modules/_metreeca_keep.managing.html) for mutation events,
170
+ transactional execution, and lifecycle:
37
171
 
38
- **@metreeca/keep** is part of an integrated ecosystem for rapid application development:
172
+ ```typescript
173
+ import type { StoreClient, Store } from "@metreeca/keep";
174
+ import { createManagingStore } from "@metreeca/keep/managing";
175
+ import { createValidatingStore } from "@metreeca/keep/validating";
176
+
177
+ function createMyStore(): Store {
178
+
179
+ const backend: StoreClient = {
180
+
181
+ lookup({ entry, shape, model }) { /* query the backend, return matching data or undefined */ },
182
+
183
+ create({ entry, shape, state }) { /* create if absent, return entry or undefined */ },
184
+ update({ entry, shape, state }) { /* update if present, return entry or undefined */ },
185
+ delete({ entry, shape }) { /* delete if present, return entry or undefined */ },
186
+
187
+ insert({ entry, shape, state }) { /* upsert unconditionally, return entry */ },
188
+ remove({ entry, shape }) { /* remove unconditionally, return entry */ }
189
+
190
+ };
191
+
192
+ return createManagingStore(createValidatingStore(backend, { trusted: true }), {
193
+
194
+ execute: task => { /* run task within a backend transaction */ },
195
+ close: () => { /* release connections */ }
196
+
197
+ });
198
+
199
+ }
200
+ ```
201
+
202
+ Pass `trusted: true` to `createValidatingStore` when the backend is trusted to deliver shape-conforming data, so
203
+ `lookup` responses skip the redundant outbound validation pass. Supply `execute` to `createManagingStore` to route every
204
+ standalone call and the entire `execute` task body through the backend's transaction primitive; omit it for
205
+ non-transactional backends and the wrapper degrades to per-call atomicity. `close` defaults to a no-op.
206
+
207
+ All errors reach the caller as promise rejections through the unified Store error channel — `RangeError` for malformed
208
+ entries, `TraceError` for shape validation failures, `Problem` for network, storage, or other processing failures.
209
+ Connectors MUST preserve this contract: convert backend-specific exceptions into `Problem` rejections and let validation
210
+ rejections propagate untouched.
211
+
212
+ Each connector MUST document its supported transaction isolation level and mutation event scope per the conventions
213
+ defined in the [Transaction Design](https://metreeca.github.io/keep/documents/_metreeca_keep.Transaction_Design.html)
214
+ companion document.
215
+
216
+ ## Testing
217
+
218
+ Use [@metreeca/keep-suite](https://github.com/metreeca/keep/tree/main/packages/keep-suite) to run the full conformance
219
+ suite against your connector:
220
+
221
+ ```typescript
222
+ import { testStore } from "@metreeca/keep-suite";
223
+ import { describe } from "vitest";
224
+
225
+ describe("my-store", () => testStore({
226
+
227
+ build: () => createMyStore(), // create a store with schema but no data
228
+
229
+ contains: id => { /* true if the resource has any stored data */ },
230
+ includes: (resource, shape) => { /* true if every fact described by `resource` is present */ },
231
+ excludes: (resource, shape) => { /* true if every fact described by `resource` is absent */ },
232
+
233
+ populate: () => { /* clear and reload the sample dataset */ },
234
+ generate: (sample, shape) => { /* insert an isolated copy of `sample` with a unique id */ },
235
+
236
+ close: () => { /* optional: release store resources after all tests */ }
237
+
238
+ }));
239
+ ```
39
240
 
40
- | Package | Description |
41
- |-----------------------------|------------------------------------------------------------|
42
- | @metreeca/qest | Data types for client-driven, queryable REST/JSON APIs |
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 |
241
+ Filter to a subset of sub-suites with the `match` option (for example, `["Retrieve", "PersistCreate"]`); see the
242
+ [API reference](https://metreeca.github.io/keep/modules/_metreeca_keep-suite.html) for the full list.
46
243
 
47
244
  # Support
48
245
 
@@ -0,0 +1,29 @@
1
+ import type { Broker, Detect, Handler, Lookup, Modify, Select } from "./batching.js";
2
+ /**
3
+ * Creates a batching request {@link Broker}.
4
+ *
5
+ * Backs {@link createBatchingStore}: returns a broker that queues each submitted request by type and
6
+ * drives the queues to completion against the supplied handlers. The first submission kicks a drain
7
+ * loop that runs all four handlers concurrently, round by round, until every queue is quiescent;
8
+ * requests issued mid-round, whether forwarded by a handler or freshly submitted, are served on a
9
+ * later round, so handlers must not assume any ordering between handlers.
10
+ *
11
+ * Handler errors poison the run: every still-pending {@link Deferred} is rejected with the
12
+ * originating error, so callers see a deterministic failure rather than a hang.
13
+ *
14
+ * @param handlers The batch handlers, one per request type. Each settles every {@link Deferred} in
15
+ * its batch and may issue nested {@link Broker.lookup} / {@link Broker.select} calls while
16
+ * processing; its returned promise must settle only after every owned request, including
17
+ * transitively nested ones, has settled.
18
+ *
19
+ * @returns An immutable {@link Broker} surface for enqueuing requests and receiving their
20
+ * materialised results; each call returns a promise that settles when the relevant batch
21
+ * completes
22
+ */
23
+ export declare function createBroker(handlers: {
24
+ detect: Handler<Detect>;
25
+ lookup: Handler<Lookup>;
26
+ select: Handler<Select>;
27
+ modify: Handler<Modify>;
28
+ }): Broker;
29
+ //# 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":"AAoBA,OAAO,KAAK,EAAE,MAAM,EAAY,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAW,MAAM,EAAE,MAAM,eAAe,CAAC;AAExG;;;;;;;;;;;;;;;;;;;;GAoBG;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,CAgIT"}
@@ -0,0 +1,114 @@
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 { immutable } from "@metreeca/core/structures";
17
+ /**
18
+ * Creates a batching request {@link Broker}.
19
+ *
20
+ * Backs {@link createBatchingStore}: returns a broker that queues each submitted request by type and
21
+ * drives the queues to completion against the supplied handlers. The first submission kicks a drain
22
+ * loop that runs all four handlers concurrently, round by round, until every queue is quiescent;
23
+ * requests issued mid-round, whether forwarded by a handler or freshly submitted, are served on a
24
+ * later round, so handlers must not assume any ordering between handlers.
25
+ *
26
+ * Handler errors poison the run: every still-pending {@link Deferred} is rejected with the
27
+ * originating error, so callers see a deterministic failure rather than a hang.
28
+ *
29
+ * @param handlers The batch handlers, one per request type. Each settles every {@link Deferred} in
30
+ * its batch and may issue nested {@link Broker.lookup} / {@link Broker.select} calls while
31
+ * processing; its returned promise must settle only after every owned request, including
32
+ * transitively nested ones, has settled.
33
+ *
34
+ * @returns An immutable {@link Broker} surface for enqueuing requests and receiving their
35
+ * materialised results; each call returns a promise that settles when the relevant batch
36
+ * completes
37
+ */
38
+ export function createBroker(handlers) {
39
+ const queues = Object.fromEntries(Object.entries(handlers).map(([key, handler]) => {
40
+ const items = [];
41
+ return [key, {
42
+ items,
43
+ drain: async () => {
44
+ if (items.length > 0) {
45
+ const batch = items.slice();
46
+ await handler(batch, broker); // ;(cast) erased to the handler's union parameter
47
+ items.splice(0, batch.length);
48
+ }
49
+ },
50
+ purge: error => {
51
+ items.forEach(deferred => deferred.reject(error));
52
+ items.length = 0;
53
+ }
54
+ }];
55
+ }));
56
+ // Inner surface handed to the handlers: nested calls enqueue without kicking a drain.
57
+ const broker = immutable({
58
+ detect(request) {
59
+ return new Promise((resolve, reject) => queues.detect.items.push({ request, resolve, reject }));
60
+ },
61
+ lookup(request) {
62
+ return new Promise((resolve, reject) => queues.lookup.items.push({ request, resolve, reject }));
63
+ },
64
+ select(request) {
65
+ return new Promise((resolve, reject) => queues.select.items.push({ request, resolve, reject }));
66
+ },
67
+ modify(request) {
68
+ return new Promise((resolve, reject) => queues.modify.items.push({ request, resolve, reject }));
69
+ }
70
+ });
71
+ // Public surface: every call enqueues through the inner broker and kicks a drain via `process`.
72
+ return immutable({
73
+ detect(request) {
74
+ return process(() => broker.detect(request));
75
+ },
76
+ lookup(request) {
77
+ return process(() => broker.lookup(request));
78
+ },
79
+ select(request) {
80
+ return process(() => broker.select(request));
81
+ },
82
+ modify(request) {
83
+ return process(() => broker.modify(request));
84
+ }
85
+ });
86
+ // Kicks a drain only when every queue is idle: a running drain keeps its snapshot queued, so
87
+ // "all empty" means "no drain in flight". The kick is a microtask, so a synchronous burst of
88
+ // submissions coalesces into one drain. The check runs before `submit` enqueues, or it would
89
+ // always see the request it just pushed.
90
+ function process(submit) {
91
+ // a running drain keeps its snapshot in the queues until the round ends, so any non-empty
92
+ // queue means a drain is already in flight; all-empty is the only "no drain running" state
93
+ if (Object.values(queues).every(queue => queue.items.length === 0)) {
94
+ Promise.resolve().then(async () => {
95
+ // Drain in rounds until every queue is quiescent, all four handlers concurrently per
96
+ // round. A handler keeps its snapshot in the queue across the `await` (so the idle-check
97
+ // still sees work and won't kick a second drain) and removes exactly it on success;
98
+ // anything pushed mid-round is served next round. A throw leaves the snapshot queued to purge.
99
+ try {
100
+ while (Object.values(queues).some(queue => queue.items.length > 0)) {
101
+ await Promise.all(Object.values(queues).map(queue => queue.drain()));
102
+ }
103
+ }
104
+ catch (error) {
105
+ // A handler throw poisons the run: purge every queue so pending callers reject
106
+ // instead of hanging; already-settled entries no-op.
107
+ Object.values(queues).forEach(queue => queue.purge(error));
108
+ }
109
+ });
110
+ }
111
+ return submit();
112
+ }
113
+ }
114
+ //# 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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"}
@@ -0,0 +1,223 @@
1
+ /**
2
+ * Batching store client.
3
+ *
4
+ * Builds a {@link StoreClient} that coalesces concurrent requests of the same kind into a single
5
+ * batch, so a connector can serve many requests with one backend round-trip. A connector author
6
+ * supplies one {@link Handler} per request type to {@link createBatchingStore}; the returned client
7
+ * routes every call through a {@link Broker} that queues like-typed requests and hands each handler
8
+ * the whole batch it accumulated.
9
+ *
10
+ * Four request types partition the work, each with its own handler:
11
+ *
12
+ * - **`detect`** — probes whether each {@link Detect} entry exists in the store
13
+ * - **`lookup`** — materialises each {@link Lookup} against its `model`; multi-valued slots reached
14
+ * by the model may be delegated to the `select` handler via {@link Broker.select}
15
+ * - **`select`** — materialises each {@link Select} collection (see {@link Query} for the admitted
16
+ * forms); element resources reached by the query may be delegated to the `lookup` handler via
17
+ * {@link Broker.lookup}
18
+ * - **`modify`** — creates, updates, or deletes each {@link Modify} target
19
+ *
20
+ * Retrieval is driven by the requested `model` and `query`, not by the stored graph: `lookup` and
21
+ * `select` hand work back and forth, `lookup` spawning a `select` for each multi-valued slot its
22
+ * model reaches and `select` spawning a `lookup` for each element resource its query reaches. The
23
+ * handover recurses through this alternation and bottoms out where the `model` or `query` stops
24
+ * nesting, so each request descends exactly as deep as it asks for, however deep the underlying
25
+ * graph runs.
26
+ *
27
+ * Handlers run concurrently and forward nested requests to one another through the {@link Broker}, so
28
+ * a handler must not assume any ordering between handlers, and must await every nested promise it
29
+ * issues before settling the request that depends on it. Correctness rests on this data dependency
30
+ * alone, not on handler scheduling.
31
+ *
32
+ * @module
33
+ */
34
+ import type { Property, ResourceShape } from "@metreeca/blue/resource";
35
+ import type { Lazy } from "@metreeca/core";
36
+ import type { Tag } from "@metreeca/core/language";
37
+ import type { Reference, Resource } from "@metreeca/qest/resource";
38
+ import type { Instance, Query, Template } from "@metreeca/qest/template";
39
+ import type { StoreClient } from "../index.js";
40
+ /**
41
+ * Requests accepted by {@link Broker}.
42
+ */
43
+ export type Request = Detect | Lookup | Select | Modify;
44
+ /**
45
+ * Result of running a {@link Request}, narrowed by request variant:
46
+ *
47
+ * - a {@link Detect} resolves to a `boolean` existence flag
48
+ * - a {@link Lookup} resolves to its materialised resource {@link Instance}
49
+ * - a {@link Select} resolves to its materialised collection {@link Instance}
50
+ * - a {@link Modify} resolves to its mutated entry's {@link Reference}
51
+ *
52
+ * @typeParam R The request whose result type is selected
53
+ */
54
+ export type Response<R extends Request> = R extends Select<infer T> ? Instance<T> : R extends Lookup<infer T> ? Instance<T> : R extends Modify ? Reference : R extends Detect ? boolean : never;
55
+ /**
56
+ * Resource existence request.
57
+ *
58
+ * Carries the `entry` to probe; resolves to whether that entry exists in the store.
59
+ */
60
+ export type Detect = {
61
+ readonly entry: Reference;
62
+ };
63
+ /**
64
+ * Resource retrieval request.
65
+ *
66
+ * Carries a single resource (`entry` of `shape`), the `model` against which to materialise it, and
67
+ * the `locale` priority driving language negotiation for its localised content (§6.2). Resolves to
68
+ * the materialised resource.
69
+ *
70
+ * @typeParam T The resource model selecting the result shape
71
+ */
72
+ export type Lookup<T extends Template = Template> = {
73
+ readonly entry: Reference;
74
+ readonly shape: Lazy<ResourceShape>;
75
+ readonly model: T;
76
+ readonly locale: readonly Tag[];
77
+ };
78
+ /**
79
+ * Collection retrieval request.
80
+ *
81
+ * Carries a single multi-valued property (`field` on `entry` of `shape`), the `query` describing the
82
+ * shape of its values (one of the {@link Query} arms), and the `locale` priority driving language
83
+ * negotiation for its localised content (§6.2). Resolves to the materialised collection.
84
+ *
85
+ * @typeParam T The collection query selecting the result shape
86
+ */
87
+ export type Select<T extends Query = Query> = {
88
+ readonly entry: Reference;
89
+ readonly shape: Lazy<ResourceShape>;
90
+ readonly field: Property;
91
+ readonly query: T;
92
+ readonly locale: readonly Tag[];
93
+ };
94
+ /**
95
+ * Resource mutation request.
96
+ *
97
+ * Carries the `entry` to mutate (of `shape`) and the target `state` to persist: a present `state`
98
+ * creates or updates the entry, an omitted `state` deletes it. Resolves to the mutated entry's
99
+ * {@link Reference}.
100
+ */
101
+ export type Modify = {
102
+ readonly entry: Reference;
103
+ readonly shape: Lazy<ResourceShape>;
104
+ readonly state?: Resource;
105
+ };
106
+ /**
107
+ * Batch handler for a single request type.
108
+ *
109
+ * Supplied by the connector author to {@link createBatchingStore}, one per request type. Receives the
110
+ * whole batch the {@link Broker} has accumulated for this type, together with the broker for issuing
111
+ * nested requests, and settles each {@link Deferred} in the batch with its result or error. The
112
+ * returned promise must resolve only once every request the handler owns has settled, including any
113
+ * nested requests issued through the broker.
114
+ *
115
+ * The broker never dispatches an empty batch, so the handler need not guard against one.
116
+ *
117
+ * @typeParam R The request variant handled
118
+ */
119
+ export type Handler<R extends Request> = {
120
+ (batch: readonly Deferred<R>[], broker: Broker): Promise<void>;
121
+ };
122
+ /**
123
+ * A queued request paired with its promise-resolution hooks.
124
+ *
125
+ * Each batch a {@link Handler} receives is an array of `Deferred` entries. The handler reads each
126
+ * `request`, then calls `resolve` with its result (typed by {@link Response | Response<R>}) or
127
+ * `reject` with an error, once the backend round-trip and any nested requests issued through the
128
+ * {@link Broker} have settled.
129
+ *
130
+ * @typeParam R The queued request variant: a {@link Detect} probe, a {@link Lookup} resource fetch,
131
+ * a {@link Select} collection fetch, or a {@link Modify} mutation
132
+ */
133
+ export type Deferred<R extends Request> = {
134
+ readonly request: R;
135
+ resolve(value: Response<R>): void;
136
+ reject(error: unknown): void;
137
+ };
138
+ /**
139
+ * Request-submission surface over the batching {@link Handler | handlers}.
140
+ *
141
+ * Brokers each store request (detection, lookup, selection, modification) to its handler: a call
142
+ * enqueues the request and returns a promise that settles with the request's result. Held both by
143
+ * outside callers, as the entry point, and by handlers, to delegate nested work to a sibling
144
+ * handler.
145
+ */
146
+ export type Broker = {
147
+ /**
148
+ * Enqueue a {@link Detect} request.
149
+ *
150
+ * @param request The entry to probe
151
+ *
152
+ * @returns A promise resolving to `true` when the request's entry exists in the store; `false`
153
+ * otherwise
154
+ */
155
+ detect(request: Detect): Promise<boolean>;
156
+ /**
157
+ * Enqueue a {@link Lookup} request.
158
+ *
159
+ * Carries no existence precondition: an entry with no stored state resolves to an empty instance of
160
+ * the request's `model` rather than rejecting.
161
+ *
162
+ * @param request The resource and model to materialise
163
+ *
164
+ * @returns A promise resolving to the materialised {@link Instance} of the request's `model` (the
165
+ * resource value), settled once the owning batch and any nested promises needed to assemble its
166
+ * value have resolved
167
+ */
168
+ lookup<T extends Template>(request: Lookup<T>): Promise<Instance<T>>;
169
+ /**
170
+ * Enqueue a {@link Select} request.
171
+ *
172
+ * Carries no existence precondition: an entry with no matching members resolves to an empty
173
+ * collection rather than rejecting.
174
+ *
175
+ * @param request The property and query to materialise
176
+ *
177
+ * @returns A promise resolving to the materialised {@link Instance} of the request's `query` (one
178
+ * of the tuple-wrapped {@link Query} arms), settled once the owning batch and any nested promises
179
+ * needed to assemble its value have resolved
180
+ */
181
+ select<T extends Query>(request: Select<T>): Promise<Instance<T>>;
182
+ /**
183
+ * Enqueue a {@link Modify} request.
184
+ *
185
+ * Mutates unconditionally, with no existence precondition: a present `state` upserts the entry,
186
+ * creating it when absent; an omitted `state` deletes it leniently, resolving as a no-op when
187
+ * absent.
188
+ *
189
+ * @param request The entry and target state to persist
190
+ *
191
+ * @returns A promise resolving to the mutated entry's {@link Reference}, settled once the owning
192
+ * batch has completed
193
+ */
194
+ modify(request: Modify): Promise<Reference>;
195
+ };
196
+ /**
197
+ * Creates a batching store client backed by a set of request handlers.
198
+ *
199
+ * Routes every {@link StoreClient} call through a {@link Broker} that coalesces concurrent requests
200
+ * into batches and dispatches each batch through the matching `handlers` entry. Adds conditional CRUD
201
+ * semantics on top of the handlers: existence is probed through the `detect` handler before a create,
202
+ * update, or delete is delegated to `modify`, sparing each handler that bookkeeping.
203
+ *
204
+ * The `modify` handler is additionally wrapped so the batching layer, not the storage handler,
205
+ * enforces the §4.1 state-`id` constraint: a request whose `state` carries an `id` other than its
206
+ * `entry` is rejected on its own before dispatch, and a backend failure is settled as a rejection
207
+ * across the surviving batch rather than surfaced as a handler throw. The wrapped handler thus always
208
+ * receives a non-empty batch of pre-validated requests to apply unconditionally.
209
+ *
210
+ * @param handlers The batch handlers, one per request type, dispatching detect, lookup, select, and
211
+ * modify requests
212
+ *
213
+ * @returns An immutable {@link StoreClient} that batches its requests through the broker
214
+ *
215
+ * @throws {RangeError} If a create or update `state` carries an `id` that differs from its `entry`
216
+ */
217
+ export declare function createBatchingStore(handlers: {
218
+ detect: Handler<Detect>;
219
+ lookup: Handler<Lookup>;
220
+ select: Handler<Select>;
221
+ modify: Handler<Modify>;
222
+ }): StoreClient;
223
+ //# sourceMappingURL=batching.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"batching.d.ts","sourceRoot":"","sources":["../../src/clients/batching.ts"],"names":[],"mappings":"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"}