doxum 0.1.38 → 0.1.40

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 CHANGED
@@ -322,6 +322,16 @@ const ids = derive.keyed.keys(records);
322
322
  const all = derive.keyed.values(records);
323
323
  const entries = derive.keyed.entries(records);
324
324
  const active = derive.keyed.get(records, activeRecordId);
325
+ const selectedRecords = derive.keyed.subset(
326
+ records,
327
+ { selection },
328
+ ({ selection }) => selection.recordIds
329
+ );
330
+ const selectedRecord = derive.keyed.get(
331
+ records,
332
+ { selection },
333
+ ({ selection }) => selection.activeRecordId
334
+ );
325
335
  const rowsFromArray = derive.keyed.from(rowArray, row => row.id);
326
336
  const effectiveRows = derive.keyed.merge([baseRows, rowOverrides], { conflict: 'last' });
327
337
  const visible = derive.keyed.filter(records, record => record.visible);
@@ -342,6 +352,15 @@ source-relative order. `compact` omits keys whose selected value is `undefined`
342
352
  uses its optional equality for present values. `filter` and `compact` support the same
343
353
  named global/dynamic keyed dependency object as ordinary `derive.keyed`.
344
354
 
355
+ `get` accepts a static key, a key projection, or `(source, dependencies, selectKey, equality?)`.
356
+ `subset` accepts a static ordered key array, a key-array projection, or
357
+ `(source, dependencies, selectOrderedKeys)`. Named selectors run when their declared
358
+ dependencies change; source-only updates use the selected keys to filter deltas.
359
+ Missing requested keys remain live for later adds. Subset preserves source value
360
+ references and requested order, and has no value equality option. Unrelated source
361
+ updates may wake the processor to inspect deltas, but do not rerun selectors or rebuild
362
+ the subset. See [Keyed selection](docs/projections.md#keyed-selection).
363
+
345
364
  `from` turns either a static readonly array or `Projection<readonly V[]>` into a keyed
346
365
  projection using `keyOf(value)`. Input order becomes formal keyed order; duplicate keys
347
366
  are errors rather than implicit overwrites. Static outer arrays are snapshotted at
package/dist/advanced.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_transform = require("./transform-D45KWMzj.cjs");
2
+ const require_transform = require("./transform-8xXf0OKB.cjs");
3
3
  const require_record = require("./record-ChYUi8PP.cjs");
4
4
  //#region core/src/projection/advanced.ts
5
5
  const resetCollectionChange = Object.freeze({ kind: "reset" });
package/dist/advanced.js CHANGED
@@ -1,4 +1,4 @@
1
- import { D as defineProcessor, S as compileProjectionDependencies, _ as assertSynchronous, r as collectionChange, t as createKeyedTransform, w as collectionView } from "./transform-3mj0eStL.js";
1
+ import { D as defineProcessor, S as compileProjectionDependencies, _ as assertSynchronous, r as collectionChange, t as createKeyedTransform, w as collectionView } from "./transform--WCn6_v0.js";
2
2
  import { n as isPlainObject } from "./record-B2yRhYsc.js";
3
3
  //#region core/src/projection/advanced.ts
4
4
  const resetCollectionChange = Object.freeze({ kind: "reset" });
package/dist/index.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_transform = require("./transform-D45KWMzj.cjs");
2
+ const require_transform = require("./transform-8xXf0OKB.cjs");
3
3
  const require_record = require("./record-ChYUi8PP.cjs");
4
4
  const require_driver = require("./driver-CN3dGxQI.cjs");
5
5
  //#region core/src/schema/model.ts
@@ -2883,6 +2883,163 @@ function createKeyedFlatMap(source, dependenciesOrSelect, selectOrEquality, mayb
2883
2883
  return projection;
2884
2884
  }
2885
2885
  //#endregion
2886
+ //#region core/src/projection/keyed/selection.ts
2887
+ /** Compiles selection inputs; runtime instances retain only the request and its dependency revisions.
2888
+ * Source slot zero belongs to the collection being queried, not to the selection expression.
2889
+ */
2890
+ const compileSelection = (input, named, select, normalize, name) => {
2891
+ let projections;
2892
+ let compute;
2893
+ let initial;
2894
+ if (named) {
2895
+ const compiled = require_transform.compileProjectionDependencies(input, name);
2896
+ if (typeof select !== "function") throw new TypeError(`${name} requires a selection callback.`);
2897
+ projections = compiled.projections;
2898
+ const selectValues = select;
2899
+ compute = (sources) => selectValues(require_transform.snapshotProjectionValues(compiled.names, sources, 1));
2900
+ } else if (require_transform.isProjection(input)) {
2901
+ if (require_transform.outputKind(input) !== "value") throw new TypeError(`${name} selection must be a scalar projection.`);
2902
+ projections = [input];
2903
+ compute = (sources) => {
2904
+ const source = sources[1];
2905
+ if (source?.kind !== "value") throw new Error(`${name} resolved a non-scalar selection.`);
2906
+ return source.value;
2907
+ };
2908
+ } else {
2909
+ projections = [];
2910
+ initial = normalize(input);
2911
+ }
2912
+ return {
2913
+ projections,
2914
+ create: () => {
2915
+ let initialized = compute === void 0;
2916
+ let current = initial;
2917
+ const revisions = new Array(projections.length).fill(-1);
2918
+ return {
2919
+ get value() {
2920
+ if (!initialized) throw new Error(`${name} selection was not initialized.`);
2921
+ return current;
2922
+ },
2923
+ refresh(sources) {
2924
+ if (!compute) return false;
2925
+ let dirty = !initialized;
2926
+ for (let index = 0; index < revisions.length; index++) {
2927
+ const source = sources[index + 1];
2928
+ if (source.reset || source.revision !== revisions[index]) dirty = true;
2929
+ }
2930
+ if (!dirty) return false;
2931
+ const candidate = compute(sources);
2932
+ require_transform.assertSynchronous(candidate);
2933
+ const next = normalize(candidate, current);
2934
+ const changed = !initialized || !Object.is(current, next);
2935
+ current = next;
2936
+ initialized = true;
2937
+ for (let index = 0; index < revisions.length; index++) revisions[index] = sources[index + 1].revision;
2938
+ return changed;
2939
+ }
2940
+ };
2941
+ }
2942
+ };
2943
+ };
2944
+ const normalizeKey = (value) => {
2945
+ if (value !== void 0 && typeof value !== "string") throw new TypeError("derive.keyed.get key must be a string or undefined.");
2946
+ return value;
2947
+ };
2948
+ const normalizeOrderedKeys = (value, previous) => {
2949
+ if (!Array.isArray(value)) throw new TypeError("derive.keyed.subset ordered keys must be an array.");
2950
+ if (previous && require_transform.sameArray(previous.keys, value)) return previous;
2951
+ const requested = /* @__PURE__ */ new Set();
2952
+ for (const key of value) {
2953
+ if (typeof key !== "string") throw new TypeError("derive.keyed.subset keys must be strings.");
2954
+ if (requested.has(key)) throw new TypeError("derive.keyed.subset keys must not contain duplicate keys.");
2955
+ requested.add(key);
2956
+ }
2957
+ return {
2958
+ keys: require_transform.snapshotArray(value),
2959
+ requested
2960
+ };
2961
+ };
2962
+ function createKeyedGet(source, input, selectOrEquality, maybeEquality) {
2963
+ const name = "derive.keyed.get";
2964
+ require_transform.assertKeyedProjection(source, `${name} source`);
2965
+ const named = !require_transform.isProjection(input) && typeof input !== "string" && input !== void 0;
2966
+ const selection = compileSelection(input, named, named ? selectOrEquality : void 0, normalizeKey, name);
2967
+ const equality = named ? maybeEquality : selectOrEquality;
2968
+ if (equality !== void 0 && typeof equality !== "function") throw new TypeError(`${name} equality must be a function.`);
2969
+ const [projection] = require_transform.defineProcessor({
2970
+ dependencies: [source, ...selection.projections],
2971
+ outputs: [{
2972
+ kind: "value",
2973
+ equality: equality ?? Object.is
2974
+ }],
2975
+ name,
2976
+ create: () => {
2977
+ const selected = selection.create();
2978
+ return { evaluate: (evaluation) => {
2979
+ const collection = evaluation.sources[0];
2980
+ const output = evaluation.outputs[0];
2981
+ if (collection.kind !== "collection" || output.kind !== "value") throw new Error(`${name} resolved invalid input/output kinds.`);
2982
+ const keyChanged = selected.refresh(evaluation.sources);
2983
+ const key = selected.value;
2984
+ const change = collection.change;
2985
+ if (evaluation.reset || keyChanged || change?.kind === "reset" || key !== void 0 && change && require_transform.collectionChangeTouchesKey(change, key)) output.output.set(key === void 0 ? void 0 : collection.read.get(key));
2986
+ } };
2987
+ }
2988
+ });
2989
+ return projection;
2990
+ }
2991
+ function createKeyedSubset(source, input, selectOrderedKeys) {
2992
+ const name = "derive.keyed.subset";
2993
+ require_transform.assertKeyedProjection(source, `${name} source`);
2994
+ const selection = compileSelection(input, !require_transform.isProjection(input) && !Array.isArray(input), selectOrderedKeys, normalizeOrderedKeys, name);
2995
+ const [projection] = require_transform.defineProcessor({
2996
+ dependencies: [source, ...selection.projections],
2997
+ outputs: [{
2998
+ kind: "collection",
2999
+ equality: Object.is
3000
+ }],
3001
+ name,
3002
+ create: () => {
3003
+ const selected = selection.create();
3004
+ return { evaluate: (evaluation) => {
3005
+ const driver = evaluation.sources[0];
3006
+ const output = evaluation.outputs[0];
3007
+ if (driver.kind !== "collection" || output.kind !== "collection") throw new Error(`${name} requires collection input and output.`);
3008
+ const keysChanged = selected.refresh(evaluation.sources);
3009
+ const { keys, requested } = selected.value;
3010
+ const change = driver.change;
3011
+ if (evaluation.reset || change?.kind === "reset") {
3012
+ const present = keys.filter((key) => driver.read.has(key));
3013
+ for (const key of present) output.output.set(key, driver.read.get(key));
3014
+ output.output.order(present);
3015
+ return;
3016
+ }
3017
+ let orderChanged = keysChanged;
3018
+ if (keysChanged) {
3019
+ for (const key of output.previous.ids()) if (!requested.has(key) || !driver.read.has(key)) output.output.remove(key);
3020
+ for (const key of keys) if (!output.previous.has(key) && driver.read.has(key)) output.output.set(key, driver.read.get(key));
3021
+ } else if (change) {
3022
+ for (const entry of change.removed) {
3023
+ if (!requested.has(entry.key)) continue;
3024
+ output.output.remove(entry.key);
3025
+ orderChanged = true;
3026
+ }
3027
+ for (const entry of change.added) {
3028
+ if (!requested.has(entry.key)) continue;
3029
+ output.output.set(entry.key, entry.after);
3030
+ orderChanged = true;
3031
+ }
3032
+ }
3033
+ if (change) {
3034
+ for (const entry of change.updated) if (requested.has(entry.key) && (!keysChanged || output.previous.has(entry.key))) output.output.set(entry.key, entry.after);
3035
+ }
3036
+ if (orderChanged) output.output.order(keys.filter((key) => driver.read.has(key)));
3037
+ } };
3038
+ }
3039
+ });
3040
+ return projection;
3041
+ }
3042
+ //#endregion
2886
3043
  //#region core/src/projection/keyed/composition.ts
2887
3044
  const assertEquality = (equality, role) => {
2888
3045
  if (equality !== void 0 && typeof equality !== "function") throw new TypeError(`${role} equality must be a function.`);
@@ -3211,19 +3368,6 @@ function createKeyedSingleton(sourceOrDependencies, compute, equality) {
3211
3368
  }
3212
3369
  //#endregion
3213
3370
  //#region core/src/projection/derive/keyed.ts
3214
- const compileOrderedKeys = (value, role) => {
3215
- if (!Array.isArray(value)) throw new TypeError(`${role} must be an array of string keys.`);
3216
- const seen = /* @__PURE__ */ new Set();
3217
- for (const key of value) {
3218
- if (typeof key !== "string") throw new TypeError(`${role} must contain only string keys.`);
3219
- if (seen.has(key)) throw new TypeError(`${role} must not contain duplicate keys.`);
3220
- seen.add(key);
3221
- }
3222
- return {
3223
- keys: value,
3224
- requested: seen
3225
- };
3226
- };
3227
3371
  function createKeyedDerive(source, dependenciesOrSelect, selectOrEquality, maybeEquality) {
3228
3372
  const dependencySpecs = typeof dependenciesOrSelect === "function" ? void 0 : dependenciesOrSelect;
3229
3373
  const select = dependencySpecs === void 0 ? dependenciesOrSelect : selectOrEquality;
@@ -3328,44 +3472,6 @@ const createOrderedSnapshot = (source, name, make) => {
3328
3472
  };
3329
3473
  const createKeyedValues = (source) => createOrderedSnapshot(source, "derive.keyed.values", (_key, value) => value);
3330
3474
  const createKeyedEntries = (source) => createOrderedSnapshot(source, "derive.keyed.entries", (key, value) => Object.freeze([key, value]));
3331
- const createKeyedGet = (source, key, equality = Object.is) => {
3332
- require_transform.assertKeyedProjection(source, "derive.keyed.get source");
3333
- if (require_transform.outputKind(key) !== "value") throw new TypeError("derive.keyed.get key must be a scalar projection.");
3334
- const [projection] = require_transform.defineProcessor({
3335
- dependencies: [source, key],
3336
- outputs: [{
3337
- kind: "value",
3338
- equality
3339
- }],
3340
- create: () => {
3341
- let sourceRevision = -1;
3342
- let keyRevision = -1;
3343
- let selected;
3344
- return { evaluate: (evaluation) => {
3345
- const collection = evaluation.sources[0];
3346
- const selectedKey = evaluation.sources[1];
3347
- const output = evaluation.outputs[0];
3348
- if (collection.kind !== "collection" || selectedKey.kind !== "value" || output.kind !== "value") throw new Error("derive.keyed.get resolved invalid input/output kinds.");
3349
- let affected = evaluation.reset;
3350
- if (evaluation.reset || keyRevision !== selectedKey.revision) {
3351
- const candidate = selectedKey.value;
3352
- if (candidate !== void 0 && typeof candidate !== "string") throw new TypeError("derive.keyed.get key must resolve to a string or undefined.");
3353
- selected = candidate;
3354
- affected = true;
3355
- } else if (sourceRevision !== collection.revision) {
3356
- const change = collection.change;
3357
- if (!change || change.kind === "reset") affected = true;
3358
- else if (selected !== void 0) affected = require_transform.collectionChangeTouchesKey(change, selected);
3359
- }
3360
- if (affected) output.output.set(selected === void 0 ? void 0 : collection.read.get(selected));
3361
- sourceRevision = collection.revision;
3362
- keyRevision = selectedKey.revision;
3363
- } };
3364
- },
3365
- name: "derive.keyed.get"
3366
- });
3367
- return projection;
3368
- };
3369
3475
  function createKeyedFilter(source, dependenciesOrPredicate, maybePredicate) {
3370
3476
  const dependencySpecs = typeof dependenciesOrPredicate === "function" ? void 0 : dependenciesOrPredicate;
3371
3477
  const predicate = dependencySpecs === void 0 ? dependenciesOrPredicate : maybePredicate;
@@ -3400,101 +3506,6 @@ function createKeyedCompact(source, dependenciesOrSelect, selectOrEquality, mayb
3400
3506
  }
3401
3507
  });
3402
3508
  }
3403
- const createKeyedSubset = (source, orderedKeys) => {
3404
- require_transform.assertKeyedProjection(source, "Keyed subset source");
3405
- const keyProjection = require_transform.isProjection(orderedKeys) ? orderedKeys : void 0;
3406
- if (keyProjection && require_transform.outputKind(keyProjection) !== "value") throw new TypeError("Keyed subset ordered keys must be a value projection.");
3407
- const staticSelection = keyProjection ? void 0 : (() => {
3408
- const compiled = compileOrderedKeys(orderedKeys, "Keyed subset ordered keys");
3409
- return {
3410
- keys: Object.freeze([...compiled.keys]),
3411
- requested: compiled.requested
3412
- };
3413
- })();
3414
- const [projection] = require_transform.defineProcessor({
3415
- dependencies: keyProjection ? [source, keyProjection] : [source],
3416
- outputs: [{
3417
- kind: "collection",
3418
- equality: Object.is
3419
- }],
3420
- create: () => {
3421
- let keys = staticSelection?.keys;
3422
- let requested = staticSelection?.requested;
3423
- let keyRevision = -1;
3424
- return {
3425
- evaluate: (evaluation) => {
3426
- const driver = evaluation.sources[0];
3427
- const output = evaluation.outputs[0];
3428
- if (driver.kind !== "collection" || output.kind !== "collection") throw new Error("derive.keyed.subset requires collection input and output.");
3429
- let keysChanged = false;
3430
- if (keyProjection) {
3431
- const sourceContext = evaluation.sources[1];
3432
- if (sourceContext?.kind !== "value") throw new TypeError("Keyed subset ordered keys resolved to a collection.");
3433
- if (keys === void 0 || sourceContext.reset || keyRevision !== sourceContext.revision) {
3434
- const compiled = compileOrderedKeys(sourceContext.value, "Keyed subset ordered keys");
3435
- keys = compiled.keys;
3436
- requested = compiled.requested;
3437
- keyRevision = sourceContext.revision;
3438
- keysChanged = true;
3439
- }
3440
- }
3441
- if (!keys || !requested) throw new Error("Keyed subset keys were not initialized.");
3442
- const activeKeys = keys;
3443
- const activeRequested = requested;
3444
- const orderedPresentKeys = () => activeKeys.filter((key) => driver.read.has(key));
3445
- const rebuild = () => {
3446
- const nextKeys = orderedPresentKeys();
3447
- for (const key of nextKeys) output.output.set(key, driver.read.get(key));
3448
- output.output.order(nextKeys);
3449
- };
3450
- const reconcileKeys = () => {
3451
- const nextKeys = orderedPresentKeys();
3452
- for (const key of output.previous.ids()) if (!activeRequested.has(key) || !driver.read.has(key)) output.output.remove(key);
3453
- for (const key of nextKeys) if (!output.previous.has(key)) output.output.set(key, driver.read.get(key));
3454
- output.output.order(nextKeys);
3455
- };
3456
- const change = driver.change;
3457
- if (evaluation.reset || change?.kind === "reset") rebuild();
3458
- else if (keysChanged) {
3459
- reconcileKeys();
3460
- if (change) for (const entry of change.updated) {
3461
- const key = entry.key;
3462
- if (activeRequested.has(key) && output.previous.has(key)) output.output.set(key, entry.after);
3463
- }
3464
- } else if (change) {
3465
- let updateOrder = false;
3466
- for (const entry of change.removed) {
3467
- const key = entry.key;
3468
- if (!activeRequested.has(key)) continue;
3469
- output.output.remove(key);
3470
- updateOrder = true;
3471
- }
3472
- for (const entry of change.added) {
3473
- const key = entry.key;
3474
- if (!activeRequested.has(key)) continue;
3475
- output.output.set(key, entry.after);
3476
- updateOrder = true;
3477
- }
3478
- for (const entry of change.updated) {
3479
- const key = entry.key;
3480
- if (activeRequested.has(key)) output.output.set(key, entry.after);
3481
- }
3482
- if (updateOrder) output.output.order(orderedPresentKeys());
3483
- }
3484
- },
3485
- release: () => {
3486
- if (keyProjection) {
3487
- keys = void 0;
3488
- requested = void 0;
3489
- keyRevision = -1;
3490
- }
3491
- }
3492
- };
3493
- },
3494
- name: "derive.keyed.subset"
3495
- });
3496
- return projection;
3497
- };
3498
3509
  const normalizeGroups = (value) => {
3499
3510
  const groups = typeof value === "string" ? [value] : value;
3500
3511
  if (!Array.isArray(groups)) throw new TypeError("derive.keyed.groupBy selector must return a string or an array of strings.");
package/dist/index.d.cts CHANGED
@@ -33,6 +33,13 @@ declare function observe<S extends ObjectSchema<object>, P>(document: ReadonlyDo
33
33
  type ProjectionDependencies = Readonly<Record<string, Projection<unknown>>> & Partial<Record<keyof Projection<unknown>, never>>;
34
34
  type ProjectionValues<D extends ProjectionDependencies> = { readonly [K in keyof D]: D[K] extends Projection<infer T> ? T : never };
35
35
  //#endregion
36
+ //#region core/src/projection/keyed/selection.d.ts
37
+ type Equality$3<T> = (previous: T, next: T) => boolean;
38
+ declare function createKeyedGet<K extends string, V>(source: KeyedProjection<K, V>, key: NoInfer<K> | undefined | Projection<NoInfer<K> | undefined>, equality?: Equality$3<NoInfer<V> | undefined>): Projection<V | undefined>;
39
+ declare function createKeyedGet<K extends string, V, const D extends ProjectionDependencies>(source: KeyedProjection<K, V>, dependencies: D, selectKey: (values: ProjectionValues<D>) => Synchronous<NoInfer<K> | undefined>, equality?: Equality$3<NoInfer<V> | undefined>): Projection<V | undefined>;
40
+ declare function createKeyedSubset<K extends string, V>(source: KeyedProjection<K, V>, orderedKeys: Projection<readonly NoInfer<K>[]> | readonly NoInfer<K>[]): KeyedProjection<K, V>;
41
+ declare function createKeyedSubset<K extends string, V, const D extends ProjectionDependencies>(source: KeyedProjection<K, V>, dependencies: D, selectOrderedKeys: (values: ProjectionValues<D>) => Synchronous<readonly NoInfer<K>[]>): KeyedProjection<K, V>;
42
+ //#endregion
36
43
  //#region core/src/projection/keyed/entries.d.ts
37
44
  type KeyedEntries<K extends string, V> = readonly (readonly [K, V])[];
38
45
  //#endregion
@@ -72,7 +79,7 @@ declare const derive: typeof createValueDerive & {
72
79
  keys: <K extends string, V>(source: KeyedProjection<K, V>) => Projection<readonly K[]>;
73
80
  values: <K extends string, V>(source: KeyedProjection<K, V>) => Projection<readonly V[]>;
74
81
  entries: <K extends string, V>(source: KeyedProjection<K, V>) => Projection<readonly (readonly [K, V])[]>;
75
- get: <K extends string, V>(source: KeyedProjection<K, V>, key: Projection<NoInfer<K> | undefined>, equality?: (previous: V | undefined, next: V | undefined) => boolean) => Projection<V | undefined>;
82
+ get: typeof createKeyedGet;
76
83
  from: typeof createKeyedFrom;
77
84
  fromEntries: typeof createKeyedFromEntries;
78
85
  merge: typeof createKeyedMerge;
@@ -82,7 +89,7 @@ declare const derive: typeof createValueDerive & {
82
89
  };
83
90
  flatMap: typeof createKeyedFlatMap;
84
91
  singleton: typeof createKeyedSingleton;
85
- subset: <K extends string, V>(source: KeyedProjection<K, V>, orderedKeys: Projection<readonly K[]> | readonly K[]) => KeyedProjection<K, V>;
92
+ subset: typeof createKeyedSubset;
86
93
  filter: {
87
94
  <K extends string, V>(source: KeyedProjection<K, V>, predicate: (value: NoInfer<V>, key: NoInfer<K>) => Synchronous<boolean>): KeyedProjection<K, V>;
88
95
  <K extends string, V, const D extends object>(source: KeyedProjection<K, V>, dependencies: D & KeyedDependencies<NoInfer<K>, NoInfer<V>, D>, predicate: (value: NoInfer<V>, key: NoInfer<K>, dependencies: KeyedDependencyValues<D>) => Synchronous<boolean>): KeyedProjection<K, V>;
package/dist/index.d.ts CHANGED
@@ -33,6 +33,13 @@ declare function observe<S extends ObjectSchema<object>, P>(document: ReadonlyDo
33
33
  type ProjectionDependencies = Readonly<Record<string, Projection<unknown>>> & Partial<Record<keyof Projection<unknown>, never>>;
34
34
  type ProjectionValues<D extends ProjectionDependencies> = { readonly [K in keyof D]: D[K] extends Projection<infer T> ? T : never };
35
35
  //#endregion
36
+ //#region core/src/projection/keyed/selection.d.ts
37
+ type Equality$3<T> = (previous: T, next: T) => boolean;
38
+ declare function createKeyedGet<K extends string, V>(source: KeyedProjection<K, V>, key: NoInfer<K> | undefined | Projection<NoInfer<K> | undefined>, equality?: Equality$3<NoInfer<V> | undefined>): Projection<V | undefined>;
39
+ declare function createKeyedGet<K extends string, V, const D extends ProjectionDependencies>(source: KeyedProjection<K, V>, dependencies: D, selectKey: (values: ProjectionValues<D>) => Synchronous<NoInfer<K> | undefined>, equality?: Equality$3<NoInfer<V> | undefined>): Projection<V | undefined>;
40
+ declare function createKeyedSubset<K extends string, V>(source: KeyedProjection<K, V>, orderedKeys: Projection<readonly NoInfer<K>[]> | readonly NoInfer<K>[]): KeyedProjection<K, V>;
41
+ declare function createKeyedSubset<K extends string, V, const D extends ProjectionDependencies>(source: KeyedProjection<K, V>, dependencies: D, selectOrderedKeys: (values: ProjectionValues<D>) => Synchronous<readonly NoInfer<K>[]>): KeyedProjection<K, V>;
42
+ //#endregion
36
43
  //#region core/src/projection/keyed/entries.d.ts
37
44
  type KeyedEntries<K extends string, V> = readonly (readonly [K, V])[];
38
45
  //#endregion
@@ -72,7 +79,7 @@ declare const derive: typeof createValueDerive & {
72
79
  keys: <K extends string, V>(source: KeyedProjection<K, V>) => Projection<readonly K[]>;
73
80
  values: <K extends string, V>(source: KeyedProjection<K, V>) => Projection<readonly V[]>;
74
81
  entries: <K extends string, V>(source: KeyedProjection<K, V>) => Projection<readonly (readonly [K, V])[]>;
75
- get: <K extends string, V>(source: KeyedProjection<K, V>, key: Projection<NoInfer<K> | undefined>, equality?: (previous: V | undefined, next: V | undefined) => boolean) => Projection<V | undefined>;
82
+ get: typeof createKeyedGet;
76
83
  from: typeof createKeyedFrom;
77
84
  fromEntries: typeof createKeyedFromEntries;
78
85
  merge: typeof createKeyedMerge;
@@ -82,7 +89,7 @@ declare const derive: typeof createValueDerive & {
82
89
  };
83
90
  flatMap: typeof createKeyedFlatMap;
84
91
  singleton: typeof createKeyedSingleton;
85
- subset: <K extends string, V>(source: KeyedProjection<K, V>, orderedKeys: Projection<readonly K[]> | readonly K[]) => KeyedProjection<K, V>;
92
+ subset: typeof createKeyedSubset;
86
93
  filter: {
87
94
  <K extends string, V>(source: KeyedProjection<K, V>, predicate: (value: NoInfer<V>, key: NoInfer<K>) => Synchronous<boolean>): KeyedProjection<K, V>;
88
95
  <K extends string, V, const D extends object>(source: KeyedProjection<K, V>, dependencies: D & KeyedDependencies<NoInfer<K>, NoInfer<V>, D>, predicate: (value: NoInfer<V>, key: NoInfer<K>, dependencies: KeyedDependencyValues<D>) => Synchronous<boolean>): KeyedProjection<K, V>;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { A as ownProjections, C as snapshotProjectionValues, D as defineProcessor, E as snapshotCollectionView, M as projectionRef, N as profile, O as defineSource, S as compileProjectionDependencies, T as mapRead, _ as assertSynchronous, a as collectionChangeTouchesKey, b as ProjectionError, c as assertKeyedProjection, d as outputKind, f as createKeyRelation, g as assertScope, h as snapshotArray, i as collectionChangeIntersects, j as producerOf, k as isProjection, l as compileKeyedDependencies, m as sameArray, n as keyedAbsent, o as collectionHasStructuralChange, p as iterableMatchesArray, s as createCollectionChange, t as createKeyedTransform, u as createKeyedDependencyRuntime, v as createScheduler, w as collectionView, x as collectKeyedInvalidation, y as ProjectionDisposedError } from "./transform-3mj0eStL.js";
1
+ import { A as ownProjections, C as snapshotProjectionValues, D as defineProcessor, E as snapshotCollectionView, M as projectionRef, N as profile, O as defineSource, S as compileProjectionDependencies, T as mapRead, _ as assertSynchronous, a as collectionChangeTouchesKey, b as ProjectionError, c as assertKeyedProjection, d as outputKind, f as createKeyRelation, g as assertScope, h as snapshotArray, i as collectionChangeIntersects, j as producerOf, k as isProjection, l as compileKeyedDependencies, m as sameArray, n as keyedAbsent, o as collectionHasStructuralChange, p as iterableMatchesArray, s as createCollectionChange, t as createKeyedTransform, u as createKeyedDependencyRuntime, v as createScheduler, w as collectionView, x as collectKeyedInvalidation, y as ProjectionDisposedError } from "./transform--WCn6_v0.js";
2
2
  import { n as isPlainObject, r as isRecord, t as installOwn } from "./record-B2yRhYsc.js";
3
3
  import { c as AddressIndex, d as invalidValue, f as children, g as validate, h as parent, i as contextOf, l as MutationRejected, m as is, o as decodeChanges, p as contains$1, r as bindContext, s as sealChanges, t as assertRuntimeWritable, u as fail } from "./driver-B7RR-VfX.js";
4
4
  //#region core/src/schema/model.ts
@@ -2882,6 +2882,163 @@ function createKeyedFlatMap(source, dependenciesOrSelect, selectOrEquality, mayb
2882
2882
  return projection;
2883
2883
  }
2884
2884
  //#endregion
2885
+ //#region core/src/projection/keyed/selection.ts
2886
+ /** Compiles selection inputs; runtime instances retain only the request and its dependency revisions.
2887
+ * Source slot zero belongs to the collection being queried, not to the selection expression.
2888
+ */
2889
+ const compileSelection = (input, named, select, normalize, name) => {
2890
+ let projections;
2891
+ let compute;
2892
+ let initial;
2893
+ if (named) {
2894
+ const compiled = compileProjectionDependencies(input, name);
2895
+ if (typeof select !== "function") throw new TypeError(`${name} requires a selection callback.`);
2896
+ projections = compiled.projections;
2897
+ const selectValues = select;
2898
+ compute = (sources) => selectValues(snapshotProjectionValues(compiled.names, sources, 1));
2899
+ } else if (isProjection(input)) {
2900
+ if (outputKind(input) !== "value") throw new TypeError(`${name} selection must be a scalar projection.`);
2901
+ projections = [input];
2902
+ compute = (sources) => {
2903
+ const source = sources[1];
2904
+ if (source?.kind !== "value") throw new Error(`${name} resolved a non-scalar selection.`);
2905
+ return source.value;
2906
+ };
2907
+ } else {
2908
+ projections = [];
2909
+ initial = normalize(input);
2910
+ }
2911
+ return {
2912
+ projections,
2913
+ create: () => {
2914
+ let initialized = compute === void 0;
2915
+ let current = initial;
2916
+ const revisions = new Array(projections.length).fill(-1);
2917
+ return {
2918
+ get value() {
2919
+ if (!initialized) throw new Error(`${name} selection was not initialized.`);
2920
+ return current;
2921
+ },
2922
+ refresh(sources) {
2923
+ if (!compute) return false;
2924
+ let dirty = !initialized;
2925
+ for (let index = 0; index < revisions.length; index++) {
2926
+ const source = sources[index + 1];
2927
+ if (source.reset || source.revision !== revisions[index]) dirty = true;
2928
+ }
2929
+ if (!dirty) return false;
2930
+ const candidate = compute(sources);
2931
+ assertSynchronous(candidate);
2932
+ const next = normalize(candidate, current);
2933
+ const changed = !initialized || !Object.is(current, next);
2934
+ current = next;
2935
+ initialized = true;
2936
+ for (let index = 0; index < revisions.length; index++) revisions[index] = sources[index + 1].revision;
2937
+ return changed;
2938
+ }
2939
+ };
2940
+ }
2941
+ };
2942
+ };
2943
+ const normalizeKey = (value) => {
2944
+ if (value !== void 0 && typeof value !== "string") throw new TypeError("derive.keyed.get key must be a string or undefined.");
2945
+ return value;
2946
+ };
2947
+ const normalizeOrderedKeys = (value, previous) => {
2948
+ if (!Array.isArray(value)) throw new TypeError("derive.keyed.subset ordered keys must be an array.");
2949
+ if (previous && sameArray(previous.keys, value)) return previous;
2950
+ const requested = /* @__PURE__ */ new Set();
2951
+ for (const key of value) {
2952
+ if (typeof key !== "string") throw new TypeError("derive.keyed.subset keys must be strings.");
2953
+ if (requested.has(key)) throw new TypeError("derive.keyed.subset keys must not contain duplicate keys.");
2954
+ requested.add(key);
2955
+ }
2956
+ return {
2957
+ keys: snapshotArray(value),
2958
+ requested
2959
+ };
2960
+ };
2961
+ function createKeyedGet(source, input, selectOrEquality, maybeEquality) {
2962
+ const name = "derive.keyed.get";
2963
+ assertKeyedProjection(source, `${name} source`);
2964
+ const named = !isProjection(input) && typeof input !== "string" && input !== void 0;
2965
+ const selection = compileSelection(input, named, named ? selectOrEquality : void 0, normalizeKey, name);
2966
+ const equality = named ? maybeEquality : selectOrEquality;
2967
+ if (equality !== void 0 && typeof equality !== "function") throw new TypeError(`${name} equality must be a function.`);
2968
+ const [projection] = defineProcessor({
2969
+ dependencies: [source, ...selection.projections],
2970
+ outputs: [{
2971
+ kind: "value",
2972
+ equality: equality ?? Object.is
2973
+ }],
2974
+ name,
2975
+ create: () => {
2976
+ const selected = selection.create();
2977
+ return { evaluate: (evaluation) => {
2978
+ const collection = evaluation.sources[0];
2979
+ const output = evaluation.outputs[0];
2980
+ if (collection.kind !== "collection" || output.kind !== "value") throw new Error(`${name} resolved invalid input/output kinds.`);
2981
+ const keyChanged = selected.refresh(evaluation.sources);
2982
+ const key = selected.value;
2983
+ const change = collection.change;
2984
+ if (evaluation.reset || keyChanged || change?.kind === "reset" || key !== void 0 && change && collectionChangeTouchesKey(change, key)) output.output.set(key === void 0 ? void 0 : collection.read.get(key));
2985
+ } };
2986
+ }
2987
+ });
2988
+ return projection;
2989
+ }
2990
+ function createKeyedSubset(source, input, selectOrderedKeys) {
2991
+ const name = "derive.keyed.subset";
2992
+ assertKeyedProjection(source, `${name} source`);
2993
+ const selection = compileSelection(input, !isProjection(input) && !Array.isArray(input), selectOrderedKeys, normalizeOrderedKeys, name);
2994
+ const [projection] = defineProcessor({
2995
+ dependencies: [source, ...selection.projections],
2996
+ outputs: [{
2997
+ kind: "collection",
2998
+ equality: Object.is
2999
+ }],
3000
+ name,
3001
+ create: () => {
3002
+ const selected = selection.create();
3003
+ return { evaluate: (evaluation) => {
3004
+ const driver = evaluation.sources[0];
3005
+ const output = evaluation.outputs[0];
3006
+ if (driver.kind !== "collection" || output.kind !== "collection") throw new Error(`${name} requires collection input and output.`);
3007
+ const keysChanged = selected.refresh(evaluation.sources);
3008
+ const { keys, requested } = selected.value;
3009
+ const change = driver.change;
3010
+ if (evaluation.reset || change?.kind === "reset") {
3011
+ const present = keys.filter((key) => driver.read.has(key));
3012
+ for (const key of present) output.output.set(key, driver.read.get(key));
3013
+ output.output.order(present);
3014
+ return;
3015
+ }
3016
+ let orderChanged = keysChanged;
3017
+ if (keysChanged) {
3018
+ for (const key of output.previous.ids()) if (!requested.has(key) || !driver.read.has(key)) output.output.remove(key);
3019
+ for (const key of keys) if (!output.previous.has(key) && driver.read.has(key)) output.output.set(key, driver.read.get(key));
3020
+ } else if (change) {
3021
+ for (const entry of change.removed) {
3022
+ if (!requested.has(entry.key)) continue;
3023
+ output.output.remove(entry.key);
3024
+ orderChanged = true;
3025
+ }
3026
+ for (const entry of change.added) {
3027
+ if (!requested.has(entry.key)) continue;
3028
+ output.output.set(entry.key, entry.after);
3029
+ orderChanged = true;
3030
+ }
3031
+ }
3032
+ if (change) {
3033
+ for (const entry of change.updated) if (requested.has(entry.key) && (!keysChanged || output.previous.has(entry.key))) output.output.set(entry.key, entry.after);
3034
+ }
3035
+ if (orderChanged) output.output.order(keys.filter((key) => driver.read.has(key)));
3036
+ } };
3037
+ }
3038
+ });
3039
+ return projection;
3040
+ }
3041
+ //#endregion
2885
3042
  //#region core/src/projection/keyed/composition.ts
2886
3043
  const assertEquality = (equality, role) => {
2887
3044
  if (equality !== void 0 && typeof equality !== "function") throw new TypeError(`${role} equality must be a function.`);
@@ -3210,19 +3367,6 @@ function createKeyedSingleton(sourceOrDependencies, compute, equality) {
3210
3367
  }
3211
3368
  //#endregion
3212
3369
  //#region core/src/projection/derive/keyed.ts
3213
- const compileOrderedKeys = (value, role) => {
3214
- if (!Array.isArray(value)) throw new TypeError(`${role} must be an array of string keys.`);
3215
- const seen = /* @__PURE__ */ new Set();
3216
- for (const key of value) {
3217
- if (typeof key !== "string") throw new TypeError(`${role} must contain only string keys.`);
3218
- if (seen.has(key)) throw new TypeError(`${role} must not contain duplicate keys.`);
3219
- seen.add(key);
3220
- }
3221
- return {
3222
- keys: value,
3223
- requested: seen
3224
- };
3225
- };
3226
3370
  function createKeyedDerive(source, dependenciesOrSelect, selectOrEquality, maybeEquality) {
3227
3371
  const dependencySpecs = typeof dependenciesOrSelect === "function" ? void 0 : dependenciesOrSelect;
3228
3372
  const select = dependencySpecs === void 0 ? dependenciesOrSelect : selectOrEquality;
@@ -3327,44 +3471,6 @@ const createOrderedSnapshot = (source, name, make) => {
3327
3471
  };
3328
3472
  const createKeyedValues = (source) => createOrderedSnapshot(source, "derive.keyed.values", (_key, value) => value);
3329
3473
  const createKeyedEntries = (source) => createOrderedSnapshot(source, "derive.keyed.entries", (key, value) => Object.freeze([key, value]));
3330
- const createKeyedGet = (source, key, equality = Object.is) => {
3331
- assertKeyedProjection(source, "derive.keyed.get source");
3332
- if (outputKind(key) !== "value") throw new TypeError("derive.keyed.get key must be a scalar projection.");
3333
- const [projection] = defineProcessor({
3334
- dependencies: [source, key],
3335
- outputs: [{
3336
- kind: "value",
3337
- equality
3338
- }],
3339
- create: () => {
3340
- let sourceRevision = -1;
3341
- let keyRevision = -1;
3342
- let selected;
3343
- return { evaluate: (evaluation) => {
3344
- const collection = evaluation.sources[0];
3345
- const selectedKey = evaluation.sources[1];
3346
- const output = evaluation.outputs[0];
3347
- if (collection.kind !== "collection" || selectedKey.kind !== "value" || output.kind !== "value") throw new Error("derive.keyed.get resolved invalid input/output kinds.");
3348
- let affected = evaluation.reset;
3349
- if (evaluation.reset || keyRevision !== selectedKey.revision) {
3350
- const candidate = selectedKey.value;
3351
- if (candidate !== void 0 && typeof candidate !== "string") throw new TypeError("derive.keyed.get key must resolve to a string or undefined.");
3352
- selected = candidate;
3353
- affected = true;
3354
- } else if (sourceRevision !== collection.revision) {
3355
- const change = collection.change;
3356
- if (!change || change.kind === "reset") affected = true;
3357
- else if (selected !== void 0) affected = collectionChangeTouchesKey(change, selected);
3358
- }
3359
- if (affected) output.output.set(selected === void 0 ? void 0 : collection.read.get(selected));
3360
- sourceRevision = collection.revision;
3361
- keyRevision = selectedKey.revision;
3362
- } };
3363
- },
3364
- name: "derive.keyed.get"
3365
- });
3366
- return projection;
3367
- };
3368
3474
  function createKeyedFilter(source, dependenciesOrPredicate, maybePredicate) {
3369
3475
  const dependencySpecs = typeof dependenciesOrPredicate === "function" ? void 0 : dependenciesOrPredicate;
3370
3476
  const predicate = dependencySpecs === void 0 ? dependenciesOrPredicate : maybePredicate;
@@ -3399,101 +3505,6 @@ function createKeyedCompact(source, dependenciesOrSelect, selectOrEquality, mayb
3399
3505
  }
3400
3506
  });
3401
3507
  }
3402
- const createKeyedSubset = (source, orderedKeys) => {
3403
- assertKeyedProjection(source, "Keyed subset source");
3404
- const keyProjection = isProjection(orderedKeys) ? orderedKeys : void 0;
3405
- if (keyProjection && outputKind(keyProjection) !== "value") throw new TypeError("Keyed subset ordered keys must be a value projection.");
3406
- const staticSelection = keyProjection ? void 0 : (() => {
3407
- const compiled = compileOrderedKeys(orderedKeys, "Keyed subset ordered keys");
3408
- return {
3409
- keys: Object.freeze([...compiled.keys]),
3410
- requested: compiled.requested
3411
- };
3412
- })();
3413
- const [projection] = defineProcessor({
3414
- dependencies: keyProjection ? [source, keyProjection] : [source],
3415
- outputs: [{
3416
- kind: "collection",
3417
- equality: Object.is
3418
- }],
3419
- create: () => {
3420
- let keys = staticSelection?.keys;
3421
- let requested = staticSelection?.requested;
3422
- let keyRevision = -1;
3423
- return {
3424
- evaluate: (evaluation) => {
3425
- const driver = evaluation.sources[0];
3426
- const output = evaluation.outputs[0];
3427
- if (driver.kind !== "collection" || output.kind !== "collection") throw new Error("derive.keyed.subset requires collection input and output.");
3428
- let keysChanged = false;
3429
- if (keyProjection) {
3430
- const sourceContext = evaluation.sources[1];
3431
- if (sourceContext?.kind !== "value") throw new TypeError("Keyed subset ordered keys resolved to a collection.");
3432
- if (keys === void 0 || sourceContext.reset || keyRevision !== sourceContext.revision) {
3433
- const compiled = compileOrderedKeys(sourceContext.value, "Keyed subset ordered keys");
3434
- keys = compiled.keys;
3435
- requested = compiled.requested;
3436
- keyRevision = sourceContext.revision;
3437
- keysChanged = true;
3438
- }
3439
- }
3440
- if (!keys || !requested) throw new Error("Keyed subset keys were not initialized.");
3441
- const activeKeys = keys;
3442
- const activeRequested = requested;
3443
- const orderedPresentKeys = () => activeKeys.filter((key) => driver.read.has(key));
3444
- const rebuild = () => {
3445
- const nextKeys = orderedPresentKeys();
3446
- for (const key of nextKeys) output.output.set(key, driver.read.get(key));
3447
- output.output.order(nextKeys);
3448
- };
3449
- const reconcileKeys = () => {
3450
- const nextKeys = orderedPresentKeys();
3451
- for (const key of output.previous.ids()) if (!activeRequested.has(key) || !driver.read.has(key)) output.output.remove(key);
3452
- for (const key of nextKeys) if (!output.previous.has(key)) output.output.set(key, driver.read.get(key));
3453
- output.output.order(nextKeys);
3454
- };
3455
- const change = driver.change;
3456
- if (evaluation.reset || change?.kind === "reset") rebuild();
3457
- else if (keysChanged) {
3458
- reconcileKeys();
3459
- if (change) for (const entry of change.updated) {
3460
- const key = entry.key;
3461
- if (activeRequested.has(key) && output.previous.has(key)) output.output.set(key, entry.after);
3462
- }
3463
- } else if (change) {
3464
- let updateOrder = false;
3465
- for (const entry of change.removed) {
3466
- const key = entry.key;
3467
- if (!activeRequested.has(key)) continue;
3468
- output.output.remove(key);
3469
- updateOrder = true;
3470
- }
3471
- for (const entry of change.added) {
3472
- const key = entry.key;
3473
- if (!activeRequested.has(key)) continue;
3474
- output.output.set(key, entry.after);
3475
- updateOrder = true;
3476
- }
3477
- for (const entry of change.updated) {
3478
- const key = entry.key;
3479
- if (activeRequested.has(key)) output.output.set(key, entry.after);
3480
- }
3481
- if (updateOrder) output.output.order(orderedPresentKeys());
3482
- }
3483
- },
3484
- release: () => {
3485
- if (keyProjection) {
3486
- keys = void 0;
3487
- requested = void 0;
3488
- keyRevision = -1;
3489
- }
3490
- }
3491
- };
3492
- },
3493
- name: "derive.keyed.subset"
3494
- });
3495
- return projection;
3496
- };
3497
3508
  const normalizeGroups = (value) => {
3498
3509
  const groups = typeof value === "string" ? [value] : value;
3499
3510
  if (!Array.isArray(groups)) throw new TypeError("derive.keyed.groupBy selector must return a string or an array of strings.");
@@ -1,5 +1,6 @@
1
1
  import { n as isPlainObject } from "./record-B2yRhYsc.js";
2
2
  const profile = {
3
+ keyRelation: (key, amount = 1) => {},
3
4
  projection: (key, amount = 1) => {},
4
5
  recorder: (key, amount = 1) => {},
5
6
  access: (key) => {},
@@ -186,9 +187,9 @@ const readNamedDependencyEntries = (dependencies, label) => {
186
187
  /** Creates the durable dependency value exposed to pure derive callbacks. */
187
188
  const snapshotDependencyValue = (source) => source.kind === "value" ? source.value : snapshotCollectionView(source.read);
188
189
  /** Builds the same readonly named value boundary for scalar and keyed pure computations. */
189
- const snapshotProjectionValues = (names, sources) => {
190
+ const snapshotProjectionValues = (names, sources, offset = 0) => {
190
191
  const values = Object.create(null);
191
- for (let index = 0; index < names.length; index++) values[names[index]] = snapshotDependencyValue(sources[index]);
192
+ for (let index = 0; index < names.length; index++) values[names[index]] = snapshotDependencyValue(sources[offset + index]);
192
193
  return Object.freeze(values);
193
194
  };
194
195
  /** Compiles the shared named Projection dependency declaration contract. */
@@ -661,57 +662,95 @@ const iterableMatchesArray = (values, expected) => {
661
662
  };
662
663
  //#endregion
663
664
  //#region core/src/projection/keyed/relation.ts
664
- /** Runtime-local domain-neutral many-to-many keyed relation. */
665
+ /** Runtime-local many-to-many relation. Callers validate unique string keys before replacing.
666
+ * Forward order is meaningful; reverse sets and map iteration carry no formal order.
667
+ */
665
668
  const createKeyRelation = () => {
666
669
  const forward = /* @__PURE__ */ new Map();
667
670
  const reverse = /* @__PURE__ */ new Map();
668
671
  const detach = (left, right) => {
669
- for (const key of right) {
670
- const dependents = reverse.get(key);
671
- dependents?.delete(left);
672
- if (dependents?.size === 0) reverse.delete(key);
672
+ const dependents = reverse.get(right);
673
+ if (!dependents?.delete(left)) return;
674
+ profile.keyRelation("detachedEdges");
675
+ if (dependents.size === 0) {
676
+ reverse.delete(right);
677
+ profile.keyRelation("deletedBuckets");
673
678
  }
674
679
  };
675
680
  const attach = (left, right) => {
676
- for (const key of right) {
677
- const dependents = reverse.get(key);
678
- if (dependents) dependents.add(left);
679
- else reverse.set(key, new Set([left]));
681
+ const dependents = reverse.get(right);
682
+ if (dependents) {
683
+ if (dependents.has(left)) return false;
684
+ dependents.add(left);
685
+ } else {
686
+ reverse.set(right, new Set([left]));
687
+ profile.keyRelation("createdBuckets");
680
688
  }
689
+ profile.keyRelation("attachedEdges");
690
+ return true;
681
691
  };
682
692
  return {
683
693
  replaceOne(left, right) {
684
694
  const previous = forward.get(left);
685
695
  if (right === void 0) {
686
696
  if (!previous) return false;
687
- detach(left, previous);
697
+ for (const key of previous) detach(left, key);
688
698
  forward.delete(left);
689
699
  return true;
690
700
  }
691
701
  if (previous?.length === 1 && previous[0] === right) return false;
692
- if (previous) detach(left, previous);
693
702
  const next = Object.freeze([right]);
703
+ if (previous) {
704
+ for (const key of previous) if (key !== right) detach(left, key);
705
+ }
706
+ attach(left, right);
694
707
  forward.set(left, next);
695
- attach(left, next);
696
708
  return true;
697
709
  },
698
710
  replace(left, right) {
699
711
  const previous = forward.get(left);
700
- if (previous ? sameArray(previous, right) : right.length === 0) return false;
701
- if (previous) detach(left, previous);
712
+ if (!previous) {
713
+ if (!right.length) return false;
714
+ const next = snapshotArray(right);
715
+ for (const key of next) attach(left, key);
716
+ forward.set(left, next);
717
+ return true;
718
+ }
719
+ if (previous === right) return false;
702
720
  if (!right.length) {
721
+ for (const key of previous) detach(left, key);
703
722
  forward.delete(left);
704
723
  return true;
705
724
  }
725
+ let start = 0;
726
+ while (start < previous.length && start < right.length && previous[start] === right[start]) start++;
727
+ if (start === previous.length && start === right.length) return false;
706
728
  const next = snapshotArray(right);
729
+ let oldEnd = previous.length;
730
+ let nextEnd = next.length;
731
+ while (oldEnd > start && nextEnd > start && previous[oldEnd - 1] === next[nextEnd - 1]) {
732
+ oldEnd--;
733
+ nextEnd--;
734
+ }
735
+ let retained = 0;
736
+ for (let index = start; index < nextEnd; index++) if (!attach(left, next[index])) retained++;
737
+ if (retained === 0) for (let index = start; index < oldEnd; index++) detach(left, previous[index]);
738
+ else if (retained < oldEnd - start) if (nextEnd - start === 1) {
739
+ for (let index = start; index < oldEnd; index++) if (previous[index] !== next[start]) detach(left, previous[index]);
740
+ } else {
741
+ const requested = /* @__PURE__ */ new Set();
742
+ for (let index = start; index < nextEnd; index++) requested.add(next[index]);
743
+ profile.keyRelation("diffSets");
744
+ profile.keyRelation("diffKeys", requested.size);
745
+ for (let index = start; index < oldEnd; index++) if (!requested.has(previous[index])) detach(left, previous[index]);
746
+ }
707
747
  forward.set(left, next);
708
- attach(left, next);
709
748
  return true;
710
749
  },
711
750
  delete(left) {
712
751
  const previous = forward.get(left);
713
752
  if (!previous) return false;
714
- detach(left, previous);
753
+ for (const key of previous) detach(left, key);
715
754
  forward.delete(left);
716
755
  return true;
717
756
  },
@@ -1,5 +1,6 @@
1
1
  const require_record = require("./record-ChYUi8PP.cjs");
2
2
  const profile = {
3
+ keyRelation: (key, amount = 1) => {},
3
4
  projection: (key, amount = 1) => {},
4
5
  recorder: (key, amount = 1) => {},
5
6
  access: (key) => {},
@@ -186,9 +187,9 @@ const readNamedDependencyEntries = (dependencies, label) => {
186
187
  /** Creates the durable dependency value exposed to pure derive callbacks. */
187
188
  const snapshotDependencyValue = (source) => source.kind === "value" ? source.value : snapshotCollectionView(source.read);
188
189
  /** Builds the same readonly named value boundary for scalar and keyed pure computations. */
189
- const snapshotProjectionValues = (names, sources) => {
190
+ const snapshotProjectionValues = (names, sources, offset = 0) => {
190
191
  const values = Object.create(null);
191
- for (let index = 0; index < names.length; index++) values[names[index]] = snapshotDependencyValue(sources[index]);
192
+ for (let index = 0; index < names.length; index++) values[names[index]] = snapshotDependencyValue(sources[offset + index]);
192
193
  return Object.freeze(values);
193
194
  };
194
195
  /** Compiles the shared named Projection dependency declaration contract. */
@@ -661,57 +662,95 @@ const iterableMatchesArray = (values, expected) => {
661
662
  };
662
663
  //#endregion
663
664
  //#region core/src/projection/keyed/relation.ts
664
- /** Runtime-local domain-neutral many-to-many keyed relation. */
665
+ /** Runtime-local many-to-many relation. Callers validate unique string keys before replacing.
666
+ * Forward order is meaningful; reverse sets and map iteration carry no formal order.
667
+ */
665
668
  const createKeyRelation = () => {
666
669
  const forward = /* @__PURE__ */ new Map();
667
670
  const reverse = /* @__PURE__ */ new Map();
668
671
  const detach = (left, right) => {
669
- for (const key of right) {
670
- const dependents = reverse.get(key);
671
- dependents?.delete(left);
672
- if (dependents?.size === 0) reverse.delete(key);
672
+ const dependents = reverse.get(right);
673
+ if (!dependents?.delete(left)) return;
674
+ profile.keyRelation("detachedEdges");
675
+ if (dependents.size === 0) {
676
+ reverse.delete(right);
677
+ profile.keyRelation("deletedBuckets");
673
678
  }
674
679
  };
675
680
  const attach = (left, right) => {
676
- for (const key of right) {
677
- const dependents = reverse.get(key);
678
- if (dependents) dependents.add(left);
679
- else reverse.set(key, new Set([left]));
681
+ const dependents = reverse.get(right);
682
+ if (dependents) {
683
+ if (dependents.has(left)) return false;
684
+ dependents.add(left);
685
+ } else {
686
+ reverse.set(right, new Set([left]));
687
+ profile.keyRelation("createdBuckets");
680
688
  }
689
+ profile.keyRelation("attachedEdges");
690
+ return true;
681
691
  };
682
692
  return {
683
693
  replaceOne(left, right) {
684
694
  const previous = forward.get(left);
685
695
  if (right === void 0) {
686
696
  if (!previous) return false;
687
- detach(left, previous);
697
+ for (const key of previous) detach(left, key);
688
698
  forward.delete(left);
689
699
  return true;
690
700
  }
691
701
  if (previous?.length === 1 && previous[0] === right) return false;
692
- if (previous) detach(left, previous);
693
702
  const next = Object.freeze([right]);
703
+ if (previous) {
704
+ for (const key of previous) if (key !== right) detach(left, key);
705
+ }
706
+ attach(left, right);
694
707
  forward.set(left, next);
695
- attach(left, next);
696
708
  return true;
697
709
  },
698
710
  replace(left, right) {
699
711
  const previous = forward.get(left);
700
- if (previous ? sameArray(previous, right) : right.length === 0) return false;
701
- if (previous) detach(left, previous);
712
+ if (!previous) {
713
+ if (!right.length) return false;
714
+ const next = snapshotArray(right);
715
+ for (const key of next) attach(left, key);
716
+ forward.set(left, next);
717
+ return true;
718
+ }
719
+ if (previous === right) return false;
702
720
  if (!right.length) {
721
+ for (const key of previous) detach(left, key);
703
722
  forward.delete(left);
704
723
  return true;
705
724
  }
725
+ let start = 0;
726
+ while (start < previous.length && start < right.length && previous[start] === right[start]) start++;
727
+ if (start === previous.length && start === right.length) return false;
706
728
  const next = snapshotArray(right);
729
+ let oldEnd = previous.length;
730
+ let nextEnd = next.length;
731
+ while (oldEnd > start && nextEnd > start && previous[oldEnd - 1] === next[nextEnd - 1]) {
732
+ oldEnd--;
733
+ nextEnd--;
734
+ }
735
+ let retained = 0;
736
+ for (let index = start; index < nextEnd; index++) if (!attach(left, next[index])) retained++;
737
+ if (retained === 0) for (let index = start; index < oldEnd; index++) detach(left, previous[index]);
738
+ else if (retained < oldEnd - start) if (nextEnd - start === 1) {
739
+ for (let index = start; index < oldEnd; index++) if (previous[index] !== next[start]) detach(left, previous[index]);
740
+ } else {
741
+ const requested = /* @__PURE__ */ new Set();
742
+ for (let index = start; index < nextEnd; index++) requested.add(next[index]);
743
+ profile.keyRelation("diffSets");
744
+ profile.keyRelation("diffKeys", requested.size);
745
+ for (let index = start; index < oldEnd; index++) if (!requested.has(previous[index])) detach(left, previous[index]);
746
+ }
707
747
  forward.set(left, next);
708
- attach(left, next);
709
748
  return true;
710
749
  },
711
750
  delete(left) {
712
751
  const previous = forward.get(left);
713
752
  if (!previous) return false;
714
- detach(left, previous);
753
+ for (const key of previous) detach(left, key);
715
754
  forward.delete(left);
716
755
  return true;
717
756
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doxum",
3
- "version": "0.1.38",
3
+ "version": "0.1.40",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Doxum is a typed runtime for complex mutable documents.",
@@ -45,7 +45,7 @@ Follow these choices unless the requirement says otherwise:
45
45
  4. Use `observe` to bring a document/readable/external source into the projection graph.
46
46
  5. Use named-object `derive` for pure aggregate values.
47
47
  6. Use `derive.keyed.from` to turn an ordered scalar/static collection into a keyed projection. Use `derive.keyed.singleton({ inputs }, computeEntry)` for zero or one computed member (`[key, value]` or `undefined`), and `derive.keyed.fromEntries({ inputs }, compute)` for a complete keyed result of arbitrary size; use `flatMap` when individual keyed parents must recompute independently. Use `derive.keyed.merge` to compose multiple keyed projections; for base + sparse overrides, use `{ conflict: 'last' }`.
48
- 7. Use `derive.keyed` when one keyed source owns output membership/order. Prefer its built-in `keys`, `values`, `entries`, `get`, `from`, `fromEntries`, `merge`, `subset`, `filter`, `compact`, `groupBy`, `flatMap`, and `singleton` primitives over hand-written incremental collection patches.
48
+ 7. Use `derive.keyed` when one keyed source owns output membership/order. Use `get(source, dependencies, selectKey)` or `subset(source, dependencies, selectOrderedKeys)` when named projections determine which existing members to read; static keys/key arrays and existing scalar selection projections also work. Prefer the keyed family's built-in primitives over hand-written incremental collection patches.
49
49
  8. Express per-output-key joins through `{ source }` for same-key lookup, `{ source, key }` for one mapped key, or `{ source, keys }` for several mapped keys. Let the Runtime own binding and reverse invalidation; do not maintain an application `Map` just to route dependency changes.
50
50
  9. Use `runtime.items(keyedProjection)` or `scope.items(...)` for stable per-membership item `Readable`s.
51
51
  10. Use `incremental.keyed` for independent retained state per driver key. Use `incremental.collection` or `incremental.group` only when direct incremental output patching or shared retained state is required.
@@ -41,7 +41,11 @@ This file is maintainer-only. Application/library consumers should use [Public A
41
41
  remains static. `derive.keyed` declares extra sources in one named dependency object;
42
42
  `{ source, key }` members bind one source key and `{ source, keys }` members bind an
43
43
  ordered duplicate-free set of source keys for each output key. The materialized Runtime owns those bindings and reverse indexes,
44
- including bindings to currently missing source entries. Processors do not create graph
44
+ including bindings to currently missing source entries. `projection/keyed/relation.ts`
45
+ owns edge differences and ordered forward snapshots. Keep retained reverse buckets intact;
46
+ pure reorder changes forward order even with zero edge writes. Callers validate unique
47
+ string keys, and reverse iteration is not formal order. Sequence comparison and
48
+ dependency snapshots can still cost O(number of selected keys). Processors do not create graph
45
49
  dependencies through imperative Runtime reads. A scope adds lifecycle ownership only
46
50
  through `scope.own`; scoped definitions may depend on root definitions, while root and
47
51
  sibling scopes cannot depend on scoped definitions. Notification failures leave commits
@@ -167,9 +167,26 @@ Produces ordered readonly `[key,value]` tuples. Value-only updates publish corre
167
167
 
168
168
  ```ts
169
169
  const active = derive.keyed.get(rows, activeId);
170
+ const fixed = derive.keyed.get(rows, rowId);
171
+ const selected = derive.keyed.get(rows, { selection }, ({ selection }) => selection.activeRowId);
170
172
  ```
171
173
 
172
- `activeId` is a scalar `Projection<K | undefined>`. The derive binds exactly the selected key. If the key is absent, the result is `undefined` while the binding remains live for later add.
174
+ `activeId` may be a static `K | undefined` or scalar `Projection<K | undefined>`.
175
+ Named form accepts ordinary projection dependencies, including `{}`, and a synchronous
176
+ callback returning `K | undefined`. The source owns K/V; callback results, key projections
177
+ and equality must agree with those domains. The result is `Projection<V | undefined>`.
178
+ An absent selected key remains requested for later add; undefined selects no key.
179
+ Optional result equality defaults to `Object.is` and retains equivalent references.
180
+
181
+ Named selectors run on initialization, declared dependency changes or recovery. Source
182
+ updates inspect deltas for the selected key; source-only reorder is ignored. Unrelated
183
+ updates can wake the processor but do not rerun selectors or change output. Keyed named
184
+ dependencies are whole snapshots: explicitly declaring the queried source as a named
185
+ dependency also makes its changes relevant to the selector. Public Runtime reads and
186
+ driver-relative dependency specs are not allowed inside this protocol. Callback, key
187
+ validation and equality failures follow normal recovery without partial publication.
188
+ Definitions are lazy, scoped and isolated per runtime; current batch reads and deferred
189
+ notifications use the ordinary runtime contract.
173
190
 
174
191
  Use this for active entity, editor target, selected record, or current-cell lookups.
175
192
 
@@ -242,11 +259,32 @@ Value-only source changes only recompute affected keys. Structural or source-ord
242
259
 
243
260
  ```ts
244
261
  const visibleRows = derive.keyed.subset(rows, orderedVisibleIds);
262
+ const selectedRows = derive.keyed.subset(rows, { selection }, ({ selection }) => selection.rowIds);
245
263
  ```
246
264
 
247
265
  Output order is `orderedVisibleIds ∩ source membership`. Missing requested ids remain latent and appear later if the source adds them. Duplicate requested keys are invalid. Values are source values and are not re-derived.
248
266
 
249
- Use `subset` when another projection or static array already owns the requested order.
267
+ The second argument may be a static readonly key array or a scalar key-array projection.
268
+ Named form accepts ordinary projection dependencies (including `{}`) and a synchronous
269
+ callback returning `readonly K[]`. The source owns K/V; selection cannot widen the key
270
+ domain. `[]` is empty; undefined, malformed arrays and duplicate keys (even absent ones)
271
+ are errors. Static arrays are snapshotted at definition time; dynamic arrays are captured
272
+ when accepted. Equal sequences reuse request state, regardless of array identity.
273
+
274
+ Dependency changes rerun the selector. Source-only updates filter the existing request;
275
+ they do not rerun selectors. Selected values update with the source's original identity,
276
+ missing keys join when added, and source-only reorder is ignored. Subset has no value
277
+ or keys equality option. A selection change and value change in one evaluation both
278
+ take effect, even when the key sequence remains equal. Reset/recovery, scope disposal,
279
+ runtime isolation and notification-boundary item lifecycles follow the normal contract.
280
+
281
+ Each get/subset uses one processor and one output. Source-delta filtering can cost
282
+ O(delta size + selection dependency count); the processor can wake for unrelated source
283
+ changes. It does not scan all source members. Structural changes/reset may scan requested
284
+ keys and prior output. Selection changes pay callback, normalization and dependency
285
+ snapshot costs. Named keyed dependencies are whole snapshots; including the queried
286
+ source explicitly can rerun the selector on its changes. Use `get`/`subset` to select
287
+ existing members and `singleton`/`fromEntries` to construct new keyed results.
250
288
 
251
289
  ### `filter`
252
290
 
@@ -345,7 +345,9 @@ derive.keyed(source, dependencies, (value, key, dependencies) => result, equalit
345
345
  derive.keyed.keys(source): Projection<readonly K[]>
346
346
  derive.keyed.values(source): Projection<readonly V[]>
347
347
  derive.keyed.entries(source): Projection<readonly (readonly [K,V])[]>
348
- derive.keyed.get(source, keyProjection, equality?): Projection<V | undefined>
348
+ derive.keyed.get(source, keyOrProjection, equality?): Projection<V | undefined>
349
+ derive.keyed.get(source, dependencies, selectKey, equality?): Projection<V | undefined>
350
+ // selectKey(readonlyNamedValues) returns Synchronous<K | undefined>
349
351
  derive.keyed.from(source: Projection<readonly V[]>, keyOf, equality?): KeyedProjection<K,V>
350
352
  derive.keyed.from(source: readonly V[], keyOf, equality?): KeyedProjection<K,V>
351
353
  derive.keyed.fromEntries(dependencies, compute, equality?): KeyedProjection<K,V>
@@ -354,6 +356,8 @@ derive.keyed.fromEntries(dependencies, compute, equality?): KeyedProjection<K,V>
354
356
  derive.keyed.merge(sources, { conflict: 'error' | 'first' | 'last', equality? }): KeyedProjection<K,V>
355
357
  derive.keyed.merge(sources, { conflict: 'resolve', resolve, equality? }): KeyedProjection<K,V>
356
358
  derive.keyed.subset(source, orderedKeysOrProjection): KeyedProjection<K,V>
359
+ derive.keyed.subset(source, dependencies, selectOrderedKeys): KeyedProjection<K,V>
360
+ // selectOrderedKeys(readonlyNamedValues) returns Synchronous<readonly K[]>
357
361
  derive.keyed.filter(source, predicate): KeyedProjection<K,V>
358
362
  derive.keyed.filter(source, dependencies, predicate): KeyedProjection<K,V>
359
363
  derive.keyed.compact(source, selector, equality?): KeyedProjection<K,T>
@@ -368,6 +372,8 @@ derive.keyed.singleton(dependencies, computeEntry, equality?): KeyedProjection<K
368
372
  // computeEntry(readonlyNamedValues) returns Synchronous<readonly [K,V] | undefined>
369
373
  ```
370
374
 
375
+ `get` and `subset` accept named ordinary projection dependencies, including `{}`, to compute one optional key or an ordered key array. Source defines K/V; selectors and equality cannot widen them. Source-only updates filter deltas without rerunning selectors. Missing keys remain requested. Subset preserves source values and requested order, rejects duplicate keys, and has no equality option. Equal key sequences reuse request state. Get's optional result equality defaults to `Object.is`. Callback/validation/equality failure follows normal recovery without partial publication. Processors may inspect unrelated source deltas; this is not scheduler-level key subscription. A keyed named dependency is a whole snapshot, including when it is also the queried source. See [Precise scalar lookup](projections.md#precise-scalar-lookup) and [Subset](projections.md#subset).
376
+
371
377
  `from` uses `keyOf(value)` as member identity and preserves the input array's formal order. Duplicate keys are processor errors; there is no silent first/last overwrite. Static arrays are shallow-snapshotted at definition creation while `keyOf` remains lazy. For a scalar array projection, each scalar publication is scanned because the source has no per-entry delta. Equality defaults to `Object.is`; an equality-equivalent value under the same key retains the previous published value identity.
372
378
 
373
379
  `fromEntries` accepts the same named projection object as `derive`, including `{}` and keyed projections. Single inputs use `{ source }`; direct-source, static-array and driver-relative dependency specs are not overloads. The callback receives readonly named values and returns the complete ordered tuple array synchronously. Empty arrays mean no members; missing keys are removed; present `undefined` is valid. Duplicate string keys, malformed tuples and callback/equality failures follow processor error/recovery rules without partial installation. Per-entry equality defaults to `Object.is` and retains equivalent value references without suppressing membership or order. Computation scans the complete old/new result, while downstream publication is incremental. A keyed dependency is a whole collection, not automatically tracked by `get`; compose `derive.keyed.get` for precise scalar lookup. See [Projections](projections.md#keyed-results-from-named-dependencies) for lifecycle, cost and examples.
@@ -323,10 +323,31 @@ Shared keys use override values while keeping the first formal position from `ba
323
323
  ```ts
324
324
  const activeId = input<RowId | undefined>(undefined);
325
325
  const activeRow = derive.keyed.get(rows, activeId);
326
+ const selectedRow = derive.keyed.get(rows, { selection }, ({ selection }) => selection.activeRowId);
326
327
  ```
327
328
 
328
329
  The binding remains meaningful while the selected key is absent and reacts if that key appears later.
329
330
 
331
+ Use a static key directly for a fixed lookup. Use named dependencies when an existing
332
+ selection projection owns the key; an intermediate scalar derive is unnecessary.
333
+
334
+ ## Ordered selected members
335
+
336
+ ```ts
337
+ const selectedNodes = derive.keyed.subset(
338
+ graph.node,
339
+ { selection },
340
+ ({ selection }) => selection.nodeIds
341
+ );
342
+ ```
343
+
344
+ Output follows `selection.nodeIds` order, including requested members that appear later
345
+ in the source. Selected source values pass through by identity. Source-only updates do
346
+ not rerun the selector; a new selection with equal keys does not produce a fake change.
347
+ Use `[]` to select nothing and avoid duplicate keys. Keep source out of named dependencies
348
+ unless computing the keys actually needs the entire source collection. The processor can
349
+ still inspect unrelated source deltas; it does not rebuild the whole subset for them.
350
+
330
351
  ## Reverse index / groupBy
331
352
 
332
353
  ```ts