doxum 0.1.29 → 0.1.31

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
@@ -300,6 +300,8 @@ const ids = derive.keyed.keys(records);
300
300
  const all = derive.keyed.values(records);
301
301
  const entries = derive.keyed.entries(records);
302
302
  const active = derive.keyed.get(records, activeRecordId);
303
+ const rowsFromArray = derive.keyed.from(rowArray, row => row.id);
304
+ const effectiveRows = derive.keyed.merge([baseRows, rowOverrides], { conflict: 'last' });
303
305
  const visible = derive.keyed.filter(records, record => record.visible);
304
306
  const orderedVisible = derive.keyed.subset(records, visibleIds);
305
307
  const definedContent = derive.keyed.compact(records, record => record.content);
@@ -318,10 +320,26 @@ source-relative order. `compact` omits keys whose selected value is `undefined`
318
320
  uses its optional equality for present values. `filter` and `compact` support the same
319
321
  named global/dynamic keyed dependency object as ordinary `derive.keyed`.
320
322
 
323
+ `from` turns either a static readonly array or `Projection<readonly V[]>` into a keyed
324
+ projection using `keyOf(value)`. Input order becomes formal keyed order; duplicate keys
325
+ are errors rather than implicit overwrites. Static outer arrays are snapshotted at
326
+ definition creation, while key selection remains lazy. Same-key equality-equivalent
327
+ values retain the previous published identity.
328
+
329
+ `merge` combines a fixed list of keyed sources with union membership and an explicit
330
+ `error`, `first`, `last`, or `resolve` conflict policy. Its formal order is the stable
331
+ first occurrence from the sources' formal orders, independent of which source supplies
332
+ the effective value. `derive.keyed.merge([base, overrides], { conflict: 'last' })` is
333
+ therefore the standard sparse-override composition: shared override values win without
334
+ moving their base positions, override-only keys join the union, and removing an override
335
+ falls back to the base without breaking the key's merged membership lifecycle.
336
+
321
337
  `groupBy` is the general reverse-index primitive. A source entry may return one group
322
338
  key or several; each output bucket contains source keys in the source's formal order,
323
- and group order follows first appearance in that same order. `singleton` converts an
324
- optional scalar projection into a zero-or-one keyed projection.
339
+ and group keys are ordered by the earliest current source member that belongs to them.
340
+ If several groups first appear on the same source member, they follow that member's
341
+ selector-result order. Source membership/order changes may therefore reorder group keys.
342
+ `singleton` converts an optional scalar projection into a zero-or-one keyed projection.
325
343
 
326
344
  All keyed projection producers use the public `KeyedProjection<K,V>` handle, including
327
345
  collection `observe`, `derive.keyed`, `incremental.collection`, and collection leaves
@@ -599,8 +617,9 @@ Builds produce root `dist` ESM/CJS/declarations for `doxum`,
599
617
  The runtime shares one transaction lifecycle; complete mutation operations are
600
618
  organized by domain under `core/src/mutation/operations`, with access, first-touch
601
619
  recording and publication retaining their own responsibilities.
602
- The published [runtime skill](skills/doxum-runtime/SKILL.md) includes English and
603
- Chinese application guidance.
620
+ The published [runtime skill](skills/doxum-runtime/SKILL.md) is a source-free consumer
621
+ contract with complete public API routing, lifecycle guidance, recipes, integration
622
+ semantics and separate maintainer invariants.
604
623
 
605
624
  ## Release
606
625
 
package/dist/index.cjs CHANGED
@@ -2775,6 +2775,279 @@ const isExternalSource = (source) => {
2775
2775
  return (candidate.kind === "value" || candidate.kind === "collection") && typeof candidate.current === "function" && typeof candidate.revision === "function" && typeof candidate.subscribe === "function";
2776
2776
  };
2777
2777
  //#endregion
2778
+ //#region core/src/projection/keyed/composition.ts
2779
+ const assertEquality = (equality, role) => {
2780
+ if (equality !== void 0 && typeof equality !== "function") throw new TypeError(`${role} equality must be a function.`);
2781
+ return equality ?? Object.is;
2782
+ };
2783
+ const compileMergeOptions = (options) => {
2784
+ if (options === null || typeof options !== "object") throw new TypeError("derive.keyed.merge requires conflict options.");
2785
+ const equality = assertEquality(options.equality, "derive.keyed.merge");
2786
+ switch (options.conflict) {
2787
+ case "error":
2788
+ case "first":
2789
+ case "last":
2790
+ if ("resolve" in options && options.resolve !== void 0) throw new TypeError("derive.keyed.merge resolve is only valid when conflict is \"resolve\".");
2791
+ return Object.freeze({
2792
+ conflict: options.conflict,
2793
+ equality
2794
+ });
2795
+ case "resolve":
2796
+ if (typeof options.resolve !== "function") throw new TypeError("derive.keyed.merge requires a resolver when conflict is \"resolve\".");
2797
+ return Object.freeze({
2798
+ conflict: options.conflict,
2799
+ equality,
2800
+ resolve: options.resolve
2801
+ });
2802
+ default: throw new TypeError("derive.keyed.merge conflict must be \"error\", \"first\", \"last\", or \"resolve\".");
2803
+ }
2804
+ };
2805
+ const assertCollectionSources = (sources, expected) => {
2806
+ if (sources.length !== expected) throw new Error("derive.keyed.merge resolved an invalid dependency count.");
2807
+ for (const source of sources) if (source.kind !== "collection") throw new Error("derive.keyed.merge requires collection inputs.");
2808
+ };
2809
+ function createKeyedFrom(source, keyOf, equality) {
2810
+ if (typeof keyOf !== "function") throw new TypeError("derive.keyed.from requires a key selector.");
2811
+ const outputEquality = assertEquality(equality, "derive.keyed.from");
2812
+ let dependency;
2813
+ let staticValues;
2814
+ if (require_transform.isProjection(source)) {
2815
+ if (require_transform.outputKind(source) !== "value") throw new TypeError("derive.keyed.from source projection must be scalar.");
2816
+ dependency = source;
2817
+ } else {
2818
+ if (!Array.isArray(source)) throw new TypeError("derive.keyed.from source must be a scalar projection or readonly array.");
2819
+ staticValues = require_transform.snapshotArray(source);
2820
+ }
2821
+ const [projection] = require_transform.defineProcessor({
2822
+ dependencies: dependency ? [dependency] : [],
2823
+ outputs: [{
2824
+ kind: "collection",
2825
+ equality: outputEquality
2826
+ }],
2827
+ create: () => ({ evaluate: (evaluation) => {
2828
+ const output = evaluation.outputs[0];
2829
+ if (output.kind !== "collection") throw new Error("derive.keyed.from requires a collection output.");
2830
+ let values = staticValues;
2831
+ if (dependency) {
2832
+ const scalar = evaluation.sources[0];
2833
+ if (scalar?.kind !== "value") throw new Error("derive.keyed.from resolved a non-scalar input.");
2834
+ if (!Array.isArray(scalar.value)) throw new TypeError("derive.keyed.from source value must be an array.");
2835
+ values = scalar.value;
2836
+ }
2837
+ if (!values) throw new Error("derive.keyed.from has no source values.");
2838
+ const seen = /* @__PURE__ */ new Set();
2839
+ const order = new Array(values.length);
2840
+ for (let index = 0; index < values.length; index++) {
2841
+ const value = values[index];
2842
+ const key = keyOf(value);
2843
+ require_transform.assertSynchronous(key);
2844
+ if (typeof key !== "string") throw new TypeError("derive.keyed.from key selector must return a string.");
2845
+ if (seen.has(key)) throw new TypeError(`derive.keyed.from duplicate key ${JSON.stringify(key)}.`);
2846
+ seen.add(key);
2847
+ order[index] = key;
2848
+ output.output.set(key, value);
2849
+ }
2850
+ for (const previousKey of output.previous.ids()) if (!seen.has(previousKey)) output.output.remove(previousKey);
2851
+ output.output.order(order);
2852
+ } }),
2853
+ name: "derive.keyed.from"
2854
+ });
2855
+ return projection;
2856
+ }
2857
+ function createKeyedMerge(sources, options) {
2858
+ if (!Array.isArray(sources)) throw new TypeError("derive.keyed.merge sources must be an array of keyed projections.");
2859
+ const capturedSources = require_transform.snapshotArray(sources);
2860
+ for (let index = 0; index < capturedSources.length; index++) require_transform.assertKeyedProjection(capturedSources[index], `derive.keyed.merge source ${index}`);
2861
+ const compiled = compileMergeOptions(options);
2862
+ const [projection] = require_transform.defineProcessor({
2863
+ dependencies: capturedSources,
2864
+ outputs: [{
2865
+ kind: "collection",
2866
+ equality: compiled.equality
2867
+ }],
2868
+ create: () => {
2869
+ const revisions = new Array(capturedSources.length).fill(-1);
2870
+ const remember = (sources) => {
2871
+ for (let index = 0; index < revisions.length; index++) revisions[index] = sources[index].revision;
2872
+ };
2873
+ const writeEffective = (sources, output, key) => {
2874
+ if (compiled.conflict === "first") {
2875
+ for (let sourceIndex = 0; sourceIndex < sources.length; sourceIndex++) {
2876
+ const source = sources[sourceIndex];
2877
+ if (source.kind !== "collection") throw new Error("derive.keyed.merge requires collection inputs.");
2878
+ if (!source.read.has(key)) continue;
2879
+ output.set(key, source.read.get(key));
2880
+ return;
2881
+ }
2882
+ output.remove(key);
2883
+ return;
2884
+ }
2885
+ let present = false;
2886
+ let selected;
2887
+ let firstSourceIndex = -1;
2888
+ let contributions;
2889
+ for (let sourceIndex = 0; sourceIndex < sources.length; sourceIndex++) {
2890
+ const source = sources[sourceIndex];
2891
+ if (source.kind !== "collection") throw new Error("derive.keyed.merge requires collection inputs.");
2892
+ if (!source.read.has(key)) continue;
2893
+ const value = source.read.get(key);
2894
+ if (!present) {
2895
+ present = true;
2896
+ selected = value;
2897
+ firstSourceIndex = sourceIndex;
2898
+ continue;
2899
+ }
2900
+ if (compiled.conflict === "error") throw new TypeError(`derive.keyed.merge duplicate key ${JSON.stringify(key)}.`);
2901
+ if (compiled.conflict === "last") {
2902
+ selected = value;
2903
+ continue;
2904
+ }
2905
+ if (!contributions) contributions = [Object.freeze({
2906
+ sourceIndex: firstSourceIndex,
2907
+ value: selected
2908
+ }), Object.freeze({
2909
+ sourceIndex,
2910
+ value
2911
+ })];
2912
+ else contributions.push(Object.freeze({
2913
+ sourceIndex,
2914
+ value
2915
+ }));
2916
+ }
2917
+ if (!present) {
2918
+ output.remove(key);
2919
+ return;
2920
+ }
2921
+ if (compiled.conflict === "resolve" && contributions) {
2922
+ const resolved = compiled.resolve(Object.freeze(contributions), key);
2923
+ require_transform.assertSynchronous(resolved);
2924
+ output.set(key, resolved);
2925
+ return;
2926
+ }
2927
+ output.set(key, selected);
2928
+ };
2929
+ const rebuild = (sources, output, previousIds, reset) => {
2930
+ const seen = /* @__PURE__ */ new Set();
2931
+ const order = [];
2932
+ const conflicts = compiled.conflict === "resolve" ? /* @__PURE__ */ new Set() : void 0;
2933
+ for (let sourceIndex = 0; sourceIndex < sources.length; sourceIndex++) {
2934
+ const source = sources[sourceIndex];
2935
+ if (source.kind !== "collection") throw new Error("derive.keyed.merge requires collection inputs.");
2936
+ for (const key of source.read.ids()) {
2937
+ const duplicate = seen.has(key);
2938
+ if (!duplicate) {
2939
+ seen.add(key);
2940
+ order.push(key);
2941
+ }
2942
+ if (compiled.conflict === "error") {
2943
+ if (duplicate) throw new TypeError(`derive.keyed.merge duplicate key ${JSON.stringify(key)}.`);
2944
+ output.set(key, source.read.get(key));
2945
+ continue;
2946
+ }
2947
+ if (compiled.conflict === "first") {
2948
+ if (!duplicate) output.set(key, source.read.get(key));
2949
+ continue;
2950
+ }
2951
+ if (compiled.conflict === "last") {
2952
+ output.set(key, source.read.get(key));
2953
+ continue;
2954
+ }
2955
+ if (!duplicate) output.set(key, source.read.get(key));
2956
+ else conflicts.add(key);
2957
+ }
2958
+ }
2959
+ if (conflicts) for (const key of conflicts) writeEffective(sources, output, key);
2960
+ if (!reset) {
2961
+ for (const previousKey of previousIds) if (!seen.has(previousKey)) output.remove(previousKey);
2962
+ }
2963
+ output.order(order);
2964
+ };
2965
+ const rebuildOrder = (sources, output) => {
2966
+ const seen = /* @__PURE__ */ new Set();
2967
+ const order = [];
2968
+ for (const source of sources) {
2969
+ if (source.kind !== "collection") throw new Error("derive.keyed.merge requires collection inputs.");
2970
+ for (const key of source.read.ids()) {
2971
+ if (seen.has(key)) continue;
2972
+ seen.add(key);
2973
+ order.push(key);
2974
+ }
2975
+ }
2976
+ output.order(order);
2977
+ };
2978
+ return {
2979
+ evaluate: (evaluation) => {
2980
+ const output = evaluation.outputs[0];
2981
+ if (output.kind !== "collection") throw new Error("derive.keyed.merge requires a collection output.");
2982
+ assertCollectionSources(evaluation.sources, capturedSources.length);
2983
+ if (evaluation.reset) {
2984
+ rebuild(evaluation.sources, output.output, output.previous.ids(), true);
2985
+ remember(evaluation.sources);
2986
+ return;
2987
+ }
2988
+ const affected = /* @__PURE__ */ new Set();
2989
+ let fullRebuild = false;
2990
+ let orderDirty = false;
2991
+ for (let sourceIndex = 0; sourceIndex < evaluation.sources.length; sourceIndex++) {
2992
+ const source = evaluation.sources[sourceIndex];
2993
+ if (revisions[sourceIndex] === source.revision) continue;
2994
+ if (source.kind !== "collection") throw new Error("derive.keyed.merge requires collection inputs.");
2995
+ const change = source.change;
2996
+ if (!change || change.kind === "reset") {
2997
+ fullRebuild = true;
2998
+ continue;
2999
+ }
3000
+ for (const entry of change.added) affected.add(entry.key);
3001
+ for (const entry of change.updated) affected.add(entry.key);
3002
+ for (const entry of change.removed) affected.add(entry.key);
3003
+ if (Boolean(change.added.length || change.removed.length || change.order)) orderDirty = true;
3004
+ }
3005
+ if (fullRebuild) {
3006
+ rebuild(evaluation.sources, output.output, output.previous.ids(), false);
3007
+ remember(evaluation.sources);
3008
+ return;
3009
+ }
3010
+ for (const key of affected) writeEffective(evaluation.sources, output.output, key);
3011
+ if (orderDirty) rebuildOrder(evaluation.sources, output.output);
3012
+ remember(evaluation.sources);
3013
+ },
3014
+ release: () => revisions.fill(-1)
3015
+ };
3016
+ },
3017
+ name: "derive.keyed.merge"
3018
+ });
3019
+ return projection;
3020
+ }
3021
+ const createKeyedSingleton = (source, keyOf, equality = Object.is) => {
3022
+ if (require_transform.outputKind(source) !== "value") throw new TypeError("derive.keyed.singleton source must be a scalar projection.");
3023
+ if (typeof keyOf !== "function") throw new TypeError("derive.keyed.singleton requires a key selector.");
3024
+ const [projection] = require_transform.defineProcessor({
3025
+ dependencies: [source],
3026
+ outputs: [{
3027
+ kind: "collection",
3028
+ equality
3029
+ }],
3030
+ create: () => ({ evaluate: (evaluation) => {
3031
+ const scalar = evaluation.sources[0];
3032
+ const output = evaluation.outputs[0];
3033
+ if (scalar.kind !== "value" || output.kind !== "collection") throw new Error("derive.keyed.singleton resolved invalid input/output kinds.");
3034
+ const previous = output.previous.ids();
3035
+ if (scalar.value === void 0) {
3036
+ for (const key of previous) output.output.remove(key);
3037
+ return;
3038
+ }
3039
+ const key = keyOf(scalar.value);
3040
+ require_transform.assertSynchronous(key);
3041
+ if (typeof key !== "string") throw new TypeError("derive.keyed.singleton key selector must return a string.");
3042
+ for (const previousKey of previous) if (previousKey !== key) output.output.remove(previousKey);
3043
+ output.output.set(key, scalar.value);
3044
+ if (previous.length !== 1 || previous[0] !== key) output.output.order([key]);
3045
+ } }),
3046
+ name: "derive.keyed.singleton"
3047
+ });
3048
+ return projection;
3049
+ };
3050
+ //#endregion
2778
3051
  //#region core/src/projection/derive/keyed.ts
2779
3052
  const compileOrderedKeys = (value, role) => {
2780
3053
  if (!Array.isArray(value)) throw new TypeError(`${role} must be an array of string keys.`);
@@ -3187,7 +3460,8 @@ function createKeyedGroupBy(source, dependenciesOrSelector, maybeSelector) {
3187
3460
  return;
3188
3461
  } else {
3189
3462
  for (const entry of change.removed) {
3190
- orderDirty ||= removeSourceKey(entry.key, affected);
3463
+ const changed = removeSourceKey(entry.key, affected);
3464
+ orderDirty ||= changed;
3191
3465
  dependencies.remove(entry.key);
3192
3466
  dirty.delete(entry.key);
3193
3467
  }
@@ -3200,7 +3474,10 @@ function createKeyedGroupBy(source, dependenciesOrSelector, maybeSelector) {
3200
3474
  }
3201
3475
  all ||= dependencies.collectInvalidated(evaluation.sources, dirty);
3202
3476
  if (all) for (const key of driver.read.ids()) dirty.add(key);
3203
- for (const key of dirty) orderDirty ||= selectGroups(driver, evaluation.sources, key, affected);
3477
+ for (const key of dirty) {
3478
+ const changed = selectGroups(driver, evaluation.sources, key, affected);
3479
+ orderDirty ||= changed;
3480
+ }
3204
3481
  if (structural) markAllGroups(output, affected);
3205
3482
  publishAffected(affected, output, orderDirty);
3206
3483
  dependencies.remember(evaluation.sources);
@@ -3217,40 +3494,13 @@ function createKeyedGroupBy(source, dependenciesOrSelector, maybeSelector) {
3217
3494
  });
3218
3495
  return projection;
3219
3496
  }
3220
- const createKeyedSingleton = (source, keyOf, equality = Object.is) => {
3221
- if (require_transform.outputKind(source) !== "value") throw new TypeError("derive.keyed.singleton source must be a scalar projection.");
3222
- if (typeof keyOf !== "function") throw new TypeError("derive.keyed.singleton requires a key selector.");
3223
- const [projection] = require_transform.defineProcessor({
3224
- dependencies: [source],
3225
- outputs: [{
3226
- kind: "collection",
3227
- equality
3228
- }],
3229
- create: () => ({ evaluate: (evaluation) => {
3230
- const scalar = evaluation.sources[0];
3231
- const output = evaluation.outputs[0];
3232
- if (scalar.kind !== "value" || output.kind !== "collection") throw new Error("derive.keyed.singleton resolved invalid input/output kinds.");
3233
- const previous = output.previous.ids();
3234
- if (scalar.value === void 0) {
3235
- for (const key of previous) output.output.remove(key);
3236
- return;
3237
- }
3238
- const key = keyOf(scalar.value);
3239
- require_transform.assertSynchronous(key);
3240
- if (typeof key !== "string") throw new TypeError("derive.keyed.singleton key selector must return a string.");
3241
- for (const previousKey of previous) if (previousKey !== key) output.output.remove(previousKey);
3242
- output.output.set(key, scalar.value);
3243
- if (previous.length !== 1 || previous[0] !== key) output.output.order([key]);
3244
- } }),
3245
- name: "derive.keyed.singleton"
3246
- });
3247
- return projection;
3248
- };
3249
3497
  const keyedDerive = Object.assign(createKeyedDerive, {
3250
3498
  keys: createKeyedKeys,
3251
3499
  values: createKeyedValues,
3252
3500
  entries: createKeyedEntries,
3253
3501
  get: createKeyedGet,
3502
+ from: createKeyedFrom,
3503
+ merge: createKeyedMerge,
3254
3504
  groupBy: createKeyedGroupBy,
3255
3505
  singleton: createKeyedSingleton,
3256
3506
  subset: createKeyedSubset,