doxum 0.1.30 → 0.1.32

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.
@@ -32,6 +32,8 @@ Use the simplest public primitive that owns the required semantics:
32
32
  | Ordered values as scalar array | `derive.keyed.values(source)` |
33
33
  | Ordered `[key,value]` tuples as scalar array | `derive.keyed.entries(source)` |
34
34
  | Current scalar key selects one keyed entry | `derive.keyed.get(source, keyProjection)` |
35
+ | Ordered scalar/static collection becomes keyed | `derive.keyed.from(source, keyOf)` |
36
+ | Multiple keyed sources become one keyed union | `derive.keyed.merge(sources, options)` |
35
37
  | Select requested ordered subset | `derive.keyed.subset(source, orderedKeys)` |
36
38
  | Preserve source values but filter membership | `derive.keyed.filter(source, predicate)` |
37
39
  | Map entry and drop `undefined` results | `derive.keyed.compact(source, selector)` |
@@ -113,22 +115,25 @@ const cards = derive.keyed(
113
115
  items,
114
116
  {
115
117
  density,
118
+ metadata: { source: metadataByItemId },
116
119
  record: { source: records, key: item => item.recordId },
117
120
  related: { source: records, keys: item => item.relatedRecordIds },
118
121
  },
119
- (item, itemId, { density, record, related }) => renderCard(itemId, item, density, record, related)
122
+ (item, itemId, { density, metadata, record, related }) =>
123
+ renderCard(itemId, item, density, metadata, record, related)
120
124
  );
121
125
  ```
122
126
 
123
127
  Dependency forms:
124
128
 
125
129
  - ordinary `Projection<T>`: global dependency; when it changes, relevant driver work is invalidated as a whole;
130
+ - `{ source }`: same-key lookup; driver key directly selects the source key and resolves to `V | undefined`;
126
131
  - `{ source, key }`: singular driver-key → source-key binding; resolved value is `V | undefined`;
127
132
  - `{ source, keys }`: ordered duplicate-free driver-key → source-keys binding; resolved value is `ReadonlyMap<K,V>` in requested-key order, containing currently present entries only.
128
133
 
129
- The Runtime owns forward binding and reverse invalidation. A missing selected source entry still keeps the binding, so a later source add invalidates the dependent driver key correctly.
134
+ The same-key form requires `DriverKey extends SourceKey`; this preserves branded-key safety while allowing a narrower driver domain to read a broader source domain. It has no selector or relation map: source entry changes invalidate the driver entry with the same key directly. The Runtime owns forward binding and reverse invalidation for mapped forms. A missing selected source entry still keeps the binding, so a later source add invalidates the dependent driver key correctly.
130
135
 
131
- Source-only order change does not invalidate singular/plural keyed lookup because dependency identity is by selected key, not by source-global order.
136
+ Source-only order change does not invalidate same/singular/plural keyed lookup because dependency identity is by selected key, not by source-global order.
132
137
 
133
138
  `keys(...)` selectors must return duplicate-free key arrays. Treat duplicate keys as invalid input rather than as a hidden deduplication feature.
134
139
 
@@ -168,6 +173,40 @@ const active = derive.keyed.get(rows, activeId);
168
173
 
169
174
  Use this for active entity, editor target, selected record, or current-cell lookups.
170
175
 
176
+ ## Keyed construction and composition
177
+
178
+ ### `from`
179
+
180
+ ```ts
181
+ const rows = derive.keyed.from(rowArray, row => row.id);
182
+ const projectedRows = derive.keyed.from(rowsProjection, row => row.id);
183
+ ```
184
+
185
+ `source` is either a static readonly array or `Projection<readonly V[]>`. `keyOf(value)` defines stable member identity and must return a unique string key. The output formal order is exactly the input array order. Duplicate keys are processor errors; Doxum never silently overwrites or deduplicates them.
186
+
187
+ Static outer arrays are shallow-snapshotted when the definition is created, while `keyOf` remains lazy until materialization. A scalar array projection has no per-entry delta, so every scalar publication is scanned. Output publication is still exact: same-key equality-equivalent values retain the previous published value identity, and reorder-only updates publish order without inventing value updates.
188
+
189
+ ### `merge`
190
+
191
+ ```ts
192
+ const effective = derive.keyed.merge([base, overrides], {
193
+ conflict: 'last',
194
+ });
195
+ ```
196
+
197
+ `merge` accepts a fixed definition-time list of `KeyedProjection<K,V>` sources. Membership is their union. Conflict policy is required:
198
+
199
+ - `error`: overlapping keys are processor errors;
200
+ - `first`: earliest source wins;
201
+ - `last`: latest source wins;
202
+ - `resolve`: for true conflicts only, call `resolve(contributions, key)` with source-priority `{ sourceIndex, value }` contributions.
203
+
204
+ A single contribution always passes through without invoking the resolver. Equality compares the final effective value and defaults to `Object.is`.
205
+
206
+ Formal merged order is always the stable first occurrence from concatenated source orders: `stableUnique(S0.ids() ++ S1.ids() ++ ... ++ Sn.ids())`. Value winner and position are therefore independent. For base + sparse overrides, `{ conflict: 'last' }` changes shared values without moving their base positions; override-only keys are also valid union members. If an override disappears while base still contains the key, the merged membership lifecycle remains continuous and `runtime.items(merged).get(key)` keeps the same current membership identity.
207
+
208
+ Value-only source changes only recompute affected keys. Structural or source-order changes may rebuild merged formal order. Present `undefined` remains a valid value because membership is determined by `has(key)`, not by `get(key) !== undefined`.
209
+
171
210
  ## Membership-changing keyed primitives
172
211
 
173
212
  ### `subset`
@@ -204,6 +243,8 @@ const recordsBySection = derive.keyed.groupBy(records, record => record.sectionI
204
243
 
205
244
  Selector returns one group key or a readonly group-key array. Output is `KeyedProjection<GroupKey, readonly SourceKey[]>`. Each group's members follow source formal order. The Runtime incrementally maintains reverse membership for added/updated/removed/order/reset.
206
245
 
246
+ Group-key output order is also defined. Rank each group by the pair `(firstSourceIndex, selectorIndex)`: `firstSourceIndex` is the position of the earliest current source member that belongs to the group, and `selectorIndex` is that group's position in the selector result for that earliest member. Groups therefore follow first appearance in formal source order; when several groups first appear on the same source member, they follow the selector's returned group-key order for that member. If source order or membership changes, group keys may reorder according to the newly computed ranks.
247
+
207
248
  Use this for domain-neutral reverse indexing such as record→sections, node→edges, group→item ids. Keep domain names outside Doxum itself.
208
249
 
209
250
  ### `singleton`
@@ -318,6 +318,24 @@ derive({ dependencyName: projection, ... }, values => result, equality?): Projec
318
318
 
319
319
  The keyed family:
320
320
 
321
+ <!-- family:derive.keyed:start -->
322
+
323
+ | Member | Purpose |
324
+ | ----------- | ---------------------------------------------------------------- |
325
+ | `keys` | Formal ordered membership as a scalar readonly array |
326
+ | `values` | Formal ordered values as a scalar readonly array |
327
+ | `entries` | Formal ordered readonly `[key,value]` tuples |
328
+ | `get` | Scalar dynamic lookup into one current keyed entry |
329
+ | `from` | Ordered scalar/static collection → keyed projection |
330
+ | `merge` | Multiple keyed projections → union with explicit conflict policy |
331
+ | `groupBy` | One-to-many reverse index |
332
+ | `singleton` | Optional scalar → zero-or-one keyed projection |
333
+ | `subset` | Externally ordered keyed subset |
334
+ | `filter` | Predicate-controlled membership preserving source values |
335
+ | `compact` | Map values while dropping `undefined` results |
336
+
337
+ <!-- family:derive.keyed:end -->
338
+
321
339
  ```ts
322
340
  derive.keyed(source, (value, key) => result, equality?): KeyedProjection<K,T>
323
341
  derive.keyed(source, dependencies, (value, key, dependencies) => result, equality?): KeyedProjection<K,T>
@@ -326,6 +344,10 @@ derive.keyed.keys(source): Projection<readonly K[]>
326
344
  derive.keyed.values(source): Projection<readonly V[]>
327
345
  derive.keyed.entries(source): Projection<readonly (readonly [K,V])[]>
328
346
  derive.keyed.get(source, keyProjection, equality?): Projection<V | undefined>
347
+ derive.keyed.from(source: Projection<readonly V[]>, keyOf, equality?): KeyedProjection<K,V>
348
+ derive.keyed.from(source: readonly V[], keyOf, equality?): KeyedProjection<K,V>
349
+ derive.keyed.merge(sources, { conflict: 'error' | 'first' | 'last', equality? }): KeyedProjection<K,V>
350
+ derive.keyed.merge(sources, { conflict: 'resolve', resolve, equality? }): KeyedProjection<K,V>
329
351
  derive.keyed.subset(source, orderedKeysOrProjection): KeyedProjection<K,V>
330
352
  derive.keyed.filter(source, predicate): KeyedProjection<K,V>
331
353
  derive.keyed.filter(source, dependencies, predicate): KeyedProjection<K,V>
@@ -336,14 +358,25 @@ derive.keyed.groupBy(source, dependencies, selector): KeyedProjection<GroupKey,
336
358
  derive.keyed.singleton(sourceProjection, keyOf, equality?): KeyedProjection<K,V>
337
359
  ```
338
360
 
361
+ `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.
362
+
363
+ `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.
364
+
365
+ 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.
366
+
367
+ `merge` equality compares the final effective value. Value-only changes only recompute affected keys; source membership/order changes may rebuild formal merged order. Present `undefined` is valid in both `from` and `merge`; membership is never inferred from `get(key) !== undefined`.
368
+
369
+ `groupBy` has deterministic order on both axes. Every bucket value contains source keys in formal source order. Output group keys are ordered by the earliest current source member that belongs to each group; if multiple groups first occur on the same source member, their order is the selector's group-key order for that member. Source membership/order changes can therefore reorder group keys even when the set of groups is unchanged.
370
+
339
371
  Dynamic keyed dependency entries are either ordinary global `Projection`s or:
340
372
 
341
373
  ```ts
374
+ { source: keyedProjection }
342
375
  { source: keyedProjection, key: (driverValue, driverKey) => sourceKey | undefined }
343
376
  { source: keyedProjection, keys: (driverValue, driverKey) => readonly sourceKey[] }
344
377
  ```
345
378
 
346
- The singular resolved value is `V | undefined`; plural resolves to `ReadonlyMap<K,V>` in the requested key order, including only currently present entries.
379
+ `{ source }` is a same-key dependency: the current driver key directly selects the source entry. It is allowed only when `DriverKey extends SourceKey`, so branded key domains remain type-safe. `{ source, key }` is a mapped singular lookup. Both singular forms resolve to `V | undefined`. `{ source, keys }` resolves to `ReadonlyMap<K,V>` in the requested key order, including only currently present entries. Selector callbacks remain value-first: `(driverValue, driverKey)`.
347
380
 
348
381
  ### Projection runtime and scope
349
382
 
@@ -269,15 +269,17 @@ Use projection input for Runtime-local UI/application state. Keep it out of the
269
269
  const cards = derive.keyed(
270
270
  items,
271
271
  {
272
+ metadata: { source: metadataByItemId },
272
273
  record: { source: records, key: item => item.recordId },
273
274
  related: { source: records, keys: item => item.relatedRecordIds },
274
275
  density,
275
276
  },
276
- (item, itemId, { record, related, density }) => buildCard(itemId, item, record, related, density)
277
+ (item, itemId, { metadata, record, related, density }) =>
278
+ buildCard(itemId, item, metadata, record, related, density)
277
279
  );
278
280
  ```
279
281
 
280
- The Runtime owns reverse invalidation. Do not subscribe to `records` and manually maintain `recordId -> itemIds` just for dependency routing.
282
+ Use `{ source }` when the dependency uses the same key as the driver; it replaces identity selectors such as `key: (_value, id) => id`. Use `{ source, key }` for one mapped key and `{ source, keys }` for several mapped keys. The Runtime owns invalidation routing. Do not subscribe to sources and manually maintain reverse maps just for dependency routing.
281
283
 
282
284
  ## Ordered keyed entries as a scalar value
283
285
 
@@ -287,6 +289,35 @@ const entries = derive.keyed.entries(records);
287
289
 
288
290
  Use this instead of a two-stage `derive.keyed(records, (value,key) => [key,value])` followed by `values`.
289
291
 
292
+ ## Ordered scalar/static collection to keyed
293
+
294
+ ```ts
295
+ const visibleFields = derive.keyed.from(visibleFieldArray, field => field.id);
296
+ ```
297
+
298
+ Use `from` when the source shape is an ordered scalar array and downstream work needs formal keyed membership/order. `keyOf` must produce unique stable keys. For high-frequency single-key edits, keep the source keyed instead of repeatedly flattening it to a scalar array first.
299
+
300
+ ## Synthetic fixed entry plus dynamic keyed source
301
+
302
+ ```ts
303
+ const titleFields = derive.keyed.from<FieldId, Field>([titleField], field => field.id);
304
+ const allFields = derive.keyed.merge([titleFields, fields], {
305
+ conflict: 'error',
306
+ });
307
+ ```
308
+
309
+ Use this instead of an application-maintained `Set`/`Map` plus manual output ordering. `conflict: 'error'` makes an accidental collision between the synthetic entry and dynamic source explicit.
310
+
311
+ ## Base plus sparse overrides
312
+
313
+ ```ts
314
+ const effectiveRows = derive.keyed.merge([baseRows, rowOverrides], {
315
+ conflict: 'last',
316
+ });
317
+ ```
318
+
319
+ Shared keys use override values while keeping the first formal position from `baseRows`. Keys that exist only in `rowOverrides` are also included because merge membership is a union. Removing an override falls back to the base value without ending the merged key's membership lifecycle while the base key remains present.
320
+
290
321
  ## Active keyed lookup
291
322
 
292
323
  ```ts
@@ -304,6 +335,8 @@ const recordIdsBySection = derive.keyed.groupBy(records, record => record.sectio
304
335
 
305
336
  Output is keyed by section id; each value is ordered source record ids. Use this instead of maintaining a global reverse-index Map in application code.
306
337
 
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
+
307
340
  ## Optional scalar to keyed singleton
308
341
 
309
342
  ```ts