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.
- package/README.md +29 -3
- package/dist/advanced.cjs +1 -1
- package/dist/advanced.d.cts +1 -1
- package/dist/advanced.d.ts +1 -1
- package/dist/advanced.js +1 -1
- package/dist/{dependency-B3E65M1E.d.cts → dependency-B1qIJJBL.d.cts} +15 -3
- package/dist/{dependency-BYbdDHNa.d.ts → dependency-BRgPvyN6.d.ts} +15 -3
- package/dist/index.cjs +276 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +276 -30
- package/dist/index.js.map +1 -1
- package/dist/{transform-B6YxfR0R.cjs → transform-CENhTaVN.cjs} +42 -18
- package/dist/{transform-B6YxfR0R.cjs.map → transform-CENhTaVN.cjs.map} +1 -1
- package/dist/{transform-BQY9I6il.js → transform-gOtqcdHe.js} +42 -18
- package/dist/{transform-BQY9I6il.js.map → transform-gOtqcdHe.js.map} +1 -1
- package/package.json +1 -1
- package/skills/doxum-runtime/SKILL.md +6 -5
- package/skills/doxum-runtime/references/projections.md +44 -3
- package/skills/doxum-runtime/references/public-api.md +34 -1
- package/skills/doxum-runtime/references/recipes.md +35 -2
|
@@ -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 }) =>
|
|
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
|
-
|
|
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 }) =>
|
|
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
|
|
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
|