@synnaxlabs/client 0.57.0 → 0.57.2
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/dist/{client-B08VDLSA.js → client-BsVW3qAW.js} +3909 -3818
- package/dist/client-BsVW3qAW.js.map +1 -0
- package/dist/client.js +477 -477
- package/dist/client.js.map +1 -1
- package/dist/src/framer/cache/dynamic.d.ts +21 -1
- package/dist/src/framer/cache/dynamic.d.ts.map +1 -1
- package/dist/src/framer/cache/reader.d.ts +16 -2
- package/dist/src/framer/cache/reader.d.ts.map +1 -1
- package/dist/src/framer/cache/unary.d.ts +6 -4
- package/dist/src/framer/cache/unary.d.ts.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/label/store.d.ts +1 -1
- package/dist/src/label/store.d.ts.map +1 -1
- package/dist/src/ontology/client.d.ts.map +1 -1
- package/dist/src/ontology/store.d.ts +16 -1
- package/dist/src/ontology/store.d.ts.map +1 -1
- package/dist/src/query/external.d.ts +1 -0
- package/dist/src/query/external.d.ts.map +1 -1
- package/dist/src/query/indexes.d.ts +32 -0
- package/dist/src/query/indexes.d.ts.map +1 -0
- package/dist/src/query/table.d.ts +10 -1
- package/dist/src/query/table.d.ts.map +1 -1
- package/dist/src/ranger/client.d.ts +2 -0
- package/dist/src/ranger/client.d.ts.map +1 -1
- package/dist/src/status/client.d.ts.map +1 -1
- package/dist/testutil.js +1 -1
- package/package.json +4 -4
- package/src/framer/cache/dynamic.ts +104 -43
- package/src/framer/cache/reader.ts +120 -49
- package/src/framer/cache/unary.ts +9 -5
- package/src/index.ts +1 -0
- package/src/label/store.ts +7 -2
- package/src/ontology/client.ts +15 -8
- package/src/ontology/store.ts +27 -5
- package/src/query/external.ts +1 -0
- package/src/query/indexes.ts +81 -0
- package/src/query/table.ts +25 -5
- package/src/ranger/client.ts +26 -16
- package/src/status/client.ts +14 -8
- package/dist/client-B08VDLSA.js.map +0 -1
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Copyright 2026 Synnax Labs, Inc.
|
|
2
|
+
//
|
|
3
|
+
// Use of this software is governed by the Business Source License included in the file
|
|
4
|
+
// licenses/BSL.txt.
|
|
5
|
+
//
|
|
6
|
+
// As of the Change Date specified in that file, in accordance with the Business Source
|
|
7
|
+
// License, use of this software will be governed by the Apache License, Version 2.0,
|
|
8
|
+
// included in the file licenses/APL.txt.
|
|
9
|
+
|
|
10
|
+
import { type record, type state } from "@synnaxlabs/x";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* An exact-match secondary index over a table's live entries. `extract` reads one
|
|
14
|
+
* index value from each entry. Register the index on its owning table's `indexes`
|
|
15
|
+
* param; the table keeps it current across every mutation, rollbacks and resets
|
|
16
|
+
* included. Tombstones are never indexed.
|
|
17
|
+
*/
|
|
18
|
+
export class LookupIndex<
|
|
19
|
+
Key extends record.Key = record.Key,
|
|
20
|
+
Value extends state.State = state.State,
|
|
21
|
+
IndexValue extends record.Key = record.Key,
|
|
22
|
+
> {
|
|
23
|
+
private readonly extract: (value: Value) => IndexValue;
|
|
24
|
+
private readonly buckets = new Map<IndexValue, Map<Key, Value>>();
|
|
25
|
+
private readonly indexed = new Map<Key, IndexValue>();
|
|
26
|
+
|
|
27
|
+
constructor(extract: (value: Value) => IndexValue) {
|
|
28
|
+
this.extract = extract;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Returns the entries whose extracted index value equals the given value. */
|
|
32
|
+
get(value: IndexValue): Value[] {
|
|
33
|
+
const bucket = this.buckets.get(value);
|
|
34
|
+
if (bucket == null) return [];
|
|
35
|
+
return Array.from(bucket.values());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Records the entry, relocating it when its extracted value changed.
|
|
40
|
+
* Cache-internal surface: called by the owning table, not domain code.
|
|
41
|
+
*/
|
|
42
|
+
set(key: Key, value: Value): void {
|
|
43
|
+
const next = this.extract(value);
|
|
44
|
+
const prev = this.indexed.get(key);
|
|
45
|
+
if (prev !== undefined && prev !== next) this.unbucket(prev, key);
|
|
46
|
+
let bucket = this.buckets.get(next);
|
|
47
|
+
if (bucket == null) {
|
|
48
|
+
bucket = new Map();
|
|
49
|
+
this.buckets.set(next, bucket);
|
|
50
|
+
}
|
|
51
|
+
bucket.set(key, value);
|
|
52
|
+
this.indexed.set(key, next);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Removes any mapping for the given entry key.
|
|
57
|
+
* Cache-internal surface: called by the owning table, not domain code.
|
|
58
|
+
*/
|
|
59
|
+
delete(key: Key): void {
|
|
60
|
+
const prev = this.indexed.get(key);
|
|
61
|
+
if (prev === undefined) return;
|
|
62
|
+
this.unbucket(prev, key);
|
|
63
|
+
this.indexed.delete(key);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Discards every mapping.
|
|
68
|
+
* Cache-internal surface: called by the owning table, not domain code.
|
|
69
|
+
*/
|
|
70
|
+
reset(): void {
|
|
71
|
+
this.buckets.clear();
|
|
72
|
+
this.indexed.clear();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private unbucket(value: IndexValue, key: Key): void {
|
|
76
|
+
const bucket = this.buckets.get(value);
|
|
77
|
+
if (bucket == null) return;
|
|
78
|
+
bucket.delete(key);
|
|
79
|
+
if (bucket.size === 0) this.buckets.delete(value);
|
|
80
|
+
}
|
|
81
|
+
}
|
package/src/query/table.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type z from "zod";
|
|
|
23
23
|
|
|
24
24
|
import { NotFoundError } from "@/errors";
|
|
25
25
|
import { Deleted } from "@/query/deleted";
|
|
26
|
+
import { type LookupIndex } from "@/query/indexes";
|
|
26
27
|
import { type Listener } from "@/query/streamer";
|
|
27
28
|
|
|
28
29
|
/**
|
|
@@ -98,6 +99,11 @@ export interface TableParams<
|
|
|
98
99
|
* @default TimeSpan.milliseconds(10)
|
|
99
100
|
*/
|
|
100
101
|
fetchDebounce?: CrudeTimeSpan;
|
|
102
|
+
/**
|
|
103
|
+
* Secondary indexes over the table's live entries. The table keeps each current
|
|
104
|
+
* across every mutation, rollbacks and resets included.
|
|
105
|
+
*/
|
|
106
|
+
indexes?: Array<LookupIndex<Key, Value>>;
|
|
101
107
|
}
|
|
102
108
|
|
|
103
109
|
/**
|
|
@@ -156,6 +162,7 @@ export class Table<
|
|
|
156
162
|
Key[],
|
|
157
163
|
Array<Keyed<Key, Value>>
|
|
158
164
|
> | null;
|
|
165
|
+
private readonly indexes: Array<LookupIndex<Key, Value>>;
|
|
159
166
|
private gen = 0;
|
|
160
167
|
|
|
161
168
|
constructor({
|
|
@@ -164,11 +171,13 @@ export class Table<
|
|
|
164
171
|
fetch,
|
|
165
172
|
hydrate = "set",
|
|
166
173
|
fetchDebounce = DEFAULT_FETCH_DEBOUNCE,
|
|
174
|
+
indexes = [],
|
|
167
175
|
}: TableParams<Key, Value>) {
|
|
168
176
|
this.onError = onError;
|
|
169
177
|
this.equal = equal;
|
|
170
178
|
this.fetchEntries = fetch;
|
|
171
179
|
this.hydrateMode = hydrate;
|
|
180
|
+
this.indexes = indexes;
|
|
172
181
|
this.fetchBatcher =
|
|
173
182
|
fetch == null
|
|
174
183
|
? null
|
|
@@ -188,6 +197,16 @@ export class Table<
|
|
|
188
197
|
});
|
|
189
198
|
}
|
|
190
199
|
|
|
200
|
+
private applySet(key: Key, value: Value): void {
|
|
201
|
+
this.entries.set(key, value);
|
|
202
|
+
for (const index of this.indexes) index.set(key, value);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private applyDelete(key: Key): void {
|
|
206
|
+
this.entries.delete(key);
|
|
207
|
+
for (const index of this.indexes) index.delete(key);
|
|
208
|
+
}
|
|
209
|
+
|
|
191
210
|
private setOne(
|
|
192
211
|
key: Key,
|
|
193
212
|
value: state.SetArg<Value | undefined>,
|
|
@@ -197,16 +216,16 @@ export class Table<
|
|
|
197
216
|
if (next == null || (prev != null && this.equal(next, prev, key))) return undefined;
|
|
198
217
|
const prevTombstone = this.tombstones.get(key);
|
|
199
218
|
this.tombstones.delete(key);
|
|
200
|
-
this.
|
|
219
|
+
this.applySet(key, next);
|
|
201
220
|
this.notify({ variant: "set", key, value: next });
|
|
202
221
|
|
|
203
222
|
return () => {
|
|
204
223
|
if (prev === undefined) {
|
|
205
|
-
this.
|
|
224
|
+
this.applyDelete(key);
|
|
206
225
|
if (prevTombstone != null) this.tombstones.set(key, prevTombstone);
|
|
207
226
|
this.notify({ variant: "delete", key });
|
|
208
227
|
} else {
|
|
209
|
-
this.
|
|
228
|
+
this.applySet(key, prev);
|
|
210
229
|
this.notify({ variant: "set", key, value: prev });
|
|
211
230
|
}
|
|
212
231
|
};
|
|
@@ -375,7 +394,7 @@ export class Table<
|
|
|
375
394
|
|
|
376
395
|
this.batch(() =>
|
|
377
396
|
toDelete.forEach(({ key: k, value }) => {
|
|
378
|
-
this.
|
|
397
|
+
this.applyDelete(k);
|
|
379
398
|
if (tombstone && value != null)
|
|
380
399
|
this.tombstones.set(k, new Deleted(value, TimeStamp.now()));
|
|
381
400
|
this.notify({ variant: "delete", key: k });
|
|
@@ -387,7 +406,7 @@ export class Table<
|
|
|
387
406
|
toDelete.forEach(({ key: k, value }) => {
|
|
388
407
|
if (value == null) return;
|
|
389
408
|
this.tombstones.delete(k);
|
|
390
|
-
this.
|
|
409
|
+
this.applySet(k, value);
|
|
391
410
|
this.notify({ variant: "set", key: k, value });
|
|
392
411
|
}),
|
|
393
412
|
);
|
|
@@ -426,6 +445,7 @@ export class Table<
|
|
|
426
445
|
this.gen++;
|
|
427
446
|
this.entries.clear();
|
|
428
447
|
this.tombstones.clear();
|
|
448
|
+
for (const index of this.indexes) index.reset();
|
|
429
449
|
}
|
|
430
450
|
|
|
431
451
|
/**
|
package/src/ranger/client.ts
CHANGED
|
@@ -340,16 +340,13 @@ const watchRelationships = <Q extends query.Params>(
|
|
|
340
340
|
affectedRangeKeys(relOfEvent(event)),
|
|
341
341
|
);
|
|
342
342
|
|
|
343
|
-
const rangesWithLabel = (
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const keys = relationships
|
|
348
|
-
.get(
|
|
343
|
+
const rangesWithLabel = (cache: ontology.Cache, key: label.Key): Key[] | null => {
|
|
344
|
+
const keys = cache
|
|
345
|
+
.relationshipsTo(label.ontologyID(key))
|
|
346
|
+
.filter(
|
|
349
347
|
(r) =>
|
|
350
348
|
r.type === label.LABELED_BY_ONTOLOGY_RELATIONSHIP_TYPE &&
|
|
351
|
-
r.from.type === "range"
|
|
352
|
-
r.to.key === key,
|
|
349
|
+
r.from.type === "range",
|
|
353
350
|
)
|
|
354
351
|
.map((r) => r.from.key);
|
|
355
352
|
return keys.length === 0 ? null : keys;
|
|
@@ -358,10 +355,10 @@ const rangesWithLabel = (
|
|
|
358
355
|
/** Projects label content changes onto the ranges they label. */
|
|
359
356
|
const watchLabels = <Q extends query.Params>(
|
|
360
357
|
labels: query.Table<label.Key, label.Label>,
|
|
361
|
-
|
|
358
|
+
cache: ontology.Cache,
|
|
362
359
|
): query.Watch<Q, Key> =>
|
|
363
360
|
query.watch<Q, Key, label.Key, label.Label>(labels, (event) =>
|
|
364
|
-
rangesWithLabel(
|
|
361
|
+
rangesWithLabel(cache, event.key),
|
|
365
362
|
);
|
|
366
363
|
|
|
367
364
|
/** Config for {@link Client}. */
|
|
@@ -422,7 +419,9 @@ export class Client extends query.Retriever<
|
|
|
422
419
|
query.deriveWatch(relationships, (event) =>
|
|
423
420
|
affectedRangeKeys(relOfEvent(event)),
|
|
424
421
|
),
|
|
425
|
-
query.deriveWatch(labels, (event) =>
|
|
422
|
+
query.deriveWatch(labels, (event) =>
|
|
423
|
+
rangesWithLabel(ontologyClient.cache, event.key),
|
|
424
|
+
),
|
|
426
425
|
],
|
|
427
426
|
});
|
|
428
427
|
const single = cache.queries<Key | Name, Range, Key, Range>({
|
|
@@ -443,7 +442,7 @@ export class Client extends query.Retriever<
|
|
|
443
442
|
matches: (r, query) => this.requestMatches(r, query),
|
|
444
443
|
watch: [
|
|
445
444
|
watchRelationships<RetrieveRequest>(relationships),
|
|
446
|
-
watchLabels<RetrieveRequest>(labels,
|
|
445
|
+
watchLabels<RetrieveRequest>(labels, ontologyClient.cache),
|
|
447
446
|
],
|
|
448
447
|
},
|
|
449
448
|
compose: (r) => this.composeOne(r),
|
|
@@ -597,7 +596,7 @@ export class Client extends query.Retriever<
|
|
|
597
596
|
private composeOne(cached: Range): Range {
|
|
598
597
|
const id = ontologyID(cached.key);
|
|
599
598
|
const labels = label.cachedLabelsOf(
|
|
600
|
-
this.cfg.ontology.cache
|
|
599
|
+
this.cfg.ontology.cache,
|
|
601
600
|
this.cfg.labels.store,
|
|
602
601
|
id,
|
|
603
602
|
);
|
|
@@ -642,6 +641,17 @@ export class Client extends query.Retriever<
|
|
|
642
641
|
}
|
|
643
642
|
}
|
|
644
643
|
|
|
644
|
+
/** Writes a fetch response as one batch per table, so each table flushes once. */
|
|
645
|
+
private writeThroughMany(ranges: Range[]): void {
|
|
646
|
+
this.store.batch(() =>
|
|
647
|
+
this.cfg.labels.store.batch(() =>
|
|
648
|
+
this.cfg.ontology.cache.relationships.batch(() =>
|
|
649
|
+
ranges.forEach((r) => this.writeThrough(r)),
|
|
650
|
+
),
|
|
651
|
+
),
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
|
|
645
655
|
/**
|
|
646
656
|
* Fetches the given keys with labels and parents included, writing them and
|
|
647
657
|
* their relationships through. Powers the table's fetch primitive.
|
|
@@ -652,7 +662,7 @@ export class Client extends query.Retriever<
|
|
|
652
662
|
keys,
|
|
653
663
|
ignoreNotFoundError: true,
|
|
654
664
|
});
|
|
655
|
-
|
|
665
|
+
this.writeThroughMany(ranges);
|
|
656
666
|
return ranges;
|
|
657
667
|
}
|
|
658
668
|
|
|
@@ -686,7 +696,7 @@ export class Client extends query.Retriever<
|
|
|
686
696
|
private async fetchRequest(query: RetrieveRequest): Promise<Range[]> {
|
|
687
697
|
if (isKeysOnly(query)) return await this.store.retrieve(query.keys);
|
|
688
698
|
const ranges = await this.execRetrieve({ ...BASE_REQUEST, ...query });
|
|
689
|
-
|
|
699
|
+
this.writeThroughMany(ranges);
|
|
690
700
|
return ranges;
|
|
691
701
|
}
|
|
692
702
|
|
|
@@ -701,7 +711,7 @@ export class Client extends query.Retriever<
|
|
|
701
711
|
return false;
|
|
702
712
|
if (primitive.isNonZero(req.hasLabels)) {
|
|
703
713
|
const labels = label.cachedLabelsOf(
|
|
704
|
-
this.cfg.ontology.cache
|
|
714
|
+
this.cfg.ontology.cache,
|
|
705
715
|
this.cfg.labels.store,
|
|
706
716
|
ontologyID(r.key),
|
|
707
717
|
);
|
package/src/status/client.ts
CHANGED
|
@@ -99,7 +99,7 @@ export class Client extends query.Retriever<
|
|
|
99
99
|
value: (changed, prev) => {
|
|
100
100
|
const next = { ...prev, ...changed };
|
|
101
101
|
const id = ontologyID(changed.key);
|
|
102
|
-
next.labels = label.cachedLabelsOf(
|
|
102
|
+
next.labels = label.cachedLabelsOf(ontologyClient.cache, labels.store, id);
|
|
103
103
|
return next;
|
|
104
104
|
},
|
|
105
105
|
}),
|
|
@@ -278,7 +278,7 @@ export class Client extends query.Retriever<
|
|
|
278
278
|
/** Rebuilds a cached status with its cached labels attached. */
|
|
279
279
|
private compose(cached: Status): Status {
|
|
280
280
|
const labels = label.cachedLabelsOf(
|
|
281
|
-
this.cfg.ontology.cache
|
|
281
|
+
this.cfg.ontology.cache,
|
|
282
282
|
this.cfg.labels.store,
|
|
283
283
|
ontologyID(cached.key),
|
|
284
284
|
);
|
|
@@ -307,7 +307,14 @@ export class Client extends query.Retriever<
|
|
|
307
307
|
/** Fetches statuses and writes their included labels through the caches. */
|
|
308
308
|
private async fetchThrough(req: RetrieveRequest): Promise<Status[]> {
|
|
309
309
|
const statuses = await this.execRetrieve({ ...BASE_REQUEST, ...req });
|
|
310
|
-
|
|
310
|
+
// One batch per table, so each table flushes once for the whole response.
|
|
311
|
+
this.store.batch(() =>
|
|
312
|
+
this.cfg.labels.store.batch(() =>
|
|
313
|
+
this.cfg.ontology.cache.relationships.batch(() =>
|
|
314
|
+
statuses.forEach((s) => this.writeThrough(s)),
|
|
315
|
+
),
|
|
316
|
+
),
|
|
317
|
+
);
|
|
311
318
|
return statuses;
|
|
312
319
|
}
|
|
313
320
|
|
|
@@ -362,13 +369,12 @@ export class Client extends query.Retriever<
|
|
|
362
369
|
|
|
363
370
|
/** Returns the keys of cached statuses labeled by the given label. */
|
|
364
371
|
private statusesLabeledBy(labelKey: label.Key): Key[] {
|
|
365
|
-
return this.cfg.ontology.cache
|
|
366
|
-
.
|
|
372
|
+
return this.cfg.ontology.cache
|
|
373
|
+
.relationshipsTo(label.ontologyID(labelKey))
|
|
374
|
+
.filter(
|
|
367
375
|
(r) =>
|
|
368
376
|
r.type === label.LABELED_BY_ONTOLOGY_RELATIONSHIP_TYPE &&
|
|
369
|
-
r.from.type === "status"
|
|
370
|
-
r.to.type === "label" &&
|
|
371
|
-
r.to.key === labelKey,
|
|
377
|
+
r.from.type === "status",
|
|
372
378
|
)
|
|
373
379
|
.map((r) => r.from.key);
|
|
374
380
|
}
|