@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
|
@@ -0,0 +1,320 @@
|
|
|
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
|
+
* Speeds up read-heavy workloads by serving repeated retrievals from an in-memory cache. The cache stays bounded
|
|
20
|
+
* by size and age, shares concurrent identical retrievals, and stays coherent with writes made through the store, by
|
|
21
|
+
* other clients, or signalled by the backend.
|
|
22
|
+
*
|
|
23
|
+
* @module
|
|
24
|
+
*/
|
|
25
|
+
import { isArray, isObject } from "@metreeca/core";
|
|
26
|
+
import { immutable } from "@metreeca/core/values";
|
|
27
|
+
/**
|
|
28
|
+
* Creates a caching store backed by a delegate.
|
|
29
|
+
*
|
|
30
|
+
* Serves repeated retrievals from an in-memory cache, sparing the delegate a round-trip whenever a prior result can
|
|
31
|
+
* be reused. This cuts latency and backend load for read-heavy workloads, while writes and eviction keep cached
|
|
32
|
+
* results coherent and bounded.
|
|
33
|
+
*
|
|
34
|
+
* Results are cached by `entry`, `model`, and the `locale` and `limit` of the {@link Store.lookup lookup} retrieval scope.
|
|
35
|
+
* Templates differing only in member or element order share a cached result.
|
|
36
|
+
*
|
|
37
|
+
* > [!IMPORTANT]
|
|
38
|
+
* > `shape` is not part of the cache key. A retrieval result MUST depend only on `entry`, `model`, `locale` and
|
|
39
|
+
* > `limit`, with `shape` supplying structural metadata that does not affect the returned payload.
|
|
40
|
+
*
|
|
41
|
+
* > [!IMPORTANT]
|
|
42
|
+
* > The scope `locale` is part of the cache key, because it selects which localised content a retrieval returns.
|
|
43
|
+
* > Unlike the model, the locale priority list is order-significant: `["en", "it"]` and `["it", "en"]` are cached
|
|
44
|
+
* > separately. An omitted and an empty locale list share a single cached result, distinct from any explicit list.
|
|
45
|
+
*
|
|
46
|
+
* > [!IMPORTANT]
|
|
47
|
+
* > The scope `limit` is part of the cache key, because it caps the page size of each collection and so changes the
|
|
48
|
+
* > returned payload. An omitted and a `0` limit share a single cached result. The scope `plain` and `depth` are not
|
|
49
|
+
* > part of the key: they only accept or reject a model, and never alter a successful payload.
|
|
50
|
+
*
|
|
51
|
+
* The cache offers three observable guarantees:
|
|
52
|
+
*
|
|
53
|
+
* - **coalescing** — concurrent retrievals for the same key resolve from a single delegate fetch, and a failed
|
|
54
|
+
* retrieval or one finding no resource is never cached, so a later retrieval re-fetches;
|
|
55
|
+
* - **read-after-write coherence** — a retrieval never returns content staler than the latest write made through this
|
|
56
|
+
* store: {@link Store.create create}, {@link Store.update update}, {@link Store.delete delete},
|
|
57
|
+
* {@link Store.insert insert} and {@link Store.remove remove} invalidate matching entries, and a
|
|
58
|
+
* retrieval overlapping an in-progress write to the same entry is served from the delegate without being cached;
|
|
59
|
+
* - **commit coherence** — writes committed through {@link Store.execute execute}, by other clients sharing the
|
|
60
|
+
* delegate, or signalled by the backend invalidate matching entries on commit, for as long as the store stays open
|
|
61
|
+
* ({@link Store.close close} ends further invalidation).
|
|
62
|
+
*
|
|
63
|
+
* The configurable `match` predicate selects which cached entries a write invalidates.
|
|
64
|
+
*
|
|
65
|
+
* > [!IMPORTANT]
|
|
66
|
+
* > **Transaction Isolation** — Inherited from the wrapped {@link Store}.
|
|
67
|
+
*
|
|
68
|
+
* > [!IMPORTANT]
|
|
69
|
+
* > **Mutation Events** — Inherited from the wrapped {@link Store}.
|
|
70
|
+
*
|
|
71
|
+
* @param store - The delegate store to cache
|
|
72
|
+
* @param options - Cache configuration options
|
|
73
|
+
*
|
|
74
|
+
* @returns An immutable store with caching behaviour
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* ```typescript
|
|
78
|
+
* const cache = createCachingStore(restStore, { size: 1_000, ttl: 60_000 });
|
|
79
|
+
* const product = await cache.lookup({ entry, shape, model });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export function createCachingStore(store, { size = 0, ttl = 0, match = () => true } = {}) {
|
|
83
|
+
// LRU ordering is supplied for free by `Map`'s insertion-order iteration: every hit re-sets the entry,
|
|
84
|
+
// moving it to the tail, so `cache.keys().next().value` is always the least-recently-used key.
|
|
85
|
+
const cache = new Map();
|
|
86
|
+
// Tracks entries with at least one in-flight wrapper-initiated write. Reads of these entries bypass the
|
|
87
|
+
// cache entirely: they still query the delegate, but their result is not stored. This closes the window
|
|
88
|
+
// between a lookup call resolving with a pre-commit value and the reactive observer firing on commit, during
|
|
89
|
+
// which the cache would otherwise hold a stale-but-fresh-looking record.
|
|
90
|
+
const writing = new Map();
|
|
91
|
+
// Reactive invalidation: subscribe to the delegate's mutation events and drop matching records on commit.
|
|
92
|
+
// Covers writes routed through `execute`, writes from other clients sharing the delegate, and backends that
|
|
93
|
+
// emit storage-level events. The subscription is released by `close()` so the delegate does not retain a
|
|
94
|
+
// reference to a discarded wrapper.
|
|
95
|
+
const unsubscribe = store.observe(mutations => {
|
|
96
|
+
Object.keys(mutations).forEach(invalidate);
|
|
97
|
+
});
|
|
98
|
+
return immutable({
|
|
99
|
+
lookup({ entry, shape, model }, opts) {
|
|
100
|
+
return memoise(entry, model, opts, () => store.lookup({ entry, shape, model }, opts));
|
|
101
|
+
},
|
|
102
|
+
create({ entry, shape, model, state }) {
|
|
103
|
+
return write(entry, () => store.create({ entry, shape, model, state }));
|
|
104
|
+
},
|
|
105
|
+
update({ entry, shape, state }) {
|
|
106
|
+
return write(entry, () => store.update({ entry, shape, state }));
|
|
107
|
+
},
|
|
108
|
+
delete({ entry, shape }) {
|
|
109
|
+
return write(entry, () => store.delete({ entry, shape }));
|
|
110
|
+
},
|
|
111
|
+
insert({ entry, shape, state }) {
|
|
112
|
+
return write(entry, () => store.insert({ entry, shape, state }));
|
|
113
|
+
},
|
|
114
|
+
remove({ entry, shape }) {
|
|
115
|
+
return write(entry, () => store.remove({ entry, shape }));
|
|
116
|
+
},
|
|
117
|
+
observe(observer, resources) {
|
|
118
|
+
return store.observe(observer, resources);
|
|
119
|
+
},
|
|
120
|
+
execute(task) {
|
|
121
|
+
return store.execute(task); // task mutations invalidate the cache reactively through events
|
|
122
|
+
},
|
|
123
|
+
close() {
|
|
124
|
+
unsubscribe();
|
|
125
|
+
return store.close();
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
/**
|
|
129
|
+
* Retrieve-with-cache core.
|
|
130
|
+
*
|
|
131
|
+
* Three branches:
|
|
132
|
+
*
|
|
133
|
+
* - **bypass** — when `entry` has any wrapper-initiated write in flight, runs `miss()` and returns its
|
|
134
|
+
* promise without storing it, so a pre-commit snapshot cannot be installed as fresh;
|
|
135
|
+
* - **fresh hit** — returns the memoised promise and re-inserts the record to bump its LRU position;
|
|
136
|
+
* - **miss / TTL-expired** — runs `miss()`, stores the pending promise under the canonicalised key so
|
|
137
|
+
* concurrent callers share the in-flight request, and enforces TTL/size bounds via {@link purge}; a
|
|
138
|
+
* fetch resolving to an absent resource or rejecting evicts its record only if an intervening invalidation
|
|
139
|
+
* has not already replaced it.
|
|
140
|
+
*
|
|
141
|
+
* @typeParam V - The retrieved payload, opaque to the cache
|
|
142
|
+
*
|
|
143
|
+
* @param entry - Resource identifier; paired with the canonicalised model and varying scope members to form the
|
|
144
|
+
* cache key
|
|
145
|
+
* @param model - Retrieval model, canonicalised so that equivalent models in different orderings share a key
|
|
146
|
+
* @param vary - Retrieval scope members 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
|
+
* collection'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
|
|
162
|
+
// model bottom-up, sorting object keys and (already-canonical) array elements, so models differing only
|
|
163
|
+
// in 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 collection'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, canonical(model), JSON.stringify(vary.locale ?? []), vary.limit ?? 0].join("\x00");
|
|
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; ;(cast) the type-erased `Promise<unknown>` storage lets records
|
|
173
|
+
// with different payload 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; an absent resource or a
|
|
181
|
+
// failure evicts its own record once settled, so neither a missing resource nor a transient error
|
|
182
|
+
// is retained
|
|
183
|
+
if (cached) {
|
|
184
|
+
cache.delete(key);
|
|
185
|
+
}
|
|
186
|
+
const value = miss().then(resource => {
|
|
187
|
+
if (resource === undefined) {
|
|
188
|
+
evict();
|
|
189
|
+
}
|
|
190
|
+
return resource;
|
|
191
|
+
}, error => {
|
|
192
|
+
evict();
|
|
193
|
+
throw error;
|
|
194
|
+
});
|
|
195
|
+
// identity-check: only evict if this promise is still the cached one, avoiding dropping a newer
|
|
196
|
+
// record installed after an intervening invalidate
|
|
197
|
+
function evict() {
|
|
198
|
+
if (cache.get(key)?.value === value) {
|
|
199
|
+
cache.delete(key);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
cache.set(key, {
|
|
203
|
+
entry,
|
|
204
|
+
value,
|
|
205
|
+
created: Date.now()
|
|
206
|
+
});
|
|
207
|
+
purge();
|
|
208
|
+
return value;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Serialises a value to a canonical JSON-like string.
|
|
214
|
+
*
|
|
215
|
+
* Children are canonicalised first, then sorted by their already-canonical string form, so arrays of
|
|
216
|
+
* objects are ordered by structural content rather than by the `"[object Object]"` placeholder that
|
|
217
|
+
* `Array.prototype.sort` would otherwise impose. The output collapses values differing only in property
|
|
218
|
+
* or element order to the same string.
|
|
219
|
+
*
|
|
220
|
+
* @param value - Value to serialise
|
|
221
|
+
*
|
|
222
|
+
* @returns A canonical string representation of `value`
|
|
223
|
+
*/
|
|
224
|
+
function canonical(value) {
|
|
225
|
+
if (isArray(value)) {
|
|
226
|
+
return `[${value.map(canonical).sort().join(",")}]`;
|
|
227
|
+
}
|
|
228
|
+
else if (isObject(value)) {
|
|
229
|
+
return `{${Object.keys(value).sort().map(k => `${JSON.stringify(k)}:${canonical(value[k])}`).join(",")}}`;
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// JSON.stringify(undefined) returns undefined, not a string; substitute a distinct sentinel so the
|
|
233
|
+
// signature holds and `undefined` does not collide with the string literal `"undefined"` (which JSON
|
|
234
|
+
// would render with surrounding quotes)
|
|
235
|
+
return value === undefined ? "undefined" : JSON.stringify(value);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Tests whether a record is older than the configured TTL.
|
|
240
|
+
*
|
|
241
|
+
* Returns false when `ttl <= 0`, so the predicate can be reused at both the hit guard and the purge sweep
|
|
242
|
+
* without duplicating the boundary check. The reference timestamp defaults to `Date.now` but may be
|
|
243
|
+
* passed in to share a single snapshot across a batch of comparisons.
|
|
244
|
+
*
|
|
245
|
+
* @param created - Insertion timestamp (ms) of the record under test
|
|
246
|
+
* @param now - Reference timestamp (ms) to compare against; defaults to `Date.now`
|
|
247
|
+
*
|
|
248
|
+
* @returns true if `ttl > 0` and `created` is older than `ttl` milliseconds relative to `now`; false otherwise
|
|
249
|
+
*/
|
|
250
|
+
function expired(created, now = Date.now()) {
|
|
251
|
+
return ttl > 0 && now - created > ttl;
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Run a wrapper-initiated write under in-flight tracking.
|
|
255
|
+
*
|
|
256
|
+
* Marks `entry` as having an in-flight write, runs the configured pre-commit invalidation, delegates to
|
|
257
|
+
* `op`, and clears the marker once the delegate settles. While the marker is set, retrievals of `entry`
|
|
258
|
+
* bypass the cache so a pre-commit value can never be stored as fresh.
|
|
259
|
+
*
|
|
260
|
+
* @param entry - Resource identifier targeted by the write
|
|
261
|
+
* @param op - Thunk that delegates the write to the wrapped store and returns its promise
|
|
262
|
+
*
|
|
263
|
+
* @returns The promise returned by `op`, settled after the in-flight marker has been cleared
|
|
264
|
+
*/
|
|
265
|
+
function write(entry, op) {
|
|
266
|
+
writing.set(entry, (writing.get(entry) ?? 0) + 1);
|
|
267
|
+
invalidate(entry);
|
|
268
|
+
return op().finally(() => {
|
|
269
|
+
const count = (writing.get(entry) ?? 1) - 1;
|
|
270
|
+
if (count <= 0) {
|
|
271
|
+
writing.delete(entry);
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
writing.set(entry, count);
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Drop cached records matching a mutated resource.
|
|
280
|
+
*
|
|
281
|
+
* Iterates the cache and removes every record whose stored identifier satisfies the configured {@link match}
|
|
282
|
+
* predicate against the mutated `entry`. Called from both the pre-commit write path on the wrapper and the
|
|
283
|
+
* reactive observer attached to the delegate.
|
|
284
|
+
*
|
|
285
|
+
* @param entry - Identifier of the mutated resource
|
|
286
|
+
*/
|
|
287
|
+
function invalidate(entry) {
|
|
288
|
+
for (const [key, record] of cache) {
|
|
289
|
+
if (match(entry, record.entry)) {
|
|
290
|
+
cache.delete(key);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Enforce TTL and size bounds.
|
|
296
|
+
*
|
|
297
|
+
* Runs two sweeps: first, when `ttl > 0`, drops every record older than `ttl` milliseconds; second, when
|
|
298
|
+
* `size > 0` and the cache is overfull, drops the head slice of the Map (least-recently-used first) in
|
|
299
|
+
* a single batch sized to the exact overflow. Invoked after every miss so expired siblings and overfill
|
|
300
|
+
* are cleaned up incrementally, as the cache has no background sweeper.
|
|
301
|
+
*/
|
|
302
|
+
function purge() {
|
|
303
|
+
if (ttl > 0) {
|
|
304
|
+
const now = Date.now();
|
|
305
|
+
for (const [key, record] of cache) {
|
|
306
|
+
if (expired(record.created, now)) {
|
|
307
|
+
cache.delete(key);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
// LRU sweep: when over the size bound, take the head slice of Map keys (insertion order = LRU first)
|
|
312
|
+
// and drop them: `cache.size - size` is the exact overflow, so no loop condition needs re-checking
|
|
313
|
+
if (size > 0 && cache.size > size) {
|
|
314
|
+
Array.from(cache.keys())
|
|
315
|
+
.slice(0, cache.size - size)
|
|
316
|
+
.forEach(key => cache.delete(key));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
//# 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,57 @@
|
|
|
1
|
+
import type { Store } from "../index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Manages mutation events, transactional execution, and lifecycle for a connector's {@link StoreClient},
|
|
4
|
+
* exposing it as a full {@link Store}.
|
|
5
|
+
*
|
|
6
|
+
* A connector can implement only the {@link StoreClient} data surface and still offer the full {@link Store}
|
|
7
|
+
* contract. The connector supplies its transaction primitive through `management.execute`, which hands the
|
|
8
|
+
* connector's {@link StoreClient} to every data call. The option is required, since only the connector knows what
|
|
9
|
+
* atomicity its backend can provide. Backend primitives supplied through `management` may also back `observe` or
|
|
10
|
+
* `close`, and the wrapper fills in whatever is missing.
|
|
11
|
+
*
|
|
12
|
+
* Each standalone mutation call and each {@link Store.execute execute} call delivers a single filtered event to each
|
|
13
|
+
* matching {@link StoreObserver observer} once the call resolves. If the call rejects, its pending events are
|
|
14
|
+
* discarded and no observer is notified. Synchronous throws and asynchronous rejections from observers are caught and
|
|
15
|
+
* silently ignored, so one faulty observer cannot break delivery to others.
|
|
16
|
+
*
|
|
17
|
+
* All errors, including {@link !RangeError RangeError}, {@link @metreeca/core!TraceError | TraceError} and
|
|
18
|
+
* {@link @metreeca/http!Problem | Problem}, propagate as promise rejections, in line with the unified {@link Store}
|
|
19
|
+
* error channel.
|
|
20
|
+
*
|
|
21
|
+
* > [!IMPORTANT]
|
|
22
|
+
* > **Transaction Isolation** — Determined by the supplied `execute` option, typically through a backend
|
|
23
|
+
* > transaction primitive: the wrapper adds neither isolation nor atomicity of its own.
|
|
24
|
+
*
|
|
25
|
+
* > [!IMPORTANT]
|
|
26
|
+
* > **Mutation Events** — emits in-process observer events: each standalone mutation and each
|
|
27
|
+
* > {@link Store.execute execute} call delivers a single filtered batch to matching {@link StoreObserver observer}s
|
|
28
|
+
* > on resolve, and none on rejection. Mutations made by other clients reach local observers only if a
|
|
29
|
+
* > backend-bridging `observe` option is supplied through `management`.
|
|
30
|
+
*
|
|
31
|
+
* @param management - {@link Store} management methods backed by the connector: `execute` is required, and the
|
|
32
|
+
* wrapper fills in whatever else is missing
|
|
33
|
+
* @param management.execute - Transaction wrapper applied to every standalone StoreClient call and to the body of
|
|
34
|
+
* each {@link Store.execute execute} call, typically a backend transaction primitive (for example,
|
|
35
|
+
* `graph.execute` for a SPARQL connector) responsible for atomic commit and rollback. A backend with no
|
|
36
|
+
* transaction primitive supplies a wrapper applying the task directly to its StoreClient, and declares on its
|
|
37
|
+
* factory that a failing task neither rolls back nor isolates its mutations. The option MUST call each task it
|
|
38
|
+
* receives with a `scope` StoreClient dedicated to that call, so per-call state, such as a freshly bound graph
|
|
39
|
+
* buffer, never leaks across concurrent invocations; every data call within the transaction is routed through
|
|
40
|
+
* `scope`. Calls made by a user task inside {@link Store.execute execute} do not open further transactions: they
|
|
41
|
+
* share the outer one. The option MUST convert synchronous throws from the task into promise rejections, to
|
|
42
|
+
* preserve the unified Store error channel: lookups are routed through it with no additional guard
|
|
43
|
+
* @param management.observe - Backend registration for storage-level events, such as mutations made by other clients
|
|
44
|
+
* sharing the backend. Each observer is registered both with this option and locally, and a single detach call
|
|
45
|
+
* releases both registrations. The resource filter always arrives as an array of references, or as `undefined`
|
|
46
|
+
* for an unfiltered registration, so the option never has to handle bare references or single-pass iterables.
|
|
47
|
+
* Absent by default
|
|
48
|
+
* @param management.close - Exposed as is on the returned store. Defaults to a resolved no-op
|
|
49
|
+
*
|
|
50
|
+
* @returns An immutable {@link Store} serving data calls through the supplied `management` opts
|
|
51
|
+
*/
|
|
52
|
+
export declare function createManagingStore({ observe, execute, close }: {
|
|
53
|
+
readonly observe?: Store["observe"];
|
|
54
|
+
readonly execute: Store["execute"];
|
|
55
|
+
readonly close?: Store["close"];
|
|
56
|
+
}): Store;
|
|
57
|
+
//# sourceMappingURL=managing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"managing.d.ts","sourceRoot":"","sources":["../../src/stores/managing.ts"],"names":[],"mappings":"AAiCA,OAAO,KAAK,EAAE,KAAK,EAA8B,MAAM,aAAa,CAAC;AAGrE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiDG;AACH,wBAAgB,mBAAmB,CAAC,EAEnC,OAAO,EACP,OAAO,EAEP,KAA+B,EAE/B,EAAE;IAEF,QAAQ,CAAC,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAA;IACnC,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,SAAS,CAAC,CAAA;IAElC,QAAQ,CAAC,KAAK,CAAC,EAAE,KAAK,CAAC,OAAO,CAAC,CAAA;CAE/B,GAAG,KAAK,CA2HR"}
|
|
@@ -0,0 +1,147 @@
|
|
|
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 { some } from "@metreeca/core/arrays";
|
|
17
|
+
import { isNestedIRI } from "@metreeca/core/resource";
|
|
18
|
+
import { immutable } from "@metreeca/core/values";
|
|
19
|
+
/**
|
|
20
|
+
* Manages mutation events, transactional execution, and lifecycle for a connector's {@link StoreClient},
|
|
21
|
+
* exposing it as a full {@link Store}.
|
|
22
|
+
*
|
|
23
|
+
* A connector can implement only the {@link StoreClient} data surface and still offer the full {@link Store}
|
|
24
|
+
* contract. The connector supplies its transaction primitive through `management.execute`, which hands the
|
|
25
|
+
* connector's {@link StoreClient} to every data call. The option is required, since only the connector knows what
|
|
26
|
+
* atomicity its backend can provide. Backend primitives supplied through `management` may also back `observe` or
|
|
27
|
+
* `close`, and the wrapper fills in whatever is missing.
|
|
28
|
+
*
|
|
29
|
+
* Each standalone mutation call and each {@link Store.execute execute} call delivers a single filtered event to each
|
|
30
|
+
* matching {@link StoreObserver observer} once the call resolves. If the call rejects, its pending events are
|
|
31
|
+
* discarded and no observer is notified. Synchronous throws and asynchronous rejections from observers are caught and
|
|
32
|
+
* silently ignored, so one faulty observer cannot break delivery to others.
|
|
33
|
+
*
|
|
34
|
+
* All errors, including {@link !RangeError RangeError}, {@link @metreeca/core!TraceError | TraceError} and
|
|
35
|
+
* {@link @metreeca/http!Problem | Problem}, propagate as promise rejections, in line with the unified {@link Store}
|
|
36
|
+
* error channel.
|
|
37
|
+
*
|
|
38
|
+
* > [!IMPORTANT]
|
|
39
|
+
* > **Transaction Isolation** — Determined by the supplied `execute` option, typically through a backend
|
|
40
|
+
* > transaction primitive: the wrapper adds neither isolation nor atomicity of its own.
|
|
41
|
+
*
|
|
42
|
+
* > [!IMPORTANT]
|
|
43
|
+
* > **Mutation Events** — emits in-process observer events: each standalone mutation and each
|
|
44
|
+
* > {@link Store.execute execute} call delivers a single filtered batch to matching {@link StoreObserver observer}s
|
|
45
|
+
* > on resolve, and none on rejection. Mutations made by other clients reach local observers only if a
|
|
46
|
+
* > backend-bridging `observe` option is supplied through `management`.
|
|
47
|
+
*
|
|
48
|
+
* @param management - {@link Store} management methods backed by the connector: `execute` is required, and the
|
|
49
|
+
* wrapper fills in whatever else is missing
|
|
50
|
+
* @param management.execute - Transaction wrapper applied to every standalone StoreClient call and to the body of
|
|
51
|
+
* each {@link Store.execute execute} call, typically a backend transaction primitive (for example,
|
|
52
|
+
* `graph.execute` for a SPARQL connector) responsible for atomic commit and rollback. A backend with no
|
|
53
|
+
* transaction primitive supplies a wrapper applying the task directly to its StoreClient, and declares on its
|
|
54
|
+
* factory that a failing task neither rolls back nor isolates its mutations. The option MUST call each task it
|
|
55
|
+
* receives with a `scope` StoreClient dedicated to that call, so per-call state, such as a freshly bound graph
|
|
56
|
+
* buffer, never leaks across concurrent invocations; every data call within the transaction is routed through
|
|
57
|
+
* `scope`. Calls made by a user task inside {@link Store.execute execute} do not open further transactions: they
|
|
58
|
+
* share the outer one. The option MUST convert synchronous throws from the task into promise rejections, to
|
|
59
|
+
* preserve the unified Store error channel: lookups are routed through it with no additional guard
|
|
60
|
+
* @param management.observe - Backend registration for storage-level events, such as mutations made by other clients
|
|
61
|
+
* sharing the backend. Each observer is registered both with this option and locally, and a single detach call
|
|
62
|
+
* releases both registrations. The resource filter always arrives as an array of references, or as `undefined`
|
|
63
|
+
* for an unfiltered registration, so the option never has to handle bare references or single-pass iterables.
|
|
64
|
+
* Absent by default
|
|
65
|
+
* @param management.close - Exposed as is on the returned store. Defaults to a resolved no-op
|
|
66
|
+
*
|
|
67
|
+
* @returns An immutable {@link Store} serving data calls through the supplied `management` opts
|
|
68
|
+
*/
|
|
69
|
+
export function createManagingStore({ observe, execute, close = () => Promise.resolve() }) {
|
|
70
|
+
const observers = new Map();
|
|
71
|
+
return immutable({
|
|
72
|
+
lookup: (request, opts) => execute(store => store.lookup(request, opts)),
|
|
73
|
+
create: request => notify(({ mutated }) => execute(store => store.create(request).then(mutated))),
|
|
74
|
+
update: request => notify(({ mutated }) => execute(store => store.update(request).then(mutated))),
|
|
75
|
+
delete: request => notify(({ deleted }) => execute(store => store.delete(request).then(deleted))),
|
|
76
|
+
insert: request => notify(({ mutated }) => execute(store => store.insert(request).then(mutated))),
|
|
77
|
+
remove: request => notify(({ deleted }) => execute(store => store.remove(request).then(deleted))),
|
|
78
|
+
observe(observer, resources) {
|
|
79
|
+
// normalise upfront: the filter is drawn from once, so a single-pass iterable is safely
|
|
80
|
+
// shared between the local registration and the delegate
|
|
81
|
+
const filter = resources === undefined ? undefined : some(resources);
|
|
82
|
+
if (filter?.length === 0) {
|
|
83
|
+
return observe?.(observer, []) ?? (() => { });
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
const token = Symbol();
|
|
87
|
+
const detach = observe?.(observer, filter);
|
|
88
|
+
observers.set(token, { observer, resources: filter });
|
|
89
|
+
return () => {
|
|
90
|
+
try {
|
|
91
|
+
detach?.();
|
|
92
|
+
}
|
|
93
|
+
finally {
|
|
94
|
+
observers.delete(token);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
execute(task) {
|
|
100
|
+
return notify(({ mutated, deleted }) => execute(store => task(immutable({
|
|
101
|
+
lookup: (request, opts) => store.lookup(request, opts),
|
|
102
|
+
create: request => store.create(request).then(mutated),
|
|
103
|
+
update: request => store.update(request).then(mutated),
|
|
104
|
+
delete: request => store.delete(request).then(deleted),
|
|
105
|
+
insert: request => store.insert(request).then(mutated),
|
|
106
|
+
remove: request => store.remove(request).then(deleted)
|
|
107
|
+
}))));
|
|
108
|
+
},
|
|
109
|
+
close
|
|
110
|
+
});
|
|
111
|
+
async function notify(task) {
|
|
112
|
+
const mutations = new Map();
|
|
113
|
+
const value = await task({
|
|
114
|
+
mutated: entry => {
|
|
115
|
+
if (entry !== undefined) {
|
|
116
|
+
mutations.set(entry, true);
|
|
117
|
+
}
|
|
118
|
+
return entry;
|
|
119
|
+
},
|
|
120
|
+
deleted: entry => {
|
|
121
|
+
if (entry !== undefined) {
|
|
122
|
+
mutations.set(entry, false);
|
|
123
|
+
}
|
|
124
|
+
return entry;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
if (mutations.size > 0) {
|
|
128
|
+
observers.forEach(({ observer, resources }) => {
|
|
129
|
+
const event = {};
|
|
130
|
+
mutations.forEach((exists, id) => {
|
|
131
|
+
// `resources` is `undefined` for an unfiltered registration (fires for every mutation)
|
|
132
|
+
// and an array (possibly empty) for a filtered one
|
|
133
|
+
if (resources === undefined || resources.some(r => isNestedIRI(r, id))) {
|
|
134
|
+
event[id] = exists;
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
if (Object.keys(event).length > 0) {
|
|
138
|
+
Promise.resolve(event)
|
|
139
|
+
.then(observer)
|
|
140
|
+
.catch(() => { });
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return value;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=managing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"managing.js","sourceRoot":"","sources":["../../src/stores/managing.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metreeca/keep",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.11.0",
|
|
4
|
+
"description": "Core model-driven storage API for @metreeca/keep.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
7
|
-
"
|
|
7
|
+
"json-ld",
|
|
8
|
+
"model-driven",
|
|
9
|
+
"storage"
|
|
8
10
|
],
|
|
9
11
|
"homepage": "https://metreeca.github.io/keep/",
|
|
10
12
|
"bugs": {
|
|
@@ -19,14 +21,32 @@
|
|
|
19
21
|
},
|
|
20
22
|
"repository": {
|
|
21
23
|
"type": "git",
|
|
22
|
-
"url": "git+https://github.com/metreeca/keep.git"
|
|
24
|
+
"url": "git+https://github.com/metreeca/keep.git",
|
|
25
|
+
"directory": "packages/components/keep"
|
|
23
26
|
},
|
|
24
27
|
"type": "module",
|
|
25
28
|
"sideEffects": false,
|
|
26
29
|
"exports": {
|
|
27
|
-
".":
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
".": {
|
|
31
|
+
"@metreeca/source": "./src/index.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"./validating": {
|
|
35
|
+
"@metreeca/source": "./src/clients/validating.ts",
|
|
36
|
+
"default": "./dist/clients/validating.js"
|
|
37
|
+
},
|
|
38
|
+
"./batching": {
|
|
39
|
+
"@metreeca/source": "./src/clients/batching.ts",
|
|
40
|
+
"default": "./dist/clients/batching.js"
|
|
41
|
+
},
|
|
42
|
+
"./caching": {
|
|
43
|
+
"@metreeca/source": "./src/stores/caching.ts",
|
|
44
|
+
"default": "./dist/stores/caching.js"
|
|
45
|
+
},
|
|
46
|
+
"./managing": {
|
|
47
|
+
"@metreeca/source": "./src/stores/managing.ts",
|
|
48
|
+
"default": "./dist/stores/managing.js"
|
|
49
|
+
}
|
|
30
50
|
},
|
|
31
51
|
"files": [
|
|
32
52
|
"README.md",
|
|
@@ -34,24 +54,20 @@
|
|
|
34
54
|
"dist"
|
|
35
55
|
],
|
|
36
56
|
"engines": {
|
|
37
|
-
"node": ">=22.
|
|
57
|
+
"node": ">=22.22.0"
|
|
38
58
|
},
|
|
39
59
|
"scripts": {
|
|
40
60
|
"clean": "rm -rf dist docs node_modules",
|
|
41
|
-
"
|
|
42
|
-
"
|
|
43
|
-
"check": "npx vitest --typecheck run",
|
|
44
|
-
"watch": "npx tsc --watch",
|
|
45
|
-
"proof": "npx typedoc && (npx typedoc --watch & npx five-server docs --open)"
|
|
61
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json",
|
|
62
|
+
"check": "npx vitest --typecheck run"
|
|
46
63
|
},
|
|
47
64
|
"dependencies": {
|
|
48
|
-
"@metreeca/
|
|
49
|
-
"@metreeca/
|
|
50
|
-
"@metreeca/
|
|
65
|
+
"@metreeca/blue": "^0.12.0",
|
|
66
|
+
"@metreeca/core": "^0.12.0",
|
|
67
|
+
"@metreeca/http": "^0.4.0",
|
|
68
|
+
"@metreeca/qest": "^0.12.0"
|
|
51
69
|
},
|
|
52
70
|
"devDependencies": {
|
|
53
|
-
"five-server": "^0.4.5",
|
|
54
|
-
"typedoc": "^0.28.14",
|
|
55
71
|
"typescript": "^5.9.3",
|
|
56
72
|
"vitest": "^4.0.13"
|
|
57
73
|
}
|
package/dist/index/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index/index.ts"],"names":[],"mappings":""}
|