doxum 0.1.30 → 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 +20 -2
- package/dist/index.cjs +275 -29
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +19 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +275 -29
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/skills/doxum-runtime/SKILL.md +6 -5
- package/skills/doxum-runtime/references/projections.md +38 -0
- package/skills/doxum-runtime/references/public-api.md +32 -0
- package/skills/doxum-runtime/references/recipes.md +31 -0
package/package.json
CHANGED
|
@@ -44,11 +44,12 @@ 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`
|
|
48
|
-
7.
|
|
49
|
-
8.
|
|
50
|
-
9. Use `
|
|
51
|
-
10. Use `
|
|
47
|
+
6. Use `derive.keyed.from` to turn an ordered scalar/static collection into a keyed projection. Use `derive.keyed.merge` to compose multiple keyed projections; for base + sparse overrides, use `{ conflict: 'last' }`.
|
|
48
|
+
7. Use `derive.keyed` when one keyed source owns output membership/order. Prefer its built-in `keys`, `values`, `entries`, `get`, `from`, `merge`, `subset`, `filter`, `compact`, `groupBy`, and `singleton` primitives over hand-written incremental collection patches.
|
|
49
|
+
8. Express per-output-key joins through `{ source, key }` or `{ source, keys }` dependencies. Let the Runtime own binding and reverse invalidation; do not maintain an application `Map` just to route dependency changes.
|
|
50
|
+
9. Use `runtime.items(keyedProjection)` or `scope.items(...)` for stable per-membership item `Readable`s.
|
|
51
|
+
10. Use `incremental.keyed` for independent retained state per driver key. Use `incremental.collection` or `incremental.group` only when direct incremental output patching or shared retained state is required.
|
|
52
|
+
11. Use `ProjectionScope` only for lifecycle ownership. Define projections normally, then `scope.own(...)` before that definition is first materialized as a root.
|
|
52
53
|
|
|
53
54
|
## Failure and lifecycle rules
|
|
54
55
|
|
|
@@ -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)` |
|
|
@@ -168,6 +170,40 @@ const active = derive.keyed.get(rows, activeId);
|
|
|
168
170
|
|
|
169
171
|
Use this for active entity, editor target, selected record, or current-cell lookups.
|
|
170
172
|
|
|
173
|
+
## Keyed construction and composition
|
|
174
|
+
|
|
175
|
+
### `from`
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const rows = derive.keyed.from(rowArray, row => row.id);
|
|
179
|
+
const projectedRows = derive.keyed.from(rowsProjection, row => row.id);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
`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.
|
|
183
|
+
|
|
184
|
+
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.
|
|
185
|
+
|
|
186
|
+
### `merge`
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
const effective = derive.keyed.merge([base, overrides], {
|
|
190
|
+
conflict: 'last',
|
|
191
|
+
});
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
`merge` accepts a fixed definition-time list of `KeyedProjection<K,V>` sources. Membership is their union. Conflict policy is required:
|
|
195
|
+
|
|
196
|
+
- `error`: overlapping keys are processor errors;
|
|
197
|
+
- `first`: earliest source wins;
|
|
198
|
+
- `last`: latest source wins;
|
|
199
|
+
- `resolve`: for true conflicts only, call `resolve(contributions, key)` with source-priority `{ sourceIndex, value }` contributions.
|
|
200
|
+
|
|
201
|
+
A single contribution always passes through without invoking the resolver. Equality compares the final effective value and defaults to `Object.is`.
|
|
202
|
+
|
|
203
|
+
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.
|
|
204
|
+
|
|
205
|
+
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`.
|
|
206
|
+
|
|
171
207
|
## Membership-changing keyed primitives
|
|
172
208
|
|
|
173
209
|
### `subset`
|
|
@@ -204,6 +240,8 @@ const recordsBySection = derive.keyed.groupBy(records, record => record.sectionI
|
|
|
204
240
|
|
|
205
241
|
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
242
|
|
|
243
|
+
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.
|
|
244
|
+
|
|
207
245
|
Use this for domain-neutral reverse indexing such as record→sections, node→edges, group→item ids. Keep domain names outside Doxum itself.
|
|
208
246
|
|
|
209
247
|
### `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,6 +358,16 @@ 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
|
|
@@ -287,6 +287,35 @@ const entries = derive.keyed.entries(records);
|
|
|
287
287
|
|
|
288
288
|
Use this instead of a two-stage `derive.keyed(records, (value,key) => [key,value])` followed by `values`.
|
|
289
289
|
|
|
290
|
+
## Ordered scalar/static collection to keyed
|
|
291
|
+
|
|
292
|
+
```ts
|
|
293
|
+
const visibleFields = derive.keyed.from(visibleFieldArray, field => field.id);
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
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.
|
|
297
|
+
|
|
298
|
+
## Synthetic fixed entry plus dynamic keyed source
|
|
299
|
+
|
|
300
|
+
```ts
|
|
301
|
+
const titleFields = derive.keyed.from<FieldId, Field>([titleField], field => field.id);
|
|
302
|
+
const allFields = derive.keyed.merge([titleFields, fields], {
|
|
303
|
+
conflict: 'error',
|
|
304
|
+
});
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
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.
|
|
308
|
+
|
|
309
|
+
## Base plus sparse overrides
|
|
310
|
+
|
|
311
|
+
```ts
|
|
312
|
+
const effectiveRows = derive.keyed.merge([baseRows, rowOverrides], {
|
|
313
|
+
conflict: 'last',
|
|
314
|
+
});
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
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.
|
|
318
|
+
|
|
290
319
|
## Active keyed lookup
|
|
291
320
|
|
|
292
321
|
```ts
|
|
@@ -304,6 +333,8 @@ const recordIdsBySection = derive.keyed.groupBy(records, record => record.sectio
|
|
|
304
333
|
|
|
305
334
|
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
335
|
|
|
336
|
+
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.
|
|
337
|
+
|
|
307
338
|
## Optional scalar to keyed singleton
|
|
308
339
|
|
|
309
340
|
```ts
|