doxum 0.1.37 → 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 +14 -0
- package/dist/index.cjs +41 -19
- package/dist/index.d.cts +3 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +41 -19
- package/package.json +1 -1
- package/skills/doxum-runtime/SKILL.md +1 -1
- package/skills/doxum-runtime/references/projections.md +33 -1
- package/skills/doxum-runtime/references/public-api.md +5 -1
- package/skills/doxum-runtime/references/recipes.md +13 -2
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
|
-
|
|
3162
|
-
|
|
3163
|
-
|
|
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 (
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
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
|
|
3180
|
-
|
|
3181
|
-
if (
|
|
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
|
|
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
|
@@ -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:
|
|
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
|
@@ -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:
|
|
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
|
-
|
|
3161
|
-
|
|
3162
|
-
|
|
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 (
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
|
|
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
|
|
3179
|
-
|
|
3180
|
-
if (
|
|
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
|
|
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
|
@@ -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.
|
|
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.
|
|
@@ -280,9 +280,41 @@ Use this for domain-neutral reverse indexing such as record→sections, node→e
|
|
|
280
280
|
|
|
281
281
|
```ts
|
|
282
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]);
|
|
283
290
|
```
|
|
284
291
|
|
|
285
|
-
|
|
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.
|
|
286
318
|
|
|
287
319
|
## Runtime read/select
|
|
288
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
|
|
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 |
|
|
@@ -364,12 +364,16 @@ derive.keyed.flatMap(source, dependencies, select, equality?): KeyedProjection<O
|
|
|
364
364
|
derive.keyed.groupBy(source, selector): KeyedProjection<GroupKey, readonly K[]>
|
|
365
365
|
derive.keyed.groupBy(source, dependencies, selector): KeyedProjection<GroupKey, readonly K[]>
|
|
366
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>
|
|
367
369
|
```
|
|
368
370
|
|
|
369
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.
|
|
370
372
|
|
|
371
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.
|
|
372
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
|
+
|
|
373
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.
|
|
374
378
|
|
|
375
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
|
-
##
|
|
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
|
-
|
|
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
|
|