doxum 0.1.36 → 0.1.38

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
@@ -362,6 +362,20 @@ and group keys are ordered by the earliest current source member that belongs to
362
362
  If several groups first appear on the same source member, they follow that member's
363
363
  selector-result order. Source membership/order changes may therefore reorder group keys.
364
364
  `singleton` converts an optional scalar projection into a zero-or-one keyed projection.
365
+ It also computes an optional member directly from named dependencies:
366
+
367
+ ```ts
368
+ const preview = derive.keyed.singleton(
369
+ { record: activeRecord, enabled: previewEnabled },
370
+ ({ record, enabled }) => (!enabled || record === undefined ? undefined : [record.id, record])
371
+ );
372
+ ```
373
+
374
+ The named form returns one readonly `[key, value]` tuple or `undefined` for no member.
375
+ `[key, undefined]` is a present member. Optional equality compares values under the
376
+ same key and preserves equivalent references; it cannot suppress a key change.
377
+ Use `singleton` for zero or one result, and `fromEntries` for a complete result with
378
+ arbitrary membership. See [Singleton](docs/projections.md#zero-or-one-keyed-member).
365
379
 
366
380
  All keyed projection producers use the public `KeyedProjection<K,V>` handle, including
367
381
  collection `observe`, `derive.keyed`, `incremental.collection`, and collection leaves
package/dist/index.cjs CHANGED
@@ -3158,35 +3158,57 @@ function createKeyedMerge(sources, options) {
3158
3158
  });
3159
3159
  return projection;
3160
3160
  }
3161
- const createKeyedSingleton = (source, keyOf, equality = Object.is) => {
3162
- if (require_transform.outputKind(source) !== "value") throw new TypeError("derive.keyed.singleton source must be a scalar projection.");
3163
- if (typeof keyOf !== "function") throw new TypeError("derive.keyed.singleton requires a key selector.");
3161
+ function createKeyedSingleton(sourceOrDependencies, compute, equality) {
3162
+ const name = "derive.keyed.singleton";
3163
+ const selection = require_transform.isProjection(sourceOrDependencies) ? {
3164
+ kind: "scalar",
3165
+ source: sourceOrDependencies,
3166
+ keyOf: compute
3167
+ } : {
3168
+ kind: "named",
3169
+ compiled: require_transform.compileProjectionDependencies(sourceOrDependencies, name),
3170
+ computeEntry: compute
3171
+ };
3172
+ if (selection.kind === "scalar" && require_transform.outputKind(selection.source) !== "value") throw new TypeError(`${name} source must be a scalar projection.`);
3173
+ if (typeof compute !== "function") throw new TypeError(`${name} requires a ${selection.kind === "scalar" ? "key selector" : "compute callback"}.`);
3174
+ const outputEquality = assertEquality(equality, name);
3164
3175
  const [projection] = require_transform.defineProcessor({
3165
- dependencies: [source],
3176
+ dependencies: selection.kind === "named" ? selection.compiled.projections : [selection.source],
3166
3177
  outputs: [{
3167
3178
  kind: "collection",
3168
- equality
3179
+ equality: outputEquality
3169
3180
  }],
3170
3181
  create: () => ({ evaluate: (evaluation) => {
3171
- const scalar = evaluation.sources[0];
3172
3182
  const output = evaluation.outputs[0];
3173
- if (scalar.kind !== "value" || output.kind !== "collection") throw new Error("derive.keyed.singleton resolved invalid input/output kinds.");
3174
- const previous = output.previous.ids();
3175
- if (scalar.value === void 0) {
3176
- for (const key of previous) output.output.remove(key);
3177
- return;
3183
+ if (output.kind !== "collection") throw new Error(`${name} requires a collection output.`);
3184
+ let key;
3185
+ let value;
3186
+ if (selection.kind === "named") {
3187
+ const entry = selection.computeEntry(require_transform.snapshotProjectionValues(selection.compiled.names, evaluation.sources));
3188
+ require_transform.assertSynchronous(entry);
3189
+ if (entry !== void 0) {
3190
+ assertKeyedEntry(entry, name);
3191
+ key = entry[0];
3192
+ value = entry[1];
3193
+ }
3194
+ } else {
3195
+ const scalar = evaluation.sources[0];
3196
+ if (scalar?.kind !== "value") throw new Error(`${name} resolved a non-scalar input.`);
3197
+ value = scalar.value;
3198
+ if (value !== void 0) {
3199
+ key = selection.keyOf(value);
3200
+ require_transform.assertSynchronous(key);
3201
+ if (typeof key !== "string") throw new TypeError(`${name} key selector must return a string.`);
3202
+ }
3178
3203
  }
3179
- const key = keyOf(scalar.value);
3180
- require_transform.assertSynchronous(key);
3181
- if (typeof key !== "string") throw new TypeError("derive.keyed.singleton key selector must return a string.");
3182
- for (const previousKey of previous) if (previousKey !== key) output.output.remove(previousKey);
3183
- output.output.set(key, scalar.value);
3184
- if (previous.length !== 1 || previous[0] !== key) output.output.order([key]);
3204
+ const previousKey = output.previous.ids()[0];
3205
+ if (previousKey !== void 0 && previousKey !== key) output.output.remove(previousKey);
3206
+ if (key !== void 0) output.output.set(key, value);
3185
3207
  } }),
3186
- name: "derive.keyed.singleton"
3208
+ name
3187
3209
  });
3188
3210
  return projection;
3189
- };
3211
+ }
3190
3212
  //#endregion
3191
3213
  //#region core/src/projection/derive/keyed.ts
3192
3214
  const compileOrderedKeys = (value, role) => {
package/dist/index.d.cts CHANGED
@@ -40,7 +40,7 @@ type KeyedEntries<K extends string, V> = readonly (readonly [K, V])[];
40
40
  type Equality$2<T> = (previous: T, next: T) => boolean;
41
41
  declare function createKeyedFrom<K extends string, V>(source: Projection<readonly V[]>, keyOf: (value: NoInfer<V>) => Synchronous<K>, equality?: Equality$2<V>): KeyedProjection<K, V>;
42
42
  declare function createKeyedFrom<K extends string, V>(source: readonly V[], keyOf: (value: NoInfer<V>) => Synchronous<K>, equality?: Equality$2<V>): KeyedProjection<K, V>;
43
- declare function createKeyedFromEntries<const D extends ProjectionDependencies, K extends string, V>(dependencies: D, compute: (values: ProjectionValues<D>) => Synchronous<KeyedEntries<K, V>>, equality?: Equality$2<NoInfer<V>>): KeyedProjection<K, V>;
43
+ declare function createKeyedFromEntries<const D extends ProjectionDependencies, K extends string, V>(dependencies: D, compute: (values: ProjectionValues<D>) => Synchronous<KeyedEntries<K, V>>, equality?: Equality$2<V>): KeyedProjection<K, V>;
44
44
  declare function createKeyedMerge<K extends string, V>(sources: readonly KeyedProjection<K, V>[], options: {
45
45
  readonly conflict: 'error' | 'first' | 'last';
46
46
  readonly equality?: Equality$2<V>;
@@ -53,6 +53,8 @@ declare function createKeyedMerge<K extends string, V>(sources: readonly KeyedPr
53
53
  }[], key: NoInfer<K>) => Synchronous<V>;
54
54
  readonly equality?: Equality$2<V>;
55
55
  }): KeyedProjection<K, V>;
56
+ declare function createKeyedSingleton<const K extends string, V>(source: Projection<V | undefined>, keyOf: (value: V) => K, equality?: Equality$2<V>): KeyedProjection<K, V>;
57
+ declare function createKeyedSingleton<const D extends ProjectionDependencies, K extends string, V>(dependencies: D, computeEntry: (values: ProjectionValues<D>) => Synchronous<readonly [K, V] | undefined>, equality?: Equality$2<V>): KeyedProjection<K, V>;
56
58
  //#endregion
57
59
  //#region core/src/projection/keyed/flat-map.d.ts
58
60
  type Equality$1<V> = (previous: V, next: V) => boolean;
@@ -79,7 +81,7 @@ declare const derive: typeof createValueDerive & {
79
81
  <K extends string, V, const D extends object, G extends string>(source: KeyedProjection<K, V>, dependencies: D & KeyedDependencies<NoInfer<K>, NoInfer<V>, D>, selector: (value: NoInfer<V>, key: NoInfer<K>, dependencies: KeyedDependencyValues<D>) => Synchronous<G | readonly G[]>): KeyedProjection<G, readonly K[]>;
80
82
  };
81
83
  flatMap: typeof createKeyedFlatMap;
82
- singleton: <K extends string, V>(source: Projection<V | undefined>, keyOf: (value: V) => Synchronous<K>, equality?: (previous: V, next: V) => boolean) => KeyedProjection<K, V>;
84
+ singleton: typeof createKeyedSingleton;
83
85
  subset: <K extends string, V>(source: KeyedProjection<K, V>, orderedKeys: Projection<readonly K[]> | readonly K[]) => KeyedProjection<K, V>;
84
86
  filter: {
85
87
  <K extends string, V>(source: KeyedProjection<K, V>, predicate: (value: NoInfer<V>, key: NoInfer<K>) => Synchronous<boolean>): KeyedProjection<K, V>;
package/dist/index.d.ts CHANGED
@@ -40,7 +40,7 @@ type KeyedEntries<K extends string, V> = readonly (readonly [K, V])[];
40
40
  type Equality$2<T> = (previous: T, next: T) => boolean;
41
41
  declare function createKeyedFrom<K extends string, V>(source: Projection<readonly V[]>, keyOf: (value: NoInfer<V>) => Synchronous<K>, equality?: Equality$2<V>): KeyedProjection<K, V>;
42
42
  declare function createKeyedFrom<K extends string, V>(source: readonly V[], keyOf: (value: NoInfer<V>) => Synchronous<K>, equality?: Equality$2<V>): KeyedProjection<K, V>;
43
- declare function createKeyedFromEntries<const D extends ProjectionDependencies, K extends string, V>(dependencies: D, compute: (values: ProjectionValues<D>) => Synchronous<KeyedEntries<K, V>>, equality?: Equality$2<NoInfer<V>>): KeyedProjection<K, V>;
43
+ declare function createKeyedFromEntries<const D extends ProjectionDependencies, K extends string, V>(dependencies: D, compute: (values: ProjectionValues<D>) => Synchronous<KeyedEntries<K, V>>, equality?: Equality$2<V>): KeyedProjection<K, V>;
44
44
  declare function createKeyedMerge<K extends string, V>(sources: readonly KeyedProjection<K, V>[], options: {
45
45
  readonly conflict: 'error' | 'first' | 'last';
46
46
  readonly equality?: Equality$2<V>;
@@ -53,6 +53,8 @@ declare function createKeyedMerge<K extends string, V>(sources: readonly KeyedPr
53
53
  }[], key: NoInfer<K>) => Synchronous<V>;
54
54
  readonly equality?: Equality$2<V>;
55
55
  }): KeyedProjection<K, V>;
56
+ declare function createKeyedSingleton<const K extends string, V>(source: Projection<V | undefined>, keyOf: (value: V) => K, equality?: Equality$2<V>): KeyedProjection<K, V>;
57
+ declare function createKeyedSingleton<const D extends ProjectionDependencies, K extends string, V>(dependencies: D, computeEntry: (values: ProjectionValues<D>) => Synchronous<readonly [K, V] | undefined>, equality?: Equality$2<V>): KeyedProjection<K, V>;
56
58
  //#endregion
57
59
  //#region core/src/projection/keyed/flat-map.d.ts
58
60
  type Equality$1<V> = (previous: V, next: V) => boolean;
@@ -79,7 +81,7 @@ declare const derive: typeof createValueDerive & {
79
81
  <K extends string, V, const D extends object, G extends string>(source: KeyedProjection<K, V>, dependencies: D & KeyedDependencies<NoInfer<K>, NoInfer<V>, D>, selector: (value: NoInfer<V>, key: NoInfer<K>, dependencies: KeyedDependencyValues<D>) => Synchronous<G | readonly G[]>): KeyedProjection<G, readonly K[]>;
80
82
  };
81
83
  flatMap: typeof createKeyedFlatMap;
82
- singleton: <K extends string, V>(source: Projection<V | undefined>, keyOf: (value: V) => Synchronous<K>, equality?: (previous: V, next: V) => boolean) => KeyedProjection<K, V>;
84
+ singleton: typeof createKeyedSingleton;
83
85
  subset: <K extends string, V>(source: KeyedProjection<K, V>, orderedKeys: Projection<readonly K[]> | readonly K[]) => KeyedProjection<K, V>;
84
86
  filter: {
85
87
  <K extends string, V>(source: KeyedProjection<K, V>, predicate: (value: NoInfer<V>, key: NoInfer<K>) => Synchronous<boolean>): KeyedProjection<K, V>;
package/dist/index.js CHANGED
@@ -3157,35 +3157,57 @@ function createKeyedMerge(sources, options) {
3157
3157
  });
3158
3158
  return projection;
3159
3159
  }
3160
- const createKeyedSingleton = (source, keyOf, equality = Object.is) => {
3161
- if (outputKind(source) !== "value") throw new TypeError("derive.keyed.singleton source must be a scalar projection.");
3162
- if (typeof keyOf !== "function") throw new TypeError("derive.keyed.singleton requires a key selector.");
3160
+ function createKeyedSingleton(sourceOrDependencies, compute, equality) {
3161
+ const name = "derive.keyed.singleton";
3162
+ const selection = isProjection(sourceOrDependencies) ? {
3163
+ kind: "scalar",
3164
+ source: sourceOrDependencies,
3165
+ keyOf: compute
3166
+ } : {
3167
+ kind: "named",
3168
+ compiled: compileProjectionDependencies(sourceOrDependencies, name),
3169
+ computeEntry: compute
3170
+ };
3171
+ if (selection.kind === "scalar" && outputKind(selection.source) !== "value") throw new TypeError(`${name} source must be a scalar projection.`);
3172
+ if (typeof compute !== "function") throw new TypeError(`${name} requires a ${selection.kind === "scalar" ? "key selector" : "compute callback"}.`);
3173
+ const outputEquality = assertEquality(equality, name);
3163
3174
  const [projection] = defineProcessor({
3164
- dependencies: [source],
3175
+ dependencies: selection.kind === "named" ? selection.compiled.projections : [selection.source],
3165
3176
  outputs: [{
3166
3177
  kind: "collection",
3167
- equality
3178
+ equality: outputEquality
3168
3179
  }],
3169
3180
  create: () => ({ evaluate: (evaluation) => {
3170
- const scalar = evaluation.sources[0];
3171
3181
  const output = evaluation.outputs[0];
3172
- if (scalar.kind !== "value" || output.kind !== "collection") throw new Error("derive.keyed.singleton resolved invalid input/output kinds.");
3173
- const previous = output.previous.ids();
3174
- if (scalar.value === void 0) {
3175
- for (const key of previous) output.output.remove(key);
3176
- return;
3182
+ if (output.kind !== "collection") throw new Error(`${name} requires a collection output.`);
3183
+ let key;
3184
+ let value;
3185
+ if (selection.kind === "named") {
3186
+ const entry = selection.computeEntry(snapshotProjectionValues(selection.compiled.names, evaluation.sources));
3187
+ assertSynchronous(entry);
3188
+ if (entry !== void 0) {
3189
+ assertKeyedEntry(entry, name);
3190
+ key = entry[0];
3191
+ value = entry[1];
3192
+ }
3193
+ } else {
3194
+ const scalar = evaluation.sources[0];
3195
+ if (scalar?.kind !== "value") throw new Error(`${name} resolved a non-scalar input.`);
3196
+ value = scalar.value;
3197
+ if (value !== void 0) {
3198
+ key = selection.keyOf(value);
3199
+ assertSynchronous(key);
3200
+ if (typeof key !== "string") throw new TypeError(`${name} key selector must return a string.`);
3201
+ }
3177
3202
  }
3178
- const key = keyOf(scalar.value);
3179
- assertSynchronous(key);
3180
- if (typeof key !== "string") throw new TypeError("derive.keyed.singleton key selector must return a string.");
3181
- for (const previousKey of previous) if (previousKey !== key) output.output.remove(previousKey);
3182
- output.output.set(key, scalar.value);
3183
- if (previous.length !== 1 || previous[0] !== key) output.output.order([key]);
3203
+ const previousKey = output.previous.ids()[0];
3204
+ if (previousKey !== void 0 && previousKey !== key) output.output.remove(previousKey);
3205
+ if (key !== void 0) output.output.set(key, value);
3184
3206
  } }),
3185
- name: "derive.keyed.singleton"
3207
+ name
3186
3208
  });
3187
3209
  return projection;
3188
- };
3210
+ }
3189
3211
  //#endregion
3190
3212
  //#region core/src/projection/derive/keyed.ts
3191
3213
  const compileOrderedKeys = (value, role) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "doxum",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "description": "Doxum is a typed runtime for complex mutable documents.",
@@ -44,7 +44,7 @@ Follow these choices unless the requirement says otherwise:
44
44
  3. Use `read` for one synchronous document read and `select` for a document `Readable` with dynamic dependency tracking.
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
- 6. Use `derive.keyed.from` to turn an ordered scalar/static collection into a keyed projection. Use `derive.keyed.fromEntries({ inputs }, compute)` when named projections compute a complete keyed result; 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' }`.
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
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.
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.
@@ -200,6 +200,8 @@ const errors = derive.keyed.fromEntries(
200
200
 
201
201
  Each synchronous computation returns the **complete** finite `readonly (readonly [K, V])[]` result. The tuple order is formal output order; `[]` removes all members. Keys must be unique strings. `[key, undefined]` is a present member. A key missing from the next result is removed. Reordering alone does not invent value updates.
202
202
 
203
+ Value inference uses both `compute` and a typed `equality` callback, including computations with empty-array branches and union values. These cases do not require an output `KeyedProjection<K, V>` annotation. Equality must accept every emitted value variant. Normal TypeScript literal widening still applies; preserve discriminants with a declared value type or `as const` when needed.
204
+
203
205
  Equality defaults to `Object.is` and compares values under the same key, not the outer array or tuple objects. Equality-equivalent values retain their existing reference; new value objects require stable references or a suitable equality callback. Equality cannot suppress membership/order. Callback failures, thenables, malformed tuples, duplicate keys and equality failures install no partial output and follow normal initialization/processor recovery semantics. Output definitions are lazy, reusable across runtimes and compatible with scopes and `runtime.items()`.
204
206
 
205
207
  This is **whole-result computation with incremental output publication**. Each necessary evaluation runs the callback for all inputs and scans the old/new result membership, costing O(old result size + new result size), plus callback work and keyed dependency snapshot costs. A keyed dependency is consumed as a whole: `records.get(activeId)` inside the callback does not establish an entry dependency. For precise dynamic lookup, compose the existing scalar lookup first:
@@ -278,9 +280,41 @@ Use this for domain-neutral reverse indexing such as record→sections, node→e
278
280
 
279
281
  ```ts
280
282
  const activeEntity = derive.keyed.singleton(activeRecord, record => record.id);
283
+
284
+ const preview = derive.keyed.singleton(
285
+ { record: activeRecord, enabled: previewEnabled },
286
+ ({ record, enabled }) => (!enabled || record === undefined ? undefined : [record.id, record])
287
+ );
288
+
289
+ const fixed = derive.keyed.singleton({}, () => ['fixed', 1]);
281
290
  ```
282
291
 
283
- Input is `Projection<V | undefined>`. `undefined` becomes empty membership; a present value becomes exactly one keyed entry.
292
+ Scalar form accepts `Projection<V | undefined>` and `keyOf(value)`. Undefined sources
293
+ are empty and skip `keyOf`; present values are preserved as one member. Named form
294
+ accepts ordinary `derive` dependencies, including `{}`, and a synchronous callback
295
+ returning a readonly `[K, V]` tuple or `undefined`. `undefined` means no member;
296
+ `[key, undefined]` is a present member. Keys must be strings. `null`, `false`, `[]`,
297
+ malformed tuples and thenables are errors. Formal order is empty or the sole key.
298
+
299
+ Optional equality defaults to `Object.is` and compares same-key values, retaining equal
300
+ references. Changing the key removes the old member and adds the new one regardless of
301
+ value equality. Typed equality participates in inference and must accept every emitted
302
+ variant; inline tuples, optional branches and union values do not require an output
303
+ projection annotation. Use a declared value type or `as const` when object discriminants
304
+ would otherwise widen under normal TypeScript rules.
305
+
306
+ Dependencies are captured at definition time; callback values are readonly. Keyed named
307
+ inputs are whole durable map snapshots. For a precise active-entry dependency, compose
308
+ `derive.keyed.get(records, activeId)` first; driver-relative specs do not belong here.
309
+ Public Runtime reads in callbacks remain forbidden. Callback, validation and equality
310
+ failures install no partial output and follow normal processor recovery. Definitions
311
+ are lazy, isolated per runtime and compatible with scopes. Current reads can evaluate
312
+ multiple times in a batch; item lifecycles end only at the notification boundary.
313
+
314
+ Both forms use one processor and one collection output. Singleton's member coordination
315
+ is O(1), plus callback and dependency snapshot costs. Use this primitive for optional
316
+ previews, selections and active members instead of manually building zero-or-one arrays
317
+ or staging collection patches. Use `fromEntries` when more than one member is possible.
284
318
 
285
319
  ## Runtime read/select
286
320
 
@@ -331,7 +331,7 @@ The keyed family:
331
331
  | `merge` | Multiple keyed projections → union with explicit conflict policy |
332
332
  | `flatMap` | Ordered pure one-to-many keyed expansion |
333
333
  | `groupBy` | One-to-many reverse index |
334
- | `singleton` | Optional scalar → zero-or-one keyed projection |
334
+ | `singleton` | Optional scalar or named dependencies → zero-or-one keyed result |
335
335
  | `subset` | Externally ordered keyed subset |
336
336
  | `filter` | Predicate-controlled membership preserving source values |
337
337
  | `compact` | Map values while dropping `undefined` results |
@@ -350,6 +350,7 @@ derive.keyed.from(source: Projection<readonly V[]>, keyOf, equality?): KeyedProj
350
350
  derive.keyed.from(source: readonly V[], keyOf, equality?): KeyedProjection<K,V>
351
351
  derive.keyed.fromEntries(dependencies, compute, equality?): KeyedProjection<K,V>
352
352
  // compute(readonlyNamedValues) returns Synchronous<readonly (readonly [K,V])[]>
353
+ // V is inferred from compute and typed equality; equality must accept every emitted variant.
353
354
  derive.keyed.merge(sources, { conflict: 'error' | 'first' | 'last', equality? }): KeyedProjection<K,V>
354
355
  derive.keyed.merge(sources, { conflict: 'resolve', resolve, equality? }): KeyedProjection<K,V>
355
356
  derive.keyed.subset(source, orderedKeysOrProjection): KeyedProjection<K,V>
@@ -363,12 +364,16 @@ derive.keyed.flatMap(source, dependencies, select, equality?): KeyedProjection<O
363
364
  derive.keyed.groupBy(source, selector): KeyedProjection<GroupKey, readonly K[]>
364
365
  derive.keyed.groupBy(source, dependencies, selector): KeyedProjection<GroupKey, readonly K[]>
365
366
  derive.keyed.singleton(sourceProjection, keyOf, equality?): KeyedProjection<K,V>
367
+ derive.keyed.singleton(dependencies, computeEntry, equality?): KeyedProjection<K,V>
368
+ // computeEntry(readonlyNamedValues) returns Synchronous<readonly [K,V] | undefined>
366
369
  ```
367
370
 
368
371
  `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.
369
372
 
370
373
  `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.
371
374
 
375
+ `singleton` returns at most one member. Scalar form accepts `Projection<V | undefined>`, omits undefined sources and preserves the source value under `keyOf(value)`. Named form uses ordinary `derive` dependencies (including `{}`), returning `[key, value]` or `undefined`; `[key, undefined]` is present. Keys are strings; empty arrays, null, false, malformed tuples and thenables are rejected. Typed equality participates in inference and compares same-key values only, preserving equivalent references. Key changes remain membership changes. Both forms share normal processor recovery and notification-boundary item lifecycles. Keyed named inputs are whole snapshots; compose `derive.keyed.get` for precise lookup. See [Singleton](projections.md#singleton) for examples and costs.
376
+
372
377
  `merge` has union membership and requires an explicit conflict policy. `first` and `last` use source-list priority. `error` rejects overlapping keys. `resolve` calls `resolve(contributions, key)` only for keys with two or more current contributions; contributions are ordered by source priority and have `{ sourceIndex, value }`. A single contribution passes through unchanged. The source list is fixed at definition time.
373
378
 
374
379
  Merged formal order is always `stableUnique(S0.ids() ++ S1.ids() ++ ... ++ Sn.ids())`, independent of which source supplies the effective value. Therefore `derive.keyed.merge([base, overrides], { conflict: 'last' })` is the standard base + sparse-override composition: override values win for shared keys without moving their base positions; override-only keys are included after earlier-source first occurrences. Removing an override does not end the merged membership lifecycle while another source still contains the key.
@@ -337,13 +337,24 @@ Output is keyed by section id; each value is ordered source record ids. Use this
337
337
 
338
338
  Group keys follow first appearance in the source's formal order. If one source record returns several groups, those groups use the selector's returned order for that record when they first appear together.
339
339
 
340
- ## Optional scalar to keyed singleton
340
+ ## Zero or one keyed member
341
341
 
342
342
  ```ts
343
343
  const activeRecordCollection = derive.keyed.singleton(activeRecord, record => record.id);
344
+
345
+ const preview = derive.keyed.singleton(
346
+ { record: activeRecord, enabled: previewEnabled },
347
+ ({ record, enabled }) => (!enabled || record === undefined ? undefined : [record.id, record])
348
+ );
344
349
  ```
345
350
 
346
- `undefined` becomes empty membership; present value becomes one keyed member.
351
+ Scalar form turns an undefined source into empty membership. Named form returns a
352
+ tuple for a present member or `undefined` for no member; `[key, undefined]` is present.
353
+ Use the named form when several projections determine the member. Pass a value equality
354
+ callback when constructing equivalent objects; it preserves same-key value references
355
+ but cannot suppress a key change. For a precise dynamic input, obtain `activeRecord`
356
+ with `derive.keyed.get(records, activeId)`. Do not wrap dependencies in an intermediate
357
+ scalar derive or use `fromEntries` solely to wrap one tuple in an array.
347
358
 
348
359
  ## Stable keyed item Readables
349
360