@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
@@ -1,4 +1,3 @@
1
- export {};
2
1
  /*
3
2
  * Copyright © 2025-2026 Metreeca srl
4
3
  *
@@ -14,4 +13,5 @@ export {};
14
13
  * See the License for the specific language governing permissions and
15
14
  * limitations under the License.
16
15
  */
16
+ export {};
17
17
  //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG"}
@@ -0,0 +1,96 @@
1
+ import type { Reference } from "@metreeca/qest/resource";
2
+ import type { Store } from "../index.js";
3
+ /**
4
+ * Creates a caching store backed by a delegate.
5
+ *
6
+ * Serves repeated retrievals from an in-memory cache, sparing the delegate a round-trip whenever a prior retrieval
7
+ * can be reused; this cuts latency and backend load for read-heavy workloads, while writes and eviction keep cached
8
+ * results coherent and bounded.
9
+ *
10
+ * Retrievals are memoised under an `(entry, model, locale, limit)` key, where the model is canonicalised bottom-up so
11
+ * templates differing only in property or element order (including arrays of objects) share a cache entry, widening
12
+ * the set of requests a single cached result can serve.
13
+ *
14
+ * > [!IMPORTANT]
15
+ * > `shape` is deliberately excluded from the cache key: the retrieval result must be a pure function of
16
+ * > `(entry, model, locale, limit)`, with `shape` supplying only structural metadata that does not affect the
17
+ * > returned payload.
18
+ *
19
+ * > [!IMPORTANT]
20
+ * > `opts.locale` is part of the cache key because it selects which localised content a retrieval returns. Unlike
21
+ * > the model, the locale priority list is order-significant — `["en", "it"]` and `["it", "en"]` key separately —
22
+ * > since order encodes language-negotiation preference. An omitted or empty locale list collapses to a single key,
23
+ * > distinct from any explicit list.
24
+ *
25
+ * > [!IMPORTANT]
26
+ * > `opts.limit` is part of the cache key because it caps each selection's `#` and so changes the returned payload,
27
+ * > with an omitted or `0` limit collapsing to a single unbounded key. `opts.plain`/`opts.depth` are excluded: they
28
+ * > only accept or reject a model, never altering a successful payload.
29
+ *
30
+ * The cache offers three observable guarantees:
31
+ *
32
+ * - **coalescing** — concurrent retrievals for the same key resolve from a single delegate fetch, and a failed
33
+ * retrieval is never cached, so a later retrieval re-fetches;
34
+ * - **read-after-write coherence** — a retrieval never returns content staler than the latest write made through this
35
+ * store: {@link Store.create create}, {@link Store.update update}, {@link Store.delete delete},
36
+ * {@link Store.insert insert} and {@link Store.remove remove} invalidate matching entries, and a
37
+ * retrieval overlapping an in-progress write to the same entry is served from the delegate without being cached;
38
+ * - **commit coherence** — writes committed through {@link Store.execute execute}, by other clients sharing the
39
+ * delegate, or signalled by the backend invalidate matching entries on commit, for as long as the store stays open
40
+ * ({@link Store.close close} ends further invalidation).
41
+ *
42
+ * The configurable `match` predicate selects which cached entries a write invalidates.
43
+ *
44
+ * > [!IMPORTANT]
45
+ * > **Transaction Isolation** — Inherited from the wrapped {@link Store}.
46
+ *
47
+ * > [!IMPORTANT]
48
+ * > **Mutation Events** — Inherited from the wrapped {@link Store}.
49
+ *
50
+ * @param store - The delegate store to cache
51
+ * @param options - Cache configuration options
52
+ *
53
+ * @returns An immutable store with caching behaviour
54
+ *
55
+ * @example
56
+ * ```typescript
57
+ * const cache = createCachingStore(restStore, { size: 1_000, ttl: 60_000 });
58
+ * const product = await cache.lookup({ entry, shape, model });
59
+ * ```
60
+ */
61
+ export declare function createCachingStore(store: Store, { size, ttl, match }?: {
62
+ /**
63
+ * Maximum number of records to cache.
64
+ *
65
+ * When the cache exceeds this bound, the least-recently-used record is evicted after each miss.
66
+ *
67
+ * @defaultValue 0 (unlimited)
68
+ */
69
+ readonly size?: number;
70
+ /**
71
+ * Maximum record age in milliseconds.
72
+ *
73
+ * Records older than this value are treated as misses on the next retrieval and evicted when a sibling miss
74
+ * triggers the TTL sweep. Expiration is checked on every hit, so a stale record is never returned even if the
75
+ * sweep hasn't run yet.
76
+ *
77
+ * @defaultValue 0 (no expiration)
78
+ */
79
+ readonly ttl?: number;
80
+ /**
81
+ * Predicate deciding which cached records an invalidation event discards.
82
+ *
83
+ * Invoked by both invalidation paths — pre-commit writes on the wrapper and reactive mutation events from the
84
+ * delegate — with the mutated resource's identifier and the identifier carried on each cached record. Return
85
+ * true to evict the record.
86
+ *
87
+ * @param entry - Identifier of the mutated resource
88
+ * @param cache - Identifier associated with a cached record
89
+ *
90
+ * @returns true if the record should be evicted; false otherwise
91
+ *
92
+ * @defaultValue `() => true` (every mutation clears the cache)
93
+ */
94
+ readonly match?: (entry: Reference, cache: Reference) => boolean;
95
+ }): Store;
96
+ //# sourceMappingURL=caching.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"caching.d.ts","sourceRoot":"","sources":["../../src/stores/caching.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEzD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAGzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,KAAK,EAAE,EAEhD,IAAQ,EACR,GAAO,EACP,KAAkB,EAElB,GAAE;IAEF;;;;;;OAMG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAEvB;;;;;;;;OAQG;IACH,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAEtB;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,KAAK,OAAO,CAAC;CAE5D,GAAG,KAAK,CAyUb"}
@@ -0,0 +1,311 @@
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
+ /**
17
+ * Caching store wrapper.
18
+ *
19
+ * Provides {@link createCachingStore}, which wraps a {@link Store} with an in-memory cache
20
+ * supporting LRU and TTL-based eviction, in-flight request sharing, an in-flight write bypass that prevents
21
+ * pre-commit values from being cached, and coherent invalidation driven by both the wrapper's own write
22
+ * operations and the delegate's mutation events.
23
+ *
24
+ * @module
25
+ */
26
+ import { isArray, isObject } from "@metreeca/core";
27
+ import { immutable } from "@metreeca/core/structures";
28
+ /**
29
+ * Creates a caching store backed by a delegate.
30
+ *
31
+ * Serves repeated retrievals from an in-memory cache, sparing the delegate a round-trip whenever a prior retrieval
32
+ * can be reused; this cuts latency and backend load for read-heavy workloads, while writes and eviction keep cached
33
+ * results coherent and bounded.
34
+ *
35
+ * Retrievals are memoised under an `(entry, model, locale, limit)` key, where the model is canonicalised bottom-up so
36
+ * templates differing only in property or element order (including arrays of objects) share a cache entry, widening
37
+ * the set of requests a single cached result can serve.
38
+ *
39
+ * > [!IMPORTANT]
40
+ * > `shape` is deliberately excluded from the cache key: the retrieval result must be a pure function of
41
+ * > `(entry, model, locale, limit)`, with `shape` supplying only structural metadata that does not affect the
42
+ * > returned payload.
43
+ *
44
+ * > [!IMPORTANT]
45
+ * > `opts.locale` is part of the cache key because it selects which localised content a retrieval returns. Unlike
46
+ * > the model, the locale priority list is order-significant — `["en", "it"]` and `["it", "en"]` key separately —
47
+ * > since order encodes language-negotiation preference. An omitted or empty locale list collapses to a single key,
48
+ * > distinct from any explicit list.
49
+ *
50
+ * > [!IMPORTANT]
51
+ * > `opts.limit` is part of the cache key because it caps each selection's `#` and so changes the returned payload,
52
+ * > with an omitted or `0` limit collapsing to a single unbounded key. `opts.plain`/`opts.depth` are excluded: they
53
+ * > only accept or reject a model, never altering a successful payload.
54
+ *
55
+ * The cache offers three observable guarantees:
56
+ *
57
+ * - **coalescing** — concurrent retrievals for the same key resolve from a single delegate fetch, and a failed
58
+ * retrieval is never cached, so a later retrieval re-fetches;
59
+ * - **read-after-write coherence** — a retrieval never returns content staler than the latest write made through this
60
+ * store: {@link Store.create create}, {@link Store.update update}, {@link Store.delete delete},
61
+ * {@link Store.insert insert} and {@link Store.remove remove} invalidate matching entries, and a
62
+ * retrieval overlapping an in-progress write to the same entry is served from the delegate without being cached;
63
+ * - **commit coherence** — writes committed through {@link Store.execute execute}, by other clients sharing the
64
+ * delegate, or signalled by the backend invalidate matching entries on commit, for as long as the store stays open
65
+ * ({@link Store.close close} ends further invalidation).
66
+ *
67
+ * The configurable `match` predicate selects which cached entries a write invalidates.
68
+ *
69
+ * > [!IMPORTANT]
70
+ * > **Transaction Isolation** — Inherited from the wrapped {@link Store}.
71
+ *
72
+ * > [!IMPORTANT]
73
+ * > **Mutation Events** — Inherited from the wrapped {@link Store}.
74
+ *
75
+ * @param store - The delegate store to cache
76
+ * @param options - Cache configuration options
77
+ *
78
+ * @returns An immutable store with caching behaviour
79
+ *
80
+ * @example
81
+ * ```typescript
82
+ * const cache = createCachingStore(restStore, { size: 1_000, ttl: 60_000 });
83
+ * const product = await cache.lookup({ entry, shape, model });
84
+ * ```
85
+ */
86
+ export function createCachingStore(store, { size = 0, ttl = 0, match = () => true } = {}) {
87
+ // LRU ordering is supplied for free by `Map`'s insertion-order iteration: every hit re-sets the entry,
88
+ // moving it to the tail, so `cache.keys().next().value` is always the least-recently-used key.
89
+ const cache = new Map();
90
+ // Tracks entries with at least one in-flight wrapper-initiated write. Reads of these entries bypass the
91
+ // cache entirely — they still query the delegate, but their result is not stored. This closes the window
92
+ // between a lookup resolving with a pre-commit value and the reactive observer firing on commit, during
93
+ // which the cache would otherwise hold a stale-but-fresh-looking record.
94
+ const writing = new Map();
95
+ // Reactive invalidation: subscribe to the delegate's mutation events and drop matching records on commit.
96
+ // Covers writes routed through `execute`, writes from other clients sharing the delegate, and backends that
97
+ // emit storage-level events. The subscription is released by `close()` so the delegate does not retain a
98
+ // reference to a discarded wrapper.
99
+ const unsubscribe = store.observe(mutations => {
100
+ Object.keys(mutations).forEach(invalidate);
101
+ });
102
+ return immutable({
103
+ lookup({ entry, shape, model }, opts) {
104
+ return memoise(entry, model, opts, () => store.lookup({ entry, shape, model }, opts));
105
+ },
106
+ create({ entry, shape, state }) {
107
+ return write(entry, () => store.create({ entry, shape, state }));
108
+ },
109
+ update({ entry, shape, state }) {
110
+ return write(entry, () => store.update({ entry, shape, state }));
111
+ },
112
+ delete({ entry, shape }) {
113
+ return write(entry, () => store.delete({ entry, shape }));
114
+ },
115
+ insert({ entry, shape, state }, opts) {
116
+ return write(entry, () => store.insert({ entry, shape, state }, opts));
117
+ },
118
+ remove({ entry, shape }) {
119
+ return write(entry, () => store.remove({ entry, shape }));
120
+ },
121
+ observe(observer, resources) {
122
+ return store.observe(observer, resources);
123
+ },
124
+ execute(task) {
125
+ return store.execute(task); // task mutations invalidate the cache reactively through events
126
+ },
127
+ close() {
128
+ unsubscribe();
129
+ return store.close();
130
+ }
131
+ });
132
+ /**
133
+ * Retrieve-with-cache core.
134
+ *
135
+ * Three branches:
136
+ *
137
+ * - **bypass** — when `entry` has any wrapper-initiated write in flight, runs `miss()` and returns its
138
+ * promise without storing it, so a pre-commit snapshot cannot be installed as fresh;
139
+ * - **fresh hit** — returns the memoised promise and re-inserts the record to bump its LRU position;
140
+ * - **miss / TTL-expired** — runs `miss()`, stores the pending promise under the canonicalised key so
141
+ * concurrent callers share the in-flight request, and enforces TTL/size bounds via {@link purge}; a
142
+ * rejected fetch evicts its record only if an intervening invalidation has not already replaced it.
143
+ *
144
+ * @param entry - Resource identifier; paired with the canonicalised model and varying opts to form the cache key
145
+ * @param model - Retrieval template, canonicalised so that equivalent templates in different orderings share a key
146
+ * @param vary - Retrieval opts whose values vary the returned payload, folded into the key: `locale`
147
+ * order-significantly (an omitted or empty list collapsing to a single key) and `limit` because it caps each
148
+ * selection's `#` pagination bound (an omitted or `0` unbounded value collapsing to a single key)
149
+ * @param miss - Fetcher invoked on cache miss, on TTL expiration, or while the entry has an in-flight write
150
+ *
151
+ * @returns The memoised, freshly-fetched, or bypass-fetched promise
152
+ */
153
+ function memoise(entry, model, vary = {}, miss) {
154
+ // Bypass the cache while a write to this entry is in flight: still query the delegate so the caller
155
+ // gets a value, but do not store the result — it is liable to be the pre-commit snapshot that the
156
+ // reactive observer would invalidate moments later
157
+ if (writing.has(entry)) {
158
+ return miss();
159
+ }
160
+ else {
161
+ // Cache key layout: `<entry>\x00<canonical-model>\x00<locale>\x00<limit>`. `canonical` walks the model
162
+ // bottom-up, sorting object keys and (already-canonical) array elements, so templates differing only in
163
+ // ordering share a key. The locale segment is serialised verbatim (order preserved, since the priority
164
+ // list is order-significant) with omitted and empty lists collapsing to `[]`. The limit segment is
165
+ // keyed because it caps each selection's `#` and so changes the payload, with omitted and `0` (both
166
+ // unbounded) collapsing to `0`. `\x00` is safe as a separator — IRIs cannot contain it and JSON always
167
+ // escapes it inside string payloads.
168
+ const key = `${entry}\x00${canonical(model)}\x00${JSON.stringify(vary.locale ?? [])}\x00${vary.limit ?? 0}`;
169
+ const cached = cache.get(key);
170
+ if (cached && !expired(cached.created)) {
171
+ // hit: re-insert to move the entry to the tail of the Map's iteration order (the LRU position),
172
+ // and return the memoised promise; the cast reflects the type-erased `Promise<unknown>` storage
173
+ // that lets records with different template types share one Map
174
+ cache.delete(key);
175
+ cache.set(key, cached);
176
+ return cached.value;
177
+ }
178
+ else {
179
+ // miss (or TTL-expired): drop any stale record, run the fetcher, and memoise the pending promise
180
+ // under the same key so concurrent callers share the in-flight request; a failure evicts its own
181
+ // record through the catch handler so transient errors are not retained
182
+ if (cached) {
183
+ cache.delete(key);
184
+ }
185
+ const value = miss().catch(error => {
186
+ // identity-check: only evict if this promise is still the cached one —
187
+ // avoids dropping a newer record installed after an intervening invalidate
188
+ if (cache.get(key)?.value === value) {
189
+ cache.delete(key);
190
+ }
191
+ throw error;
192
+ });
193
+ cache.set(key, {
194
+ entry,
195
+ value,
196
+ created: Date.now()
197
+ });
198
+ purge();
199
+ return value;
200
+ }
201
+ }
202
+ }
203
+ /**
204
+ * Serialises a value to a canonical JSON-like string.
205
+ *
206
+ * Children are canonicalised first, then sorted by their already-canonical string form — so arrays of
207
+ * objects are ordered by structural content rather than by the `"[object Object]"` placeholder that
208
+ * `Array.prototype.sort` would otherwise impose. The output collapses values differing only in property
209
+ * or element order to the same string.
210
+ *
211
+ * @param value - Value to serialise
212
+ *
213
+ * @returns A canonical string representation of `value`
214
+ */
215
+ function canonical(value) {
216
+ if (isArray(value)) {
217
+ return `[${value.map(canonical).sort().join(",")}]`;
218
+ }
219
+ else if (isObject(value)) {
220
+ return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${canonical(value[k])}`).join(",")}}`;
221
+ }
222
+ else {
223
+ // JSON.stringify(undefined) returns undefined, not a string; substitute a distinct sentinel so the
224
+ // signature holds and `undefined` does not collide with the string literal `"undefined"` (which JSON
225
+ // would render with surrounding quotes)
226
+ return value === undefined ? "undefined" : JSON.stringify(value);
227
+ }
228
+ }
229
+ /**
230
+ * Tests whether a record is older than the configured TTL.
231
+ *
232
+ * Returns false when `ttl <= 0`, so the predicate can be reused at both the hit guard and the purge sweep
233
+ * without duplicating the boundary check. The reference timestamp defaults to `Date.now` but may be
234
+ * passed in to share a single snapshot across a batch of comparisons.
235
+ *
236
+ * @param created - Insertion timestamp (ms) of the record under test
237
+ * @param now - Reference timestamp (ms) to compare against; defaults to `Date.now`
238
+ *
239
+ * @returns true if `ttl > 0` and `created` is older than `ttl` milliseconds relative to `now`; false otherwise
240
+ */
241
+ function expired(created, now = Date.now()) {
242
+ return ttl > 0 && now - created > ttl;
243
+ }
244
+ /**
245
+ * Run a wrapper-initiated write under in-flight tracking.
246
+ *
247
+ * Marks `entry` as having an in-flight write, runs the configured pre-commit invalidation, delegates to
248
+ * `op`, and clears the marker once the delegate settles. While the marker is set, retrievals of `entry`
249
+ * bypass the cache so a pre-commit value can never be stored as fresh.
250
+ *
251
+ * @param entry - Resource identifier targeted by the write
252
+ * @param op - Thunk that delegates the write to the wrapped store and returns its promise
253
+ *
254
+ * @returns The promise returned by `op`, settled after the in-flight marker has been cleared
255
+ */
256
+ function write(entry, op) {
257
+ writing.set(entry, (writing.get(entry) ?? 0) + 1);
258
+ invalidate(entry);
259
+ return op().finally(() => {
260
+ const count = (writing.get(entry) ?? 1) - 1;
261
+ if (count <= 0) {
262
+ writing.delete(entry);
263
+ }
264
+ else {
265
+ writing.set(entry, count);
266
+ }
267
+ });
268
+ }
269
+ /**
270
+ * Drop cached records matching a mutated resource.
271
+ *
272
+ * Iterates the cache and removes every record whose stored identifier satisfies the configured {@link match}
273
+ * predicate against the mutated `entry`. Called from both the pre-commit write path on the wrapper and the
274
+ * reactive observer attached to the delegate.
275
+ *
276
+ * @param entry - Identifier of the mutated resource
277
+ */
278
+ function invalidate(entry) {
279
+ for (const [key, record] of cache) {
280
+ if (match(entry, record.entry)) {
281
+ cache.delete(key);
282
+ }
283
+ }
284
+ }
285
+ /**
286
+ * Enforce TTL and size bounds.
287
+ *
288
+ * Runs two sweeps: first, when `ttl > 0`, drops every record older than `ttl` milliseconds; second, when
289
+ * `size > 0` and the cache is overfull, drops the head slice of the Map (least-recently-used first) in
290
+ * a single batch sized to the exact overflow. Invoked after every miss so expired siblings and overfill
291
+ * are cleaned up incrementally — the cache has no background sweeper.
292
+ */
293
+ function purge() {
294
+ if (ttl > 0) {
295
+ const now = Date.now();
296
+ for (const [key, record] of cache) {
297
+ if (expired(record.created, now)) {
298
+ cache.delete(key);
299
+ }
300
+ }
301
+ }
302
+ // LRU sweep: when over the size bound, take the head slice of Map keys (insertion order = LRU first)
303
+ // and drop them — `cache.size - size` is the exact overflow, so no loop condition needs re-checking
304
+ if (size > 0 && cache.size > size) {
305
+ Array.from(cache.keys())
306
+ .slice(0, cache.size - size)
307
+ .forEach(key => cache.delete(key));
308
+ }
309
+ }
310
+ }
311
+ //# sourceMappingURL=caching.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"caching.js","sourceRoot":"","sources":["../../src/stores/caching.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import type { Store, StoreClient } from "../index.js";
2
+ /**
3
+ * Manages mutation events, transactional execution, and lifecycle for a bare {@link StoreClient},
4
+ * exposing it as a full {@link Store}.
5
+ *
6
+ * This lets a connector implement only the {@link StoreClient} data surface and obtain
7
+ * mutation events, transactional execution, and lifecycle for free: supply nothing and the wrapper provides working
8
+ * defaults, or hand it backend primitives through `management` to back any of `execute`, `observe`, or `close`.
9
+ *
10
+ * Each standalone mutation call and each {@link Store.execute execute} call accumulates its own
11
+ * batch of mutation signals and delivers a single filtered event to each matching registered
12
+ * {@link StoreObserver observer} when the call resolves; if it rejects, pending signals are discarded
13
+ * and no observers are notified. Both synchronous throws and asynchronous rejections from observers
14
+ * are caught and silently ignored so that one faulty observer cannot break delivery to others.
15
+ *
16
+ * Any of the {@link Store} management methods may be supplied through `management` to delegate to an inner
17
+ * implementation; the wrapper fills in whatever is missing.
18
+ *
19
+ * All errors — `RangeError`, {@link @metreeca/core!TraceError | TraceError},
20
+ * {@link @metreeca/http!Problem | Problem} —
21
+ * propagate as promise rejections per the unified {@link Store} error channel.
22
+ *
23
+ * > [!IMPORTANT]
24
+ * > **Transaction Isolation** — `None` by default: the built-in `execute` is a deferred identity that applies each
25
+ * > call directly, with no write buffering and no rollback. When an `execute` opt is supplied through `management`,
26
+ * > the level is determined by that wrapper (typically a backend transaction primitive).
27
+ *
28
+ * > [!IMPORTANT]
29
+ * > **Mutation Events** — emits in-process observer events: each standalone mutation and each
30
+ * > {@link Store.execute execute} call delivers a single filtered batch to matching {@link StoreObserver observer}s
31
+ * > on resolve, and none on rejection. Cross-client signals reach local observers only when a backend-bridging
32
+ * > `observe` opt is supplied through `management`.
33
+ *
34
+ * @param store - Inner StoreClient to delegate data calls to
35
+ * @param management - Subset of {@link Store} management methods to delegate to; the wrapper fills in whatever is
36
+ * missing
37
+ * @param management.execute - Wraps every standalone StoreClient call and the body of {@link Store.execute execute},
38
+ * typically a backend transaction primitive (for example, `graph.execute` for a SPARQL connector) responsible for
39
+ * atomic commit and rollback. The wrapper StoreClient passed to the user task does NOT re-enter the opt; it relies
40
+ * on the outer wrap so cross-call isolation works as expected. The task receives a per-call `scope` StoreClient,
41
+ * and every delegated call within the wrap routes through `scope` (mirroring the {@link Store.execute} contract),
42
+ * letting the wrapper install per-call state such as a freshly-bound graph buffer without leaking it across
43
+ * concurrent invocations. Defaults to a deferred identity (`task => Promise.resolve().then(() => task(store))`);
44
+ * user-supplied wrappers MUST pass a per-call `scope` StoreClient to `task` and MUST convert synchronous throws
45
+ * from `task` into promise rejections to preserve the unified Store error channel; the `lookup` path delegates
46
+ * directly to the wrapper and provides no additional guard
47
+ * @param management.observe - Registers each observer with the delegate as well as locally, combining the two
48
+ * unsubscribe handles so a single detach call releases both; this is how storage-level events (mutations from
49
+ * other clients sharing the backend) reach the wrapper's local observers. Absent by default
50
+ * @param management.close - Exposed verbatim on the returned store. Defaults to a resolved no-op
51
+ *
52
+ * @returns An immutable {@link Store} composing `store` with the supplied `management` opts
53
+ */
54
+ export declare function createManagingStore(store: StoreClient, { observe, execute, close }?: Partial<Store>): Store;
55
+ //# sourceMappingURL=managing.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"managing.d.ts","sourceRoot":"","sources":["../../src/stores/managing.ts"],"names":[],"mappings":"AA+BA,OAAO,KAAK,EAAE,KAAK,EAAE,WAAW,EAAiB,MAAM,aAAa,CAAC;AAGrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,WAAW,EAAE,EAEvD,OAAO,EACP,OAAwG,EACxG,KAA+B,EAE/B,GAAE,OAAO,CAAC,KAAK,CAAM,GAAG,KAAK,CA2H7B"}
@@ -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
+ /**
17
+ * Managing store wrapper.
18
+ *
19
+ * Manages mutation events, transactional execution, and lifecycle for a plain {@link StoreClient},
20
+ * exposing it as a full {@link Store}.
21
+ *
22
+ * @module
23
+ */
24
+ import { isString } from "@metreeca/core";
25
+ import { isNestedIRI } from "@metreeca/core/resource";
26
+ import { immutable } from "@metreeca/core/structures";
27
+ /**
28
+ * Manages mutation events, transactional execution, and lifecycle for a bare {@link StoreClient},
29
+ * exposing it as a full {@link Store}.
30
+ *
31
+ * This lets a connector implement only the {@link StoreClient} data surface and obtain
32
+ * mutation events, transactional execution, and lifecycle for free: supply nothing and the wrapper provides working
33
+ * defaults, or hand it backend primitives through `management` to back any of `execute`, `observe`, or `close`.
34
+ *
35
+ * Each standalone mutation call and each {@link Store.execute execute} call accumulates its own
36
+ * batch of mutation signals and delivers a single filtered event to each matching registered
37
+ * {@link StoreObserver observer} when the call resolves; if it rejects, pending signals are discarded
38
+ * and no observers are notified. Both synchronous throws and asynchronous rejections from observers
39
+ * are caught and silently ignored so that one faulty observer cannot break delivery to others.
40
+ *
41
+ * Any of the {@link Store} management methods may be supplied through `management` to delegate to an inner
42
+ * implementation; the wrapper fills in whatever is missing.
43
+ *
44
+ * All errors — `RangeError`, {@link @metreeca/core!TraceError | TraceError},
45
+ * {@link @metreeca/http!Problem | Problem} —
46
+ * propagate as promise rejections per the unified {@link Store} error channel.
47
+ *
48
+ * > [!IMPORTANT]
49
+ * > **Transaction Isolation** — `None` by default: the built-in `execute` is a deferred identity that applies each
50
+ * > call directly, with no write buffering and no rollback. When an `execute` opt is supplied through `management`,
51
+ * > the level is determined by that wrapper (typically a backend transaction primitive).
52
+ *
53
+ * > [!IMPORTANT]
54
+ * > **Mutation Events** — emits in-process observer events: each standalone mutation and each
55
+ * > {@link Store.execute execute} call delivers a single filtered batch to matching {@link StoreObserver observer}s
56
+ * > on resolve, and none on rejection. Cross-client signals reach local observers only when a backend-bridging
57
+ * > `observe` opt is supplied through `management`.
58
+ *
59
+ * @param store - Inner StoreClient to delegate data calls to
60
+ * @param management - Subset of {@link Store} management methods to delegate to; the wrapper fills in whatever is
61
+ * missing
62
+ * @param management.execute - Wraps every standalone StoreClient call and the body of {@link Store.execute execute},
63
+ * typically a backend transaction primitive (for example, `graph.execute` for a SPARQL connector) responsible for
64
+ * atomic commit and rollback. The wrapper StoreClient passed to the user task does NOT re-enter the opt; it relies
65
+ * on the outer wrap so cross-call isolation works as expected. The task receives a per-call `scope` StoreClient,
66
+ * and every delegated call within the wrap routes through `scope` (mirroring the {@link Store.execute} contract),
67
+ * letting the wrapper install per-call state such as a freshly-bound graph buffer without leaking it across
68
+ * concurrent invocations. Defaults to a deferred identity (`task => Promise.resolve().then(() => task(store))`);
69
+ * user-supplied wrappers MUST pass a per-call `scope` StoreClient to `task` and MUST convert synchronous throws
70
+ * from `task` into promise rejections to preserve the unified Store error channel; the `lookup` path delegates
71
+ * directly to the wrapper and provides no additional guard
72
+ * @param management.observe - Registers each observer with the delegate as well as locally, combining the two
73
+ * unsubscribe handles so a single detach call releases both; this is how storage-level events (mutations from
74
+ * other clients sharing the backend) reach the wrapper's local observers. Absent by default
75
+ * @param management.close - Exposed verbatim on the returned store. Defaults to a resolved no-op
76
+ *
77
+ * @returns An immutable {@link Store} composing `store` with the supplied `management` opts
78
+ */
79
+ export function createManagingStore(store, { observe, execute = (task) => Promise.resolve().then(() => task(store)), close = () => Promise.resolve() } = {}) {
80
+ const observers = new Map();
81
+ return immutable({
82
+ lookup: (specs, opts) => execute(store => store.lookup(specs, opts)),
83
+ create: specs => notify(({ mutated }) => execute(store => store.create(specs).then(mutated))),
84
+ update: specs => notify(({ mutated }) => execute(store => store.update(specs).then(mutated))),
85
+ delete: specs => notify(({ deleted }) => execute(store => store.delete(specs).then(deleted))),
86
+ insert: (specs, opts) => notify(({ mutated }) => execute(store => store.insert(specs, opts).then(mutated))),
87
+ remove: specs => notify(({ deleted }) => execute(store => store.remove(specs).then(deleted))),
88
+ observe(observer, resources) {
89
+ if (Array.isArray(resources) && resources.length === 0) {
90
+ return observe?.(observer, []) ?? (() => { });
91
+ }
92
+ else {
93
+ const token = Symbol();
94
+ const detach = observe?.(observer, resources);
95
+ observers.set(token, {
96
+ observer,
97
+ resources: resources === undefined ? undefined
98
+ : isString(resources) ? [resources]
99
+ : [...resources]
100
+ });
101
+ return () => {
102
+ try {
103
+ detach?.();
104
+ }
105
+ finally {
106
+ observers.delete(token);
107
+ }
108
+ };
109
+ }
110
+ },
111
+ execute(task) {
112
+ return notify(({ mutated, deleted }) => execute(store => task(immutable({
113
+ lookup: (specs, opts) => store.lookup(specs, opts),
114
+ create: specs => store.create(specs).then(mutated),
115
+ update: specs => store.update(specs).then(mutated),
116
+ delete: specs => store.delete(specs).then(deleted),
117
+ insert: (specs, opts) => store.insert(specs, opts).then(mutated),
118
+ remove: specs => store.remove(specs).then(deleted)
119
+ }))));
120
+ },
121
+ close
122
+ });
123
+ async function notify(task) {
124
+ const mutations = new Map();
125
+ const value = await task({
126
+ mutated: entry => {
127
+ if (entry !== undefined) {
128
+ mutations.set(entry, true);
129
+ }
130
+ return entry;
131
+ },
132
+ deleted: entry => {
133
+ if (entry !== undefined) {
134
+ mutations.set(entry, false);
135
+ }
136
+ return entry;
137
+ }
138
+ });
139
+ if (mutations.size > 0) {
140
+ observers.forEach(({ observer, resources }) => {
141
+ const event = {};
142
+ mutations.forEach((exists, id) => {
143
+ // `resources` is `undefined` for an unfiltered registration (fires for every mutation)
144
+ // and an array — possibly empty — for a filtered one
145
+ if (resources === undefined || resources.some(r => isNestedIRI(r, id))) {
146
+ event[id] = exists;
147
+ }
148
+ });
149
+ if (Object.keys(event).length > 0) {
150
+ Promise.resolve(event)
151
+ .then(observer)
152
+ .catch(() => { });
153
+ }
154
+ });
155
+ }
156
+ return value;
157
+ }
158
+ }
159
+ //# sourceMappingURL=managing.js.map