@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.
Files changed (41) hide show
  1. package/dist/{client-B08VDLSA.js → client-BsVW3qAW.js} +3909 -3818
  2. package/dist/client-BsVW3qAW.js.map +1 -0
  3. package/dist/client.js +477 -477
  4. package/dist/client.js.map +1 -1
  5. package/dist/src/framer/cache/dynamic.d.ts +21 -1
  6. package/dist/src/framer/cache/dynamic.d.ts.map +1 -1
  7. package/dist/src/framer/cache/reader.d.ts +16 -2
  8. package/dist/src/framer/cache/reader.d.ts.map +1 -1
  9. package/dist/src/framer/cache/unary.d.ts +6 -4
  10. package/dist/src/framer/cache/unary.d.ts.map +1 -1
  11. package/dist/src/index.d.ts +1 -1
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/label/store.d.ts +1 -1
  14. package/dist/src/label/store.d.ts.map +1 -1
  15. package/dist/src/ontology/client.d.ts.map +1 -1
  16. package/dist/src/ontology/store.d.ts +16 -1
  17. package/dist/src/ontology/store.d.ts.map +1 -1
  18. package/dist/src/query/external.d.ts +1 -0
  19. package/dist/src/query/external.d.ts.map +1 -1
  20. package/dist/src/query/indexes.d.ts +32 -0
  21. package/dist/src/query/indexes.d.ts.map +1 -0
  22. package/dist/src/query/table.d.ts +10 -1
  23. package/dist/src/query/table.d.ts.map +1 -1
  24. package/dist/src/ranger/client.d.ts +2 -0
  25. package/dist/src/ranger/client.d.ts.map +1 -1
  26. package/dist/src/status/client.d.ts.map +1 -1
  27. package/dist/testutil.js +1 -1
  28. package/package.json +4 -4
  29. package/src/framer/cache/dynamic.ts +104 -43
  30. package/src/framer/cache/reader.ts +120 -49
  31. package/src/framer/cache/unary.ts +9 -5
  32. package/src/index.ts +1 -0
  33. package/src/label/store.ts +7 -2
  34. package/src/ontology/client.ts +15 -8
  35. package/src/ontology/store.ts +27 -5
  36. package/src/query/external.ts +1 -0
  37. package/src/query/indexes.ts +81 -0
  38. package/src/query/table.ts +25 -5
  39. package/src/ranger/client.ts +26 -16
  40. package/src/status/client.ts +14 -8
  41. 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
+ }
@@ -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.entries.set(key, next);
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.entries.delete(key);
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.entries.set(key, prev);
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.entries.delete(k);
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.entries.set(k, value);
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
  /**
@@ -340,16 +340,13 @@ const watchRelationships = <Q extends query.Params>(
340
340
  affectedRangeKeys(relOfEvent(event)),
341
341
  );
342
342
 
343
- const rangesWithLabel = (
344
- relationships: query.Table<string, ontology.Relationship>,
345
- key: label.Key,
346
- ): Key[] | null => {
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
- relationships: query.Table<string, ontology.Relationship>,
358
+ cache: ontology.Cache,
362
359
  ): query.Watch<Q, Key> =>
363
360
  query.watch<Q, Key, label.Key, label.Label>(labels, (event) =>
364
- rangesWithLabel(relationships, event.key),
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) => rangesWithLabel(relationships, event.key)),
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, relationships),
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.relationships,
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
- ranges.forEach((r) => this.writeThrough(r));
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
- ranges.forEach((r) => this.writeThrough(r));
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.relationships,
714
+ this.cfg.ontology.cache,
705
715
  this.cfg.labels.store,
706
716
  ontologyID(r.key),
707
717
  );
@@ -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(relationships, labels.store, id);
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.relationships,
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
- statuses.forEach((s) => this.writeThrough(s));
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.relationships
366
- .get(
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
  }