doxum 0.1.26 → 0.1.28

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 (39) hide show
  1. package/README.md +86 -3
  2. package/dist/advanced.cjs +40 -30
  3. package/dist/advanced.cjs.map +1 -1
  4. package/dist/advanced.d.cts +34 -2
  5. package/dist/advanced.d.ts +34 -2
  6. package/dist/advanced.js +31 -22
  7. package/dist/advanced.js.map +1 -1
  8. package/dist/{definition-xgk3YNBp.d.ts → dependency-B3E65M1E.d.cts} +27 -3
  9. package/dist/{definition-CDOuvb5C.d.cts → dependency-BYbdDHNa.d.ts} +27 -3
  10. package/dist/driver-B5vdPohL.js.map +1 -1
  11. package/dist/driver-BL91gn7_.cjs.map +1 -1
  12. package/dist/index.cjs +885 -394
  13. package/dist/index.cjs.map +1 -1
  14. package/dist/index.d.cts +33 -18
  15. package/dist/index.d.ts +33 -18
  16. package/dist/index.js +784 -293
  17. package/dist/index.js.map +1 -1
  18. package/dist/local-sync.cjs.map +1 -1
  19. package/dist/local-sync.js.map +1 -1
  20. package/dist/transform-B4T3AcJr.js +938 -0
  21. package/dist/transform-B4T3AcJr.js.map +1 -0
  22. package/dist/transform-DUleAsG0.cjs +1105 -0
  23. package/dist/transform-DUleAsG0.cjs.map +1 -0
  24. package/package.json +1 -1
  25. package/skills/doxum-runtime/SKILL.md +16 -6
  26. package/skills/doxum-runtime/references/api.en.md +49 -15
  27. package/skills/doxum-runtime/references/api.zh-CN.md +47 -13
  28. package/skills/doxum-runtime/references/guide.en.md +2 -1
  29. package/skills/doxum-runtime/references/guide.zh-CN.md +2 -1
  30. package/skills/doxum-runtime/references/invariants.en.md +2 -2
  31. package/skills/doxum-runtime/references/invariants.zh-CN.md +2 -2
  32. package/skills/doxum-runtime/references/patterns.en.md +1 -1
  33. package/skills/doxum-runtime/references/patterns.zh-CN.md +1 -1
  34. package/skills/doxum-runtime/references/projections.en.md +77 -4
  35. package/skills/doxum-runtime/references/projections.zh-CN.md +74 -3
  36. package/dist/scheduler--137L3j-.js +0 -500
  37. package/dist/scheduler--137L3j-.js.map +0 -1
  38. package/dist/scheduler-D4VthaaK.cjs +0 -589
  39. package/dist/scheduler-D4VthaaK.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { a as ProjectionError, c as snapshotCollectionView, d as isProjection, f as ownProjections, h as profile, i as ProjectionDisposedError, l as defineProcessor, m as projectionRef, n as assertSynchronous, o as collectionView, p as producerOf, r as createScheduler, s as mapRead, t as assertScope, u as defineSource } from "./scheduler--137L3j-.js";
1
+ import { C as defineSource, D as projectionRef, E as producerOf, O as profile, S as defineProcessor, T as ownProjections, _ as compileProjectionDependencies, a as createKeyedDependencyRuntime, b as mapRead, c as assertScope, d as ProjectionDisposedError, f as ProjectionError, g as diffCollection, h as createCollectionChange, i as compileKeyedDependencies, l as assertSynchronous, m as collectionHasStructuralChange, n as keyedAbsent, o as outputKind, p as collectionChange, r as assertKeyedProjection, s as createKeyRelation, t as createKeyedTransform, u as createScheduler, v as snapshotDependencyValue, w as isProjection, x as snapshotCollectionView, y as collectionView } from "./transform-B4T3AcJr.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-B5vdPohL.js";
4
4
  //#region core/src/schema/model.ts
@@ -2734,268 +2734,517 @@ const isExternalSource = (source) => {
2734
2734
  return (candidate.kind === "value" || candidate.kind === "collection") && typeof candidate.current === "function" && typeof candidate.revision === "function" && typeof candidate.subscribe === "function";
2735
2735
  };
2736
2736
  //#endregion
2737
- //#region core/src/projection/derive.ts
2738
- const sourceValue = (source) => source.kind === "value" ? source.value : snapshotCollectionView(source.read);
2739
- const outputKind = (projection) => {
2740
- const ref = projectionRef(projection);
2741
- const definition = ref.producer;
2742
- const output = definition.kind === "source" ? definition.output : definition.outputs[ref.output];
2743
- if (!output) throw new TypeError("Projection output does not exist.");
2744
- return output.kind;
2745
- };
2746
- const assertKeyedProjection = (projection, role) => {
2747
- if (outputKind(projection) !== "collection") throw new TypeError(`${role} must be a keyed collection projection.`);
2748
- };
2749
- const isKeyedDependency = (value) => {
2750
- if (!value || typeof value !== "object" || isProjection(value)) return false;
2751
- const candidate = value;
2752
- return isProjection(candidate.source) && typeof candidate.key === "function";
2753
- };
2754
- const bindKey = (index, outputKey, sourceKey) => {
2755
- const previous = index.forward.get(outputKey);
2756
- if (previous === sourceKey) return;
2757
- if (previous !== void 0) {
2758
- const dependents = index.reverse.get(previous);
2759
- dependents?.delete(outputKey);
2760
- if (dependents?.size === 0) index.reverse.delete(previous);
2761
- index.forward.delete(outputKey);
2737
+ //#region core/src/projection/derive/keyed.ts
2738
+ const compileOrderedKeys = (value, role) => {
2739
+ if (!Array.isArray(value)) throw new TypeError(`${role} must be an array of string keys.`);
2740
+ const seen = /* @__PURE__ */ new Set();
2741
+ for (const key of value) {
2742
+ if (typeof key !== "string") throw new TypeError(`${role} must contain only string keys.`);
2743
+ if (seen.has(key)) throw new TypeError(`${role} must not contain duplicate keys.`);
2744
+ seen.add(key);
2762
2745
  }
2763
- if (sourceKey === void 0) return;
2764
- index.forward.set(outputKey, sourceKey);
2765
- const dependents = index.reverse.get(sourceKey);
2766
- if (dependents) dependents.add(outputKey);
2767
- else index.reverse.set(sourceKey, new Set([outputKey]));
2768
- };
2769
- const unbindKey = (indexes, outputKey) => {
2770
- for (const index of indexes) bindKey(index, outputKey, void 0);
2771
- };
2772
- const createBindingIndex = () => ({
2773
- forward: /* @__PURE__ */ new Map(),
2774
- reverse: /* @__PURE__ */ new Map()
2775
- });
2776
- function createValueDerive(dependencies, compute, equality = Object.is) {
2777
- if (!isPlainObject(dependencies) || isProjection(dependencies)) throw new TypeError("Derive dependencies must be a plain object.");
2778
- const names = Object.keys(dependencies);
2779
- const ordered = names.map((name) => dependencies[name]);
2780
- ordered.forEach(projectionRef);
2746
+ return {
2747
+ keys: value,
2748
+ requested: seen
2749
+ };
2750
+ };
2751
+ function createKeyedDerive(source, dependenciesOrSelect, selectOrEquality, maybeEquality) {
2752
+ const dependencySpecs = typeof dependenciesOrSelect === "function" ? void 0 : dependenciesOrSelect;
2753
+ const select = dependencySpecs === void 0 ? dependenciesOrSelect : selectOrEquality;
2754
+ const equality = dependencySpecs === void 0 ? selectOrEquality : maybeEquality;
2755
+ if (typeof select !== "function") throw new TypeError("Keyed derive requires a selector.");
2756
+ return createKeyedTransform({
2757
+ source,
2758
+ dependencies: dependencySpecs,
2759
+ equality,
2760
+ name: "derive.keyed",
2761
+ evaluate: ({ value, key, dependencies }) => dependencySpecs === void 0 ? select(value, key) : select(value, key, dependencies)
2762
+ });
2763
+ }
2764
+ const createKeyedKeys = (source) => {
2765
+ assertKeyedProjection(source, "Keyed keys source");
2781
2766
  const [projection] = defineProcessor({
2782
- dependencies: ordered,
2767
+ dependencies: [source],
2783
2768
  outputs: [{
2784
2769
  kind: "value",
2785
- equality
2770
+ equality: Object.is
2786
2771
  }],
2787
2772
  create: () => ({ evaluate: (evaluation) => {
2773
+ const driver = evaluation.sources[0];
2788
2774
  const output = evaluation.outputs[0];
2789
- if (output.kind !== "value") throw new Error("derive requires a value output.");
2790
- const values = Object.create(null);
2791
- for (let index = 0; index < names.length; index++) values[names[index]] = sourceValue(evaluation.sources[index]);
2792
- const next = compute(Object.freeze(values));
2793
- assertSynchronous(next);
2794
- output.output.set(next);
2795
- } })
2775
+ if (driver.kind !== "collection" || output.kind !== "value") throw new Error("derive.keyed.keys requires collection input and value output.");
2776
+ if (evaluation.reset || !driver.change || collectionHasStructuralChange(driver.change)) output.output.set(driver.read.ids());
2777
+ } }),
2778
+ name: "derive.keyed.keys"
2796
2779
  });
2797
2780
  return projection;
2781
+ };
2782
+ const createOrderedSnapshot = (source, name, make) => {
2783
+ assertKeyedProjection(source, `${name} source`);
2784
+ const [projection] = defineProcessor({
2785
+ dependencies: [source],
2786
+ outputs: [{
2787
+ kind: "value",
2788
+ equality: Object.is
2789
+ }],
2790
+ create: () => {
2791
+ let indexes = /* @__PURE__ */ new Map();
2792
+ const rebuild = (driver) => {
2793
+ const nextIndexes = /* @__PURE__ */ new Map();
2794
+ const result = Object.freeze(driver.read.ids().map((key, index) => {
2795
+ nextIndexes.set(key, index);
2796
+ return make(key, driver.read.get(key));
2797
+ }));
2798
+ indexes = nextIndexes;
2799
+ return result;
2800
+ };
2801
+ const rebuildIncremental = (driver, change, previous) => {
2802
+ const changedValues = /* @__PURE__ */ new Map();
2803
+ for (const entry of change.added) changedValues.set(entry.key, entry.after);
2804
+ for (const entry of change.updated) changedValues.set(entry.key, entry.after);
2805
+ const nextIndexes = /* @__PURE__ */ new Map();
2806
+ const result = Object.freeze(driver.read.ids().map((key, index) => {
2807
+ nextIndexes.set(key, index);
2808
+ if (changedValues.has(key)) return make(key, changedValues.get(key));
2809
+ const previousIndex = indexes.get(key);
2810
+ return previousIndex === void 0 ? make(key, driver.read.get(key)) : previous[previousIndex];
2811
+ }));
2812
+ indexes = nextIndexes;
2813
+ return result;
2814
+ };
2815
+ return {
2816
+ evaluate: (evaluation) => {
2817
+ const driver = evaluation.sources[0];
2818
+ const output = evaluation.outputs[0];
2819
+ if (driver.kind !== "collection" || output.kind !== "value") throw new Error(`${name} requires collection input and value output.`);
2820
+ const change = driver.change;
2821
+ if (evaluation.reset || !change || change.kind === "reset") {
2822
+ output.output.set(rebuild(driver));
2823
+ return;
2824
+ }
2825
+ const previous = output.previous;
2826
+ if (!previous) {
2827
+ output.output.set(rebuild(driver));
2828
+ return;
2829
+ }
2830
+ if (collectionHasStructuralChange(change)) {
2831
+ output.output.set(rebuildIncremental(driver, change, previous));
2832
+ return;
2833
+ }
2834
+ if (!change.updated.length) return;
2835
+ const next = [...previous];
2836
+ for (const entry of change.updated) {
2837
+ const index = indexes.get(entry.key);
2838
+ if (index === void 0) {
2839
+ output.output.set(rebuild(driver));
2840
+ return;
2841
+ }
2842
+ next[index] = make(entry.key, entry.after);
2843
+ }
2844
+ output.output.set(Object.freeze(next));
2845
+ },
2846
+ release: () => indexes.clear()
2847
+ };
2848
+ },
2849
+ name
2850
+ });
2851
+ return projection;
2852
+ };
2853
+ const createKeyedValues = (source) => createOrderedSnapshot(source, "derive.keyed.values", (_key, value) => value);
2854
+ const createKeyedEntries = (source) => createOrderedSnapshot(source, "derive.keyed.entries", (key, value) => Object.freeze([key, value]));
2855
+ const createKeyedGet = (source, key, equality = Object.is) => {
2856
+ assertKeyedProjection(source, "derive.keyed.get source");
2857
+ if (outputKind(key) !== "value") throw new TypeError("derive.keyed.get key must be a scalar projection.");
2858
+ const [projection] = defineProcessor({
2859
+ dependencies: [source, key],
2860
+ outputs: [{
2861
+ kind: "value",
2862
+ equality
2863
+ }],
2864
+ create: () => {
2865
+ let sourceRevision = -1;
2866
+ let keyRevision = -1;
2867
+ let selected;
2868
+ return { evaluate: (evaluation) => {
2869
+ const collection = evaluation.sources[0];
2870
+ const selectedKey = evaluation.sources[1];
2871
+ const output = evaluation.outputs[0];
2872
+ if (collection.kind !== "collection" || selectedKey.kind !== "value" || output.kind !== "value") throw new Error("derive.keyed.get resolved invalid input/output kinds.");
2873
+ let affected = evaluation.reset;
2874
+ if (evaluation.reset || keyRevision !== selectedKey.revision) {
2875
+ const candidate = selectedKey.value;
2876
+ if (candidate !== void 0 && typeof candidate !== "string") throw new TypeError("derive.keyed.get key must resolve to a string or undefined.");
2877
+ selected = candidate;
2878
+ affected = true;
2879
+ } else if (sourceRevision !== collection.revision) {
2880
+ const change = collection.change;
2881
+ if (!change || change.kind === "reset") affected = true;
2882
+ else if (selected !== void 0) {
2883
+ for (const changedKey of collectionChange.keys(change)) if (changedKey === selected) {
2884
+ affected = true;
2885
+ break;
2886
+ }
2887
+ }
2888
+ }
2889
+ if (affected) output.output.set(selected === void 0 ? void 0 : collection.read.get(selected));
2890
+ sourceRevision = collection.revision;
2891
+ keyRevision = selectedKey.revision;
2892
+ } };
2893
+ },
2894
+ name: "derive.keyed.get"
2895
+ });
2896
+ return projection;
2897
+ };
2898
+ function createKeyedFilter(source, dependenciesOrPredicate, maybePredicate) {
2899
+ const dependencySpecs = typeof dependenciesOrPredicate === "function" ? void 0 : dependenciesOrPredicate;
2900
+ const predicate = dependencySpecs === void 0 ? dependenciesOrPredicate : maybePredicate;
2901
+ if (typeof predicate !== "function") throw new TypeError("Keyed filter requires a predicate.");
2902
+ return createKeyedTransform({
2903
+ source,
2904
+ dependencies: dependencySpecs,
2905
+ equality: Object.is,
2906
+ name: "derive.keyed.filter",
2907
+ evaluate: ({ value, key, dependencies }) => {
2908
+ const included = dependencySpecs === void 0 ? predicate(value, key) : predicate(value, key, dependencies);
2909
+ assertSynchronous(included);
2910
+ if (typeof included !== "boolean") throw new TypeError("Keyed filter predicate must return boolean.");
2911
+ return included ? value : keyedAbsent;
2912
+ }
2913
+ });
2798
2914
  }
2799
- function createKeyedDerive(source, dependenciesOrSelect, selectOrEquality, maybeEquality) {
2800
- assertKeyedProjection(source, "Keyed derive source");
2915
+ function createKeyedCompact(source, dependenciesOrSelect, selectOrEquality, maybeEquality) {
2801
2916
  const dependencySpecs = typeof dependenciesOrSelect === "function" ? void 0 : dependenciesOrSelect;
2802
2917
  const select = dependencySpecs === void 0 ? dependenciesOrSelect : selectOrEquality;
2803
2918
  const equality = dependencySpecs === void 0 ? selectOrEquality : maybeEquality;
2804
- if (typeof select !== "function") throw new TypeError("Keyed derive requires a selector.");
2805
- const dependencies = [source];
2806
- const dependencyNames = [];
2807
- const keyed = [];
2808
- if (dependencySpecs !== void 0) {
2809
- if (!isPlainObject(dependencySpecs) || isProjection(dependencySpecs)) throw new TypeError("Keyed derive dependencies must be a plain object.");
2810
- for (const name of Reflect.ownKeys(dependencySpecs)) {
2811
- if (typeof name !== "string") throw new TypeError("Keyed derive dependency names must be strings.");
2812
- const descriptor = Object.getOwnPropertyDescriptor(dependencySpecs, name);
2813
- if (!descriptor?.enumerable || !("value" in descriptor)) throw new TypeError("Keyed derive dependencies must be enumerable data properties.");
2814
- const dependency = descriptor.value;
2815
- dependencyNames.push(name);
2816
- if (isProjection(dependency)) {
2817
- dependencies.push(dependency);
2818
- keyed.push(void 0);
2819
- continue;
2820
- }
2821
- if (!isKeyedDependency(dependency)) throw new TypeError("Keyed derive dependencies must be projections or keyed lookups.");
2822
- assertKeyedProjection(dependency.source, "Dynamic keyed dependency source");
2823
- dependencies.push(dependency.source);
2824
- keyed.push(dependency);
2919
+ if (typeof select !== "function") throw new TypeError("Keyed compact requires a selector.");
2920
+ return createKeyedTransform({
2921
+ source,
2922
+ dependencies: dependencySpecs,
2923
+ equality,
2924
+ name: "derive.keyed.compact",
2925
+ evaluate: ({ value, key, dependencies }) => {
2926
+ const next = dependencySpecs === void 0 ? select(value, key) : select(value, key, dependencies);
2927
+ assertSynchronous(next);
2928
+ return next === void 0 ? keyedAbsent : next;
2825
2929
  }
2930
+ });
2931
+ }
2932
+ const createKeyedSubset = (source, orderedKeys) => {
2933
+ assertKeyedProjection(source, "Keyed subset source");
2934
+ const keyProjection = isProjection(orderedKeys) ? orderedKeys : void 0;
2935
+ if (keyProjection && outputKind(keyProjection) !== "value") throw new TypeError("Keyed subset ordered keys must be a value projection.");
2936
+ const staticSelection = keyProjection ? void 0 : (() => {
2937
+ const compiled = compileOrderedKeys(orderedKeys, "Keyed subset ordered keys");
2938
+ return {
2939
+ keys: Object.freeze([...compiled.keys]),
2940
+ requested: compiled.requested
2941
+ };
2942
+ })();
2943
+ const [projection] = defineProcessor({
2944
+ dependencies: keyProjection ? [source, keyProjection] : [source],
2945
+ outputs: [{
2946
+ kind: "collection",
2947
+ equality: Object.is
2948
+ }],
2949
+ create: () => {
2950
+ let keys = staticSelection?.keys;
2951
+ let requested = staticSelection?.requested;
2952
+ let keyRevision = -1;
2953
+ return {
2954
+ evaluate: (evaluation) => {
2955
+ const driver = evaluation.sources[0];
2956
+ const output = evaluation.outputs[0];
2957
+ if (driver.kind !== "collection" || output.kind !== "collection") throw new Error("derive.keyed.subset requires collection input and output.");
2958
+ let keysChanged = false;
2959
+ if (keyProjection) {
2960
+ const sourceContext = evaluation.sources[1];
2961
+ if (sourceContext?.kind !== "value") throw new TypeError("Keyed subset ordered keys resolved to a collection.");
2962
+ if (keys === void 0 || sourceContext.reset || keyRevision !== sourceContext.revision) {
2963
+ const compiled = compileOrderedKeys(sourceContext.value, "Keyed subset ordered keys");
2964
+ keys = compiled.keys;
2965
+ requested = compiled.requested;
2966
+ keyRevision = sourceContext.revision;
2967
+ keysChanged = true;
2968
+ }
2969
+ }
2970
+ if (!keys || !requested) throw new Error("Keyed subset keys were not initialized.");
2971
+ const activeKeys = keys;
2972
+ const activeRequested = requested;
2973
+ const orderedPresentKeys = () => activeKeys.filter((key) => driver.read.has(key));
2974
+ const rebuild = () => {
2975
+ const nextKeys = orderedPresentKeys();
2976
+ for (const key of nextKeys) output.output.set(key, driver.read.get(key));
2977
+ output.output.order(nextKeys);
2978
+ };
2979
+ const reconcileKeys = () => {
2980
+ const nextKeys = orderedPresentKeys();
2981
+ for (const key of output.previous.ids()) if (!activeRequested.has(key) || !driver.read.has(key)) output.output.remove(key);
2982
+ for (const key of nextKeys) if (!output.previous.has(key)) output.output.set(key, driver.read.get(key));
2983
+ output.output.order(nextKeys);
2984
+ };
2985
+ const change = driver.change;
2986
+ if (evaluation.reset || change?.kind === "reset") rebuild();
2987
+ else if (keysChanged) {
2988
+ reconcileKeys();
2989
+ if (change) for (const entry of change.updated) {
2990
+ const key = entry.key;
2991
+ if (activeRequested.has(key) && output.previous.has(key)) output.output.set(key, entry.after);
2992
+ }
2993
+ } else if (change) {
2994
+ let updateOrder = false;
2995
+ for (const entry of change.removed) {
2996
+ const key = entry.key;
2997
+ if (!activeRequested.has(key)) continue;
2998
+ output.output.remove(key);
2999
+ updateOrder = true;
3000
+ }
3001
+ for (const entry of change.added) {
3002
+ const key = entry.key;
3003
+ if (!activeRequested.has(key)) continue;
3004
+ output.output.set(key, entry.after);
3005
+ updateOrder = true;
3006
+ }
3007
+ for (const entry of change.updated) {
3008
+ const key = entry.key;
3009
+ if (activeRequested.has(key)) output.output.set(key, entry.after);
3010
+ }
3011
+ if (updateOrder) output.output.order(orderedPresentKeys());
3012
+ }
3013
+ },
3014
+ release: () => {
3015
+ if (keyProjection) {
3016
+ keys = void 0;
3017
+ requested = void 0;
3018
+ keyRevision = -1;
3019
+ }
3020
+ }
3021
+ };
3022
+ },
3023
+ name: "derive.keyed.subset"
3024
+ });
3025
+ return projection;
3026
+ };
3027
+ const normalizeGroups = (value) => {
3028
+ const groups = typeof value === "string" ? [value] : value;
3029
+ if (!Array.isArray(groups)) throw new TypeError("derive.keyed.groupBy selector must return a string or an array of strings.");
3030
+ const seen = /* @__PURE__ */ new Set();
3031
+ const result = [];
3032
+ for (const group of groups) {
3033
+ if (typeof group !== "string") throw new TypeError("derive.keyed.groupBy selector must return only string group keys.");
3034
+ if (seen.has(group)) throw new TypeError("derive.keyed.groupBy selector must not return duplicate group keys.");
3035
+ seen.add(group);
3036
+ result.push(group);
2826
3037
  }
3038
+ return Object.freeze(result);
3039
+ };
3040
+ function createKeyedGroupBy(source, dependenciesOrSelector, maybeSelector) {
3041
+ assertKeyedProjection(source, "derive.keyed.groupBy source");
3042
+ const dependencySpecs = typeof dependenciesOrSelector === "function" ? void 0 : dependenciesOrSelector;
3043
+ const selector = dependencySpecs === void 0 ? dependenciesOrSelector : maybeSelector;
3044
+ if (typeof selector !== "function") throw new TypeError("derive.keyed.groupBy requires a selector.");
3045
+ const compiled = compileKeyedDependencies(source, dependencySpecs, "derive.keyed.groupBy");
2827
3046
  const [projection] = defineProcessor({
2828
- dependencies,
3047
+ dependencies: compiled.projections,
2829
3048
  outputs: [{
2830
3049
  kind: "collection",
2831
- equality: equality ?? Object.is
3050
+ equality: (previous, next) => {
3051
+ const left = previous;
3052
+ const right = next;
3053
+ return left.length === right.length && left.every((key, index) => key === right[index]);
3054
+ }
2832
3055
  }],
2833
3056
  create: () => {
2834
- const bindings = keyed.map((dependency) => dependency ? createBindingIndex() : void 0);
2835
- const bindingIndexes = bindings.filter((index) => index !== void 0);
2836
- const globalValues = keyed.map(() => void 0);
2837
- let revisions = Object.freeze([]);
2838
- const clearBindings = () => {
2839
- for (const index of bindings) {
2840
- index?.forward.clear();
2841
- index?.reverse.clear();
3057
+ const dependencies = createKeyedDependencyRuntime(compiled);
3058
+ const relation = createKeyRelation();
3059
+ const orderIndex = /* @__PURE__ */ new Map();
3060
+ const groupRanks = /* @__PURE__ */ new Map();
3061
+ const refreshOrderIndex = (driver) => {
3062
+ orderIndex.clear();
3063
+ driver.read.ids().forEach((key, index) => orderIndex.set(key, index));
3064
+ };
3065
+ const selectGroups = (driver, sources, key, affected) => {
3066
+ if (!driver.read.has(key)) return false;
3067
+ const value = driver.read.get(key);
3068
+ const dependencyValues = dependencies.resolve(value, key, sources);
3069
+ const selected = dependencySpecs === void 0 ? selector(value, key) : selector(value, key, dependencyValues);
3070
+ assertSynchronous(selected);
3071
+ const next = normalizeGroups(selected);
3072
+ const previous = relation.forward(key) ?? Object.freeze([]);
3073
+ if (!relation.replace(key, next)) return false;
3074
+ for (const group of previous) affected.add(group);
3075
+ for (const group of next) affected.add(group);
3076
+ return true;
3077
+ };
3078
+ const updateGroup = (group, output) => {
3079
+ const reverse = relation.reverse(group);
3080
+ if (!reverse?.size) {
3081
+ groupRanks.delete(group);
3082
+ output.output.remove(group);
3083
+ return;
2842
3084
  }
3085
+ const members = [...reverse].sort((left, right) => (orderIndex.get(left) ?? Number.MAX_SAFE_INTEGER) - (orderIndex.get(right) ?? Number.MAX_SAFE_INTEGER));
3086
+ const first = members[0];
3087
+ const source = orderIndex.get(first);
3088
+ const selector = relation.forward(first)?.indexOf(group) ?? -1;
3089
+ if (source === void 0 || selector < 0) throw new Error("derive.keyed.groupBy relation is inconsistent with source order.");
3090
+ groupRanks.set(group, {
3091
+ source,
3092
+ selector
3093
+ });
3094
+ output.output.set(group, Object.freeze(members));
3095
+ };
3096
+ const publishGroupOrder = (output) => {
3097
+ const order = [...groupRanks.entries()].sort(([leftKey, left], [rightKey, right]) => {
3098
+ if (left.source !== right.source) return left.source - right.source;
3099
+ if (left.selector !== right.selector) return left.selector - right.selector;
3100
+ return leftKey < rightKey ? -1 : leftKey > rightKey ? 1 : 0;
3101
+ }).map(([group]) => group);
3102
+ output.output.order(order);
3103
+ };
3104
+ const rebuild = (driver, sources, output) => {
3105
+ dependencies.clearBindings();
3106
+ relation.clear();
3107
+ groupRanks.clear();
3108
+ refreshOrderIndex(driver);
3109
+ const affected = /* @__PURE__ */ new Set();
3110
+ for (const key of driver.read.ids()) selectGroups(driver, sources, key, affected);
3111
+ for (const key of output.previous.ids()) affected.add(key);
3112
+ for (const group of affected) updateGroup(group, output);
3113
+ publishGroupOrder(output);
3114
+ };
3115
+ const publishAffected = (affected, output, orderDirty) => {
3116
+ for (const group of affected) updateGroup(group, output);
3117
+ if (orderDirty) publishGroupOrder(output);
3118
+ };
3119
+ const removeSourceKey = (key, affected) => {
3120
+ const previous = relation.forward(key);
3121
+ if (!previous) return false;
3122
+ for (const group of previous) affected.add(group);
3123
+ return relation.delete(key);
3124
+ };
3125
+ const markAllGroups = (output, affected) => {
3126
+ for (const group of relation.rights()) affected.add(group);
3127
+ for (const group of output.previous.ids()) affected.add(group);
2843
3128
  };
2844
3129
  return {
2845
3130
  evaluate: (evaluation) => {
2846
3131
  const driver = evaluation.sources[0];
2847
3132
  const output = evaluation.outputs[0];
2848
- if (driver.kind !== "collection" || output.kind !== "collection") throw new Error("Keyed derive requires collection input and output.");
2849
- const currentRevisions = evaluation.sources.map((current) => current.revision);
2850
- for (let index = 0; index < keyed.length; index++) {
2851
- if (keyed[index]) continue;
2852
- if (evaluation.reset || revisions[index + 1] !== currentRevisions[index + 1]) globalValues[index] = sourceValue(evaluation.sources[index + 1]);
2853
- }
2854
- const project = (key) => {
2855
- if (!driver.read.has(key)) return;
2856
- const value = driver.read.get(key);
2857
- const dependencyValues = Object.create(null);
2858
- for (let index = 0; index < keyed.length; index++) {
2859
- const dependency = keyed[index];
2860
- if (!dependency) {
2861
- dependencyValues[dependencyNames[index]] = globalValues[index];
2862
- continue;
2863
- }
2864
- const sourceContext = evaluation.sources[index + 1];
2865
- if (sourceContext.kind !== "collection") throw new TypeError("Dynamic keyed dependency resolved to a non-collection source.");
2866
- const selectedKey = dependency.key(value, key);
2867
- assertSynchronous(selectedKey);
2868
- if (selectedKey !== void 0 && typeof selectedKey !== "string") throw new TypeError("Dynamic keyed dependency keys must be strings or undefined.");
2869
- bindKey(bindings[index], key, selectedKey);
2870
- dependencyValues[dependencyNames[index]] = selectedKey === void 0 ? void 0 : sourceContext.read.get(selectedKey);
2871
- }
2872
- const next = dependencySpecs === void 0 ? select(value, key) : select(value, key, Object.freeze(dependencyValues));
2873
- assertSynchronous(next);
2874
- output.output.set(key, next);
2875
- };
3133
+ if (driver.kind !== "collection" || output.kind !== "collection") throw new Error("derive.keyed.groupBy requires collection input and output.");
3134
+ dependencies.prepare(evaluation.sources, evaluation.reset);
2876
3135
  if (evaluation.reset) {
2877
- clearBindings();
2878
- const ids = driver.read.ids();
2879
- for (const key of ids) project(key);
2880
- output.output.order(ids);
2881
- revisions = Object.freeze(currentRevisions);
3136
+ rebuild(driver, evaluation.sources, output);
3137
+ dependencies.remember(evaluation.sources);
2882
3138
  return;
2883
3139
  }
2884
3140
  const dirty = /* @__PURE__ */ new Set();
3141
+ const affected = /* @__PURE__ */ new Set();
2885
3142
  let all = false;
2886
- let updateOrder = false;
2887
- if (revisions[0] !== currentRevisions[0]) {
3143
+ let structural = false;
3144
+ let orderDirty = false;
3145
+ if (dependencies.driverChanged(driver)) {
2888
3146
  const change = driver.change;
2889
3147
  if (!change || change.kind === "reset") {
2890
- all = true;
2891
- updateOrder = true;
3148
+ rebuild(driver, evaluation.sources, output);
3149
+ dependencies.remember(evaluation.sources);
3150
+ return;
2892
3151
  } else {
2893
3152
  for (const entry of change.removed) {
2894
- output.output.remove(entry.key);
2895
- unbindKey(bindingIndexes, entry.key);
3153
+ orderDirty ||= removeSourceKey(entry.key, affected);
3154
+ dependencies.remove(entry.key);
2896
3155
  dirty.delete(entry.key);
2897
3156
  }
2898
3157
  for (const entry of change.added) dirty.add(entry.key);
2899
3158
  for (const entry of change.updated) dirty.add(entry.key);
2900
- updateOrder = Boolean(change.added.length || change.removed.length || change.order);
2901
- }
2902
- }
2903
- for (let index = 0; index < keyed.length; index++) {
2904
- if (revisions[index + 1] === currentRevisions[index + 1]) continue;
2905
- if (!keyed[index]) {
2906
- all = true;
2907
- continue;
2908
- }
2909
- const sourceContext = evaluation.sources[index + 1];
2910
- if (sourceContext.kind !== "collection" || !sourceContext.change) {
2911
- all = true;
2912
- continue;
3159
+ structural = Boolean(change.added.length || change.removed.length || change.order);
3160
+ orderDirty ||= structural;
3161
+ if (structural) refreshOrderIndex(driver);
2913
3162
  }
2914
- if (sourceContext.change.kind === "reset") {
2915
- all = true;
2916
- continue;
2917
- }
2918
- const reverse = bindings[index].reverse;
2919
- for (const transition of sourceContext.change.added) for (const outputKey of reverse.get(transition.key) ?? []) dirty.add(outputKey);
2920
- for (const transition of sourceContext.change.updated) for (const outputKey of reverse.get(transition.key) ?? []) dirty.add(outputKey);
2921
- for (const transition of sourceContext.change.removed) for (const outputKey of reverse.get(transition.key) ?? []) dirty.add(outputKey);
2922
3163
  }
3164
+ all ||= dependencies.collectInvalidated(evaluation.sources, dirty);
2923
3165
  if (all) for (const key of driver.read.ids()) dirty.add(key);
2924
- for (const key of dirty) if (driver.read.has(key)) project(key);
2925
- if (updateOrder) output.output.order(driver.read.ids());
2926
- revisions = Object.freeze(currentRevisions);
3166
+ for (const key of dirty) orderDirty ||= selectGroups(driver, evaluation.sources, key, affected);
3167
+ if (structural) markAllGroups(output, affected);
3168
+ publishAffected(affected, output, orderDirty);
3169
+ dependencies.remember(evaluation.sources);
2927
3170
  },
2928
3171
  release: () => {
2929
- clearBindings();
2930
- revisions = Object.freeze([]);
3172
+ relation.clear();
3173
+ orderIndex.clear();
3174
+ groupRanks.clear();
3175
+ dependencies.release();
2931
3176
  }
2932
3177
  };
2933
3178
  },
2934
- name: "derive.keyed"
3179
+ name: "derive.keyed.groupBy"
2935
3180
  });
2936
3181
  return projection;
2937
3182
  }
2938
- const derive = Object.assign(createValueDerive, { keyed: createKeyedDerive });
2939
- //#endregion
2940
- //#region core/src/projection/collection/change.ts
2941
- const commonOrderChanged = (before, after, added, removed) => {
2942
- const beforeCommon = before.filter((key) => !removed.has(key));
2943
- const afterCommon = after.filter((key) => !added.has(key));
2944
- return beforeCommon.some((key, index) => afterCommon[index] !== key);
2945
- };
2946
- /** Builds the canonical incremental CollectionChange from already-known net transitions. */
2947
- const createCollectionChange = (input) => {
2948
- const addedKeys = new Set(input.added.map((entry) => entry.key));
2949
- const removedKeys = new Set(input.removed.map((entry) => entry.key));
2950
- const orderChanged = input.beforeOrder !== input.afterOrder && commonOrderChanged(input.beforeOrder, input.afterOrder, addedKeys, removedKeys);
2951
- if (!input.added.length && !input.updated.length && !input.removed.length && !orderChanged) return void 0;
2952
- return Object.freeze({
2953
- kind: "incremental",
2954
- added: Object.freeze([...input.added]),
2955
- updated: Object.freeze([...input.updated]),
2956
- removed: Object.freeze([...input.removed]),
2957
- ...orderChanged ? { order: Object.freeze({
2958
- before: Object.freeze([...input.beforeOrder]),
2959
- after: Object.freeze([...input.afterOrder])
2960
- }) } : {}
3183
+ const createKeyedSingleton = (source, keyOf, equality = Object.is) => {
3184
+ if (outputKind(source) !== "value") throw new TypeError("derive.keyed.singleton source must be a scalar projection.");
3185
+ if (typeof keyOf !== "function") throw new TypeError("derive.keyed.singleton requires a key selector.");
3186
+ const [projection] = defineProcessor({
3187
+ dependencies: [source],
3188
+ outputs: [{
3189
+ kind: "collection",
3190
+ equality
3191
+ }],
3192
+ create: () => ({ evaluate: (evaluation) => {
3193
+ const scalar = evaluation.sources[0];
3194
+ const output = evaluation.outputs[0];
3195
+ if (scalar.kind !== "value" || output.kind !== "collection") throw new Error("derive.keyed.singleton resolved invalid input/output kinds.");
3196
+ const previous = output.previous.ids();
3197
+ if (scalar.value === void 0) {
3198
+ for (const key of previous) output.output.remove(key);
3199
+ return;
3200
+ }
3201
+ const key = keyOf(scalar.value);
3202
+ assertSynchronous(key);
3203
+ if (typeof key !== "string") throw new TypeError("derive.keyed.singleton key selector must return a string.");
3204
+ for (const previousKey of previous) if (previousKey !== key) output.output.remove(previousKey);
3205
+ output.output.set(key, scalar.value);
3206
+ if (previous.length !== 1 || previous[0] !== key) output.output.order([key]);
3207
+ } }),
3208
+ name: "derive.keyed.singleton"
2961
3209
  });
3210
+ return projection;
2962
3211
  };
2963
- /** Exact fallback diff between two published map-like snapshots. */
2964
- const diffCollection = (previous, current, isEqual = Object.is) => {
2965
- const added = [];
2966
- const updated = [];
2967
- const removed = [];
2968
- for (const [key, before] of previous) if (!current.has(key)) removed.push({
2969
- key,
2970
- before
2971
- });
2972
- else {
2973
- const after = current.get(key);
2974
- if (!isEqual(before, after)) updated.push({
2975
- key,
2976
- before,
2977
- after
2978
- });
2979
- }
2980
- for (const [key, after] of current) if (!previous.has(key)) added.push({
2981
- key,
2982
- after
2983
- });
2984
- return createCollectionChange({
2985
- added,
2986
- updated,
2987
- removed,
2988
- beforeOrder: [...previous.keys()],
2989
- afterOrder: [...current.keys()]
3212
+ const keyedDerive = Object.assign(createKeyedDerive, {
3213
+ keys: createKeyedKeys,
3214
+ values: createKeyedValues,
3215
+ entries: createKeyedEntries,
3216
+ get: createKeyedGet,
3217
+ groupBy: createKeyedGroupBy,
3218
+ singleton: createKeyedSingleton,
3219
+ subset: createKeyedSubset,
3220
+ filter: createKeyedFilter,
3221
+ compact: createKeyedCompact
3222
+ });
3223
+ //#endregion
3224
+ //#region core/src/projection/derive.ts
3225
+ function createValueDerive(dependencies, compute, equality = Object.is) {
3226
+ const compiled = compileProjectionDependencies(dependencies, "Derive");
3227
+ const names = compiled.names;
3228
+ const ordered = compiled.projections;
3229
+ const [projection] = defineProcessor({
3230
+ dependencies: ordered,
3231
+ outputs: [{
3232
+ kind: "value",
3233
+ equality
3234
+ }],
3235
+ create: () => ({ evaluate: (evaluation) => {
3236
+ const output = evaluation.outputs[0];
3237
+ if (output.kind !== "value") throw new Error("derive requires a value output.");
3238
+ const values = Object.create(null);
3239
+ for (let index = 0; index < names.length; index++) values[names[index]] = snapshotDependencyValue(evaluation.sources[index]);
3240
+ const next = compute(Object.freeze(values));
3241
+ assertSynchronous(next);
3242
+ output.output.set(next);
3243
+ } })
2990
3244
  });
2991
- };
2992
- const collectionChangedKeys = (change) => change.kind === "reset" ? /* @__PURE__ */ new Set() : new Set([
2993
- ...change.added.map((entry) => entry.key),
2994
- ...change.updated.map((entry) => entry.key),
2995
- ...change.removed.map((entry) => entry.key)
2996
- ]);
2997
- const collectionHasStructuralChange = (change) => change.kind === "reset" || Boolean(change.added.length || change.removed.length || change.order);
2998
- const collectionHasAnyChange = (change) => change.kind === "reset" || Boolean(change.added.length || change.updated.length || change.removed.length || change.order);
3245
+ return projection;
3246
+ }
3247
+ const derive = Object.assign(createValueDerive, { keyed: keyedDerive });
2999
3248
  //#endregion
3000
3249
  //#region core/src/projection/collection/index.ts
3001
3250
  const height = (node) => node?.height ?? 0;
@@ -3208,15 +3457,36 @@ const createCollectionOutput = (binding) => {
3208
3457
  profile.projection("touchedKeys", staged.size);
3209
3458
  const added = /* @__PURE__ */ new Set();
3210
3459
  const removed = /* @__PURE__ */ new Set();
3211
- const updated = /* @__PURE__ */ new Set();
3460
+ const addedEntries = [];
3461
+ const updatedEntries = [];
3462
+ const removedEntries = [];
3212
3463
  for (const [key, entry] of staged) {
3213
3464
  const existed = published.has(key);
3214
3465
  if (entry.present) {
3215
- if (!existed) added.add(key);
3216
- else if (!isEqual(published.get(key), entry.value)) updated.add(key);
3466
+ if (!existed) {
3467
+ added.add(key);
3468
+ addedEntries.push({
3469
+ key,
3470
+ after: entry.value
3471
+ });
3472
+ continue;
3473
+ }
3474
+ const before = published.get(key);
3475
+ if (!isEqual(before, entry.value)) updatedEntries.push({
3476
+ key,
3477
+ before,
3478
+ after: entry.value
3479
+ });
3217
3480
  else if (!nextReset) staged.delete(key);
3218
- } else if (existed) removed.add(key);
3219
- else staged.delete(key);
3481
+ continue;
3482
+ }
3483
+ if (existed) {
3484
+ removed.add(key);
3485
+ removedEntries.push({
3486
+ key,
3487
+ before: published.get(key)
3488
+ });
3489
+ } else staged.delete(key);
3220
3490
  }
3221
3491
  const ids = published.ids();
3222
3492
  if (explicitOrder || added.size || removed.size) {
@@ -3224,35 +3494,7 @@ const createCollectionOutput = (binding) => {
3224
3494
  if (new Set(order).size !== order.length || order.length !== published.size() + added.size - removed.size || order.some((key) => !hasNext(key))) throw new TypeError("Projection order must contain every key exactly once.");
3225
3495
  nextIds = ids.length === order.length && ids.every((key, index) => key === order[index]) ? ids : Object.freeze([...order]);
3226
3496
  } else nextIds = ids;
3227
- const changedKeys = /* @__PURE__ */ new Set();
3228
- added.forEach((key) => changedKeys.add(key));
3229
- removed.forEach((key) => changedKeys.add(key));
3230
- updated.forEach((key) => changedKeys.add(key));
3231
- profile.projection("changedKeys", changedKeys.size);
3232
- const addedEntries = [];
3233
- const updatedEntries = [];
3234
- const removedEntries = [];
3235
- for (const key of changedKeys) {
3236
- const beforePresent = published.has(key);
3237
- const afterPresent = hasNext(key);
3238
- if (beforePresent === afterPresent) {
3239
- if (!beforePresent) continue;
3240
- if (isEqual !== Object.is && isEqual(published.get(key), getNext(key))) continue;
3241
- }
3242
- if (!beforePresent) addedEntries.push({
3243
- key,
3244
- after: getNext(key)
3245
- });
3246
- else if (!afterPresent) removedEntries.push({
3247
- key,
3248
- before: published.get(key)
3249
- });
3250
- else updatedEntries.push({
3251
- key,
3252
- before: published.get(key),
3253
- after: getNext(key)
3254
- });
3255
- }
3497
+ profile.projection("changedKeys", addedEntries.length + updatedEntries.length + removedEntries.length);
3256
3498
  change = nextReset ? Object.freeze({ kind: "reset" }) : createCollectionChange({
3257
3499
  added: addedEntries,
3258
3500
  updated: updatedEntries,
@@ -3301,7 +3543,7 @@ const createCollectionOutput = (binding) => {
3301
3543
  initialized = true;
3302
3544
  },
3303
3545
  emit: (call) => {
3304
- Array.from(listeners).forEach((listener) => call(listener));
3546
+ Array.from(listeners).forEach((listener) => call(listener, change));
3305
3547
  },
3306
3548
  hasConsumers: () => consumers.size > 0,
3307
3549
  forEachConsumer: (run) => consumers.forEach(run),
@@ -3411,7 +3653,7 @@ const createValueOutput = (binding) => {
3411
3653
  },
3412
3654
  emit: (call) => {
3413
3655
  profile.materialized.notification();
3414
- Array.from(listeners).forEach((listener) => call(listener));
3656
+ Array.from(listeners).forEach((listener) => call(listener, void 0));
3415
3657
  },
3416
3658
  hasConsumers: () => consumers.size > 0,
3417
3659
  forEachConsumer: (run) => consumers.forEach(run),
@@ -3566,6 +3808,20 @@ const createProcessor = (scheduler, definition, dependencies) => {
3566
3808
  return processor;
3567
3809
  };
3568
3810
  //#endregion
3811
+ //#region core/src/projection/readable/listeners.ts
3812
+ /** Exhaustively notifies one projection-readable listener snapshot. */
3813
+ const notifyProjectionListeners = (listeners, message) => {
3814
+ const snapshot = [...listeners];
3815
+ const failures = [];
3816
+ for (const listener of snapshot) try {
3817
+ listener();
3818
+ } catch (error) {
3819
+ failures.push(error);
3820
+ }
3821
+ if (failures.length === 1) throw failures[0];
3822
+ if (failures.length) throw new AggregateError(failures, message);
3823
+ };
3824
+ //#endregion
3569
3825
  //#region core/src/projection/readable/selection.ts
3570
3826
  let activeTracker;
3571
3827
  const trackKey = (key) => {
@@ -3617,10 +3873,9 @@ const trackedMap = (source) => {
3617
3873
  };
3618
3874
  const selectionAffects = (selection, change) => {
3619
3875
  if (!change || change.kind === "reset") return true;
3620
- if (selection.all && collectionHasAnyChange(change)) return true;
3876
+ if (selection.all) return true;
3621
3877
  if (selection.structure && collectionHasStructuralChange(change)) return true;
3622
- const changedKeys = collectionChangedKeys(change);
3623
- for (const key of selection.keys) if (changedKeys.has(key)) return true;
3878
+ for (const key of collectionChange.keys(change)) if (selection.keys.has(key)) return true;
3624
3879
  return false;
3625
3880
  };
3626
3881
  const createDirectReadable = (source) => Object.freeze({
@@ -3701,7 +3956,7 @@ const createSelectorReadable = (source, selector, equality = Object.is) => {
3701
3956
  updateCollectionSnapshot();
3702
3957
  return;
3703
3958
  }
3704
- if (evaluate()) Array.from(listeners).forEach((listener) => listener());
3959
+ if (evaluate()) notifyProjectionListeners(listeners, "Projection selector listeners failed.");
3705
3960
  };
3706
3961
  const installSource = () => {
3707
3962
  if (!unsubscribeSource) unsubscribeSource = source.subscribe(onProjectionChange);
@@ -4162,7 +4417,7 @@ const stageCollectionRead = (state, read, active, options) => {
4162
4417
  evaluation.output.order(ids);
4163
4418
  } else {
4164
4419
  const previous = state.current();
4165
- const keys = options.candidates ? [...new Set(options.candidates)] : [...new Set([...previous.ids(), ...read.ids()])];
4420
+ const keys = options.candidates ? [...options.candidates] : [...new Set([...previous.ids(), ...read.ids()])];
4166
4421
  if (!options.candidates) profile.collectionView.idsScanned(keys.length);
4167
4422
  for (const key of keys) if (read.has(key)) {
4168
4423
  profile.collectionView.mapped();
@@ -4851,6 +5106,7 @@ const materializeInputSource = (scheduler, definition) => {
4851
5106
  scheduler.assertIdle();
4852
5107
  const staged = /* @__PURE__ */ new Map();
4853
5108
  const operations = [];
5109
+ let structural = false;
4854
5110
  let active = true;
4855
5111
  const draft = Object.freeze({
4856
5112
  get: (key) => {
@@ -4866,6 +5122,8 @@ const materializeInputSource = (scheduler, definition) => {
4866
5122
  set: (key, value) => {
4867
5123
  assertScope(() => active);
4868
5124
  if (typeof key !== "string") throw new TypeError("Projection keys must be strings.");
5125
+ const previous = staged.get(key);
5126
+ structural ||= previous ? !previous.present : !values.has(key);
4869
5127
  staged.set(key, {
4870
5128
  present: true,
4871
5129
  value
@@ -4879,8 +5137,11 @@ const materializeInputSource = (scheduler, definition) => {
4879
5137
  remove: (key) => {
4880
5138
  assertScope(() => active);
4881
5139
  if (typeof key !== "string") throw new TypeError("Projection keys must be strings.");
5140
+ const previous = staged.get(key);
5141
+ const present = previous ? previous.present : values.has(key);
5142
+ structural ||= present;
4882
5143
  staged.set(key, { present: false });
4883
- operations.push({
5144
+ if (present) operations.push({
4884
5145
  kind: "remove",
4885
5146
  key
4886
5147
  });
@@ -4891,32 +5152,25 @@ const materializeInputSource = (scheduler, definition) => {
4891
5152
  } finally {
4892
5153
  active = false;
4893
5154
  }
5155
+ if (!operations.length) return;
4894
5156
  const candidates = /* @__PURE__ */ new Set();
4895
- let structural = false;
4896
- const accepted = [];
4897
- const overlay = /* @__PURE__ */ new Map();
4898
- for (const operation of operations) {
4899
- const overlayValue = overlay.get(operation.key);
4900
- const present = overlayValue ? overlayValue.present : values.has(operation.key);
4901
- if (operation.kind === "set") {
4902
- const previous = overlayValue ? overlayValue.present ? overlayValue.value : void 0 : values.get(operation.key);
4903
- if (present && source.equality(previous, operation.value)) continue;
4904
- structural ||= !present;
4905
- overlay.set(operation.key, {
4906
- present: true,
4907
- value: operation.value
4908
- });
4909
- } else {
4910
- if (!present) continue;
4911
- structural = true;
4912
- overlay.set(operation.key, { present: false });
4913
- }
4914
- accepted.push(operation);
4915
- candidates.add(operation.key);
5157
+ const retained = /* @__PURE__ */ new Map();
5158
+ for (const [key, entry] of staged) {
5159
+ const existed = values.has(key);
5160
+ if (entry.present) {
5161
+ if (!existed) {
5162
+ candidates.add(key);
5163
+ continue;
5164
+ }
5165
+ const previous = values.get(key);
5166
+ if (source.equality(previous, entry.value)) retained.set(key, previous);
5167
+ else candidates.add(key);
5168
+ } else if (existed) candidates.add(key);
4916
5169
  }
4917
- if (!accepted.length) return;
4918
- for (const operation of accepted) if (operation.kind === "set") values.set(operation.key, operation.value);
5170
+ if (!candidates.size && !structural) return;
5171
+ for (const operation of operations) if (operation.kind === "set") values.set(operation.key, operation.value);
4919
5172
  else values.delete(operation.key);
5173
+ for (const [key, value] of retained) if (values.has(key)) values.set(key, value);
4920
5174
  boundary.mark({
4921
5175
  candidates,
4922
5176
  orderMayChange: structural
@@ -4959,6 +5213,212 @@ const createSourceRegistry = (scheduler) => {
4959
5213
  };
4960
5214
  };
4961
5215
  //#endregion
5216
+ //#region core/src/projection/readable/keyed.ts
5217
+ const sameKeys = (previous, next) => previous.length === next.length && previous.every((key, index) => key === next[index]);
5218
+ /** Runtime-owned keyed consumer family over one materialized collection output. */
5219
+ const createProjectionItems = (source, checkOwner) => {
5220
+ if (source.kind !== "collection") throw new TypeError("Projection items require a keyed collection projection.");
5221
+ let disposed = false;
5222
+ const active = /* @__PURE__ */ new Map();
5223
+ const followers = /* @__PURE__ */ new Map();
5224
+ const waiting = /* @__PURE__ */ new Map();
5225
+ let keysValue = Object.freeze([...source.current().keys()]);
5226
+ let keysRevision = 0;
5227
+ const keyListeners = /* @__PURE__ */ new Set();
5228
+ const check = () => {
5229
+ if (disposed) throw new ProjectionDisposedError();
5230
+ checkOwner();
5231
+ };
5232
+ const notifications = [];
5233
+ const notify = (listeners) => {
5234
+ for (const listener of listeners) notifications.push(listener);
5235
+ };
5236
+ const flushNotifications = () => {
5237
+ notifyProjectionListeners(notifications.splice(0), "Projection item listeners failed.");
5238
+ };
5239
+ const unregisterWaiting = (state) => {
5240
+ const states = waiting.get(state.key);
5241
+ states?.delete(state);
5242
+ if (states?.size === 0) waiting.delete(state.key);
5243
+ };
5244
+ const registerWaiting = (state) => {
5245
+ if (state.tracked || state.ended) return;
5246
+ const states = waiting.get(state.key);
5247
+ if (states) states.add(state);
5248
+ else waiting.set(state.key, new Set([state]));
5249
+ };
5250
+ const publishState = (state, value) => {
5251
+ if (state.ended) return;
5252
+ if (Object.is(state.value, value)) {
5253
+ state.value = value;
5254
+ return;
5255
+ }
5256
+ state.value = value;
5257
+ state.revision++;
5258
+ notify(state.listeners);
5259
+ };
5260
+ const attachActive = (state, value) => {
5261
+ if (state.ended) return;
5262
+ unregisterWaiting(state);
5263
+ state.tracked = true;
5264
+ const canonical = active.get(state.key);
5265
+ if (!canonical) active.set(state.key, state);
5266
+ else if (canonical !== state) {
5267
+ const states = followers.get(state.key);
5268
+ if (states) states.add(state);
5269
+ else followers.set(state.key, new Set([state]));
5270
+ }
5271
+ publishState(state, value);
5272
+ };
5273
+ const terminateState = (state) => {
5274
+ if (state.ended) return;
5275
+ unregisterWaiting(state);
5276
+ state.tracked = false;
5277
+ publishState(state, void 0);
5278
+ state.ended = true;
5279
+ };
5280
+ const statesFor = (key) => {
5281
+ const result = [];
5282
+ const canonical = active.get(key);
5283
+ if (canonical) result.push(canonical);
5284
+ const extra = followers.get(key);
5285
+ if (extra) result.push(...extra);
5286
+ return result;
5287
+ };
5288
+ const terminateKey = (key) => {
5289
+ for (const state of statesFor(key)) terminateState(state);
5290
+ active.delete(key);
5291
+ followers.delete(key);
5292
+ };
5293
+ const activateWaiting = (key, value) => {
5294
+ const states = waiting.get(key);
5295
+ if (!states?.size) return;
5296
+ waiting.delete(key);
5297
+ for (const state of states) attachActive(state, value);
5298
+ };
5299
+ const updateKey = (key, value) => {
5300
+ for (const state of statesFor(key)) publishState(state, value);
5301
+ };
5302
+ const updateKeys = (current) => {
5303
+ const next = Object.freeze([...current.keys()]);
5304
+ if (sameKeys(keysValue, next)) return;
5305
+ keysValue = next;
5306
+ keysRevision++;
5307
+ notify(keyListeners);
5308
+ };
5309
+ const createItem = (key, initial) => {
5310
+ const state = {
5311
+ key,
5312
+ listeners: /* @__PURE__ */ new Set(),
5313
+ tracked: initial !== void 0,
5314
+ ended: false,
5315
+ value: initial?.value,
5316
+ revision: 0
5317
+ };
5318
+ const ensureObserved = () => {
5319
+ if (state.ended || state.tracked) return;
5320
+ const current = source.current();
5321
+ if (current.has(key)) attachActive(state, current.get(key));
5322
+ };
5323
+ state.readable = Object.freeze({
5324
+ current: () => {
5325
+ check();
5326
+ ensureObserved();
5327
+ return state.value;
5328
+ },
5329
+ revision: () => {
5330
+ check();
5331
+ ensureObserved();
5332
+ return state.revision;
5333
+ },
5334
+ subscribe: (listener) => {
5335
+ check();
5336
+ ensureObserved();
5337
+ state.listeners.add(listener);
5338
+ if (!state.tracked && !state.ended) registerWaiting(state);
5339
+ return () => {
5340
+ state.listeners.delete(listener);
5341
+ if (!state.listeners.size && !state.tracked) unregisterWaiting(state);
5342
+ };
5343
+ }
5344
+ });
5345
+ return state;
5346
+ };
5347
+ const reconcile = (current) => {
5348
+ for (const key of [...active.keys()]) if (!current.has(key)) terminateKey(key);
5349
+ else updateKey(key, current.get(key));
5350
+ for (const key of [...waiting.keys()]) if (current.has(key)) activateWaiting(key, current.get(key));
5351
+ updateKeys(current);
5352
+ };
5353
+ const onChange = (change) => {
5354
+ check();
5355
+ const current = source.current();
5356
+ if (!change || change.kind === "reset") {
5357
+ reconcile(current);
5358
+ flushNotifications();
5359
+ return;
5360
+ }
5361
+ for (const entry of change.removed) terminateKey(entry.key);
5362
+ for (const entry of change.added) activateWaiting(entry.key, entry.after);
5363
+ for (const entry of change.updated) updateKey(entry.key, entry.after);
5364
+ if (change.added.length || change.removed.length || change.order) updateKeys(current);
5365
+ flushNotifications();
5366
+ };
5367
+ const stop = source.subscribe(onChange);
5368
+ const keys = Object.freeze({
5369
+ current: () => {
5370
+ check();
5371
+ return keysValue;
5372
+ },
5373
+ revision: () => {
5374
+ check();
5375
+ return keysRevision;
5376
+ },
5377
+ subscribe: (listener) => {
5378
+ check();
5379
+ keyListeners.add(listener);
5380
+ return () => keyListeners.delete(listener);
5381
+ }
5382
+ });
5383
+ return {
5384
+ items: Object.freeze({
5385
+ keys,
5386
+ get: (key) => {
5387
+ check();
5388
+ const canonical = active.get(key);
5389
+ if (canonical) return canonical.readable;
5390
+ const pending = waiting.get(key)?.values().next().value;
5391
+ if (pending) return pending.readable;
5392
+ const current = source.current();
5393
+ if (current.has(key)) {
5394
+ const state = createItem(key, {
5395
+ present: true,
5396
+ value: current.get(key)
5397
+ });
5398
+ active.set(key, state);
5399
+ return state.readable;
5400
+ }
5401
+ return createItem(key).readable;
5402
+ }
5403
+ }),
5404
+ dispose() {
5405
+ if (disposed) return;
5406
+ disposed = true;
5407
+ stop();
5408
+ keyListeners.clear();
5409
+ const known = /* @__PURE__ */ new Set();
5410
+ active.forEach((state) => known.add(state));
5411
+ followers.forEach((states) => states.forEach((state) => known.add(state)));
5412
+ waiting.forEach((states) => states.forEach((state) => known.add(state)));
5413
+ known.forEach((state) => state.listeners.clear());
5414
+ active.clear();
5415
+ followers.clear();
5416
+ waiting.clear();
5417
+ notifications.length = 0;
5418
+ }
5419
+ };
5420
+ };
5421
+ //#endregion
4962
5422
  //#region core/src/projection/runtime.ts
4963
5423
  const createProjectionRuntime = (options) => {
4964
5424
  const scheduler = createScheduler(options?.onError ?? (() => void 0));
@@ -4966,6 +5426,30 @@ const createProjectionRuntime = (options) => {
4966
5426
  const materialized = /* @__PURE__ */ new WeakMap();
4967
5427
  const publicViews = /* @__PURE__ */ new WeakMap();
4968
5428
  const scopes = /* @__PURE__ */ new Set();
5429
+ const rootItems = /* @__PURE__ */ new Map();
5430
+ const disposeItemControllers = (registry, failures) => {
5431
+ const controllers = [...registry.values()];
5432
+ registry.clear();
5433
+ for (const controller of controllers) try {
5434
+ controller.dispose();
5435
+ } catch (error) {
5436
+ failures.push(error);
5437
+ }
5438
+ };
5439
+ const disposeScopeConsumers = (state, failures) => {
5440
+ disposeItemControllers(state.items, failures);
5441
+ const subscriptions = [...state.subscriptions];
5442
+ state.subscriptions.clear();
5443
+ for (const unsubscribe of subscriptions) try {
5444
+ unsubscribe();
5445
+ } catch (error) {
5446
+ failures.push(error);
5447
+ }
5448
+ };
5449
+ const throwCleanupFailures = (failures, message) => {
5450
+ if (failures.length === 1) throw failures[0];
5451
+ if (failures.length) throw new AggregateError(failures, message);
5452
+ };
4969
5453
  const assertAccess = (definition, requester) => {
4970
5454
  scheduler.assertActive();
4971
5455
  const owner = definition.owner;
@@ -5051,6 +5535,19 @@ const createProjectionRuntime = (options) => {
5051
5535
  subscribe: output.subscribe
5052
5536
  });
5053
5537
  };
5538
+ const projectionItems = (projection, requester) => {
5539
+ const output = resolveOutput(projection, requester);
5540
+ if (output.kind !== "collection") throw new TypeError("Projection items require a keyed collection projection.");
5541
+ const registry = requester?.items ?? rootItems;
5542
+ const existing = registry.get(output);
5543
+ if (existing) return existing.items;
5544
+ const controller = createProjectionItems(readableSource(projection, requester), () => {
5545
+ if (requester && !requester.active) throw new ProjectionDisposedError();
5546
+ assertOutput(output);
5547
+ });
5548
+ registry.set(output, controller);
5549
+ return controller.items;
5550
+ };
5054
5551
  const makeReadable = (projection, selector, equality, requester) => {
5055
5552
  const source = readableSource(projection, requester);
5056
5553
  return selector ? createSelectorReadable(source, selector, equality) : createDirectReadable(source);
@@ -5084,13 +5581,7 @@ const createProjectionRuntime = (options) => {
5084
5581
  scheduler.assertIdle();
5085
5582
  state.active = false;
5086
5583
  const failures = [];
5087
- const subscriptions = [...state.subscriptions];
5088
- state.subscriptions.clear();
5089
- for (const unsubscribe of subscriptions) try {
5090
- unsubscribe();
5091
- } catch (error) {
5092
- failures.push(error);
5093
- }
5584
+ disposeScopeConsumers(state, failures);
5094
5585
  for (let index = state.materialized.length - 1; index >= 0; index--) {
5095
5586
  const definition = state.materialized[index];
5096
5587
  const current = materialized.get(definition);
@@ -5105,14 +5596,15 @@ const createProjectionRuntime = (options) => {
5105
5596
  }
5106
5597
  state.materialized.length = 0;
5107
5598
  scopes.delete(state);
5108
- if (failures.length) throw failures[0];
5599
+ throwCleanupFailures(failures, "Projection scope cleanup failed.");
5109
5600
  };
5110
5601
  const createScope = () => {
5111
5602
  scheduler.assertIdle();
5112
5603
  const state = {
5113
5604
  active: true,
5114
5605
  materialized: [],
5115
- subscriptions: /* @__PURE__ */ new Set()
5606
+ subscriptions: /* @__PURE__ */ new Set(),
5607
+ items: /* @__PURE__ */ new Map()
5116
5608
  };
5117
5609
  scopes.add(state);
5118
5610
  const assertActive = () => {
@@ -5184,6 +5676,10 @@ const createProjectionRuntime = (options) => {
5184
5676
  return readProjection(projection, state);
5185
5677
  },
5186
5678
  select: scopeSelect,
5679
+ items: (projection) => {
5680
+ assertActive();
5681
+ return projectionItems(projection, state);
5682
+ },
5187
5683
  update: scopeUpdate,
5188
5684
  batch: scopeBatch,
5189
5685
  dispose: () => disposeScope(state)
@@ -5192,6 +5688,7 @@ const createProjectionRuntime = (options) => {
5192
5688
  return Object.freeze({
5193
5689
  read: readProjection,
5194
5690
  select: selectProjection,
5691
+ items: projectionItems,
5195
5692
  update,
5196
5693
  batch,
5197
5694
  scope: createScope,
@@ -5201,16 +5698,11 @@ const createProjectionRuntime = (options) => {
5201
5698
  const failures = [];
5202
5699
  for (const scope of scopes) {
5203
5700
  scope.active = false;
5204
- const subscriptions = [...scope.subscriptions];
5205
- scope.subscriptions.clear();
5206
- for (const unsubscribe of subscriptions) try {
5207
- unsubscribe();
5208
- } catch (error) {
5209
- failures.push(error);
5210
- }
5701
+ disposeScopeConsumers(scope, failures);
5211
5702
  scope.materialized.length = 0;
5212
5703
  }
5213
5704
  scopes.clear();
5705
+ disposeItemControllers(rootItems, failures);
5214
5706
  try {
5215
5707
  sources.dispose();
5216
5708
  } catch (error) {
@@ -5221,8 +5713,7 @@ const createProjectionRuntime = (options) => {
5221
5713
  } catch (error) {
5222
5714
  failures.push(error);
5223
5715
  }
5224
- if (failures.length === 1) throw failures[0];
5225
- if (failures.length) throw new AggregateError(failures, "Projection cleanup failed.");
5716
+ throwCleanupFailures(failures, "Projection cleanup failed.");
5226
5717
  }
5227
5718
  });
5228
5719
  };