@elaraai/east-ui 1.0.84 → 1.0.86

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.
Files changed (43) hide show
  1. package/dist/src/collections/plan/root.js +5 -7
  2. package/dist/src/collections/plan/root.js.map +1 -1
  3. package/dist/src/collections/plan/types.d.ts +1 -1
  4. package/dist/src/collections/plan/types.js +1 -1
  5. package/dist/src/collections/sheet/editing-bridge.js +1 -1
  6. package/dist/src/collections/sheet/editing-bridge.js.map +1 -1
  7. package/dist/src/collections/sheet/root.d.ts +1 -1
  8. package/dist/src/collections/sheet/root.js +1 -1
  9. package/dist/src/collections/sheet/root.js.map +1 -1
  10. package/dist/src/contracts/index.d.ts +1 -1
  11. package/dist/src/contracts/index.d.ts.map +1 -1
  12. package/dist/src/contracts/pick.js +4 -4
  13. package/dist/src/contracts/pick.js.map +1 -1
  14. package/dist/src/contracts/source.d.ts +87 -131
  15. package/dist/src/contracts/source.d.ts.map +1 -1
  16. package/dist/src/contracts/source.js +153 -368
  17. package/dist/src/contracts/source.js.map +1 -1
  18. package/dist/src/index.d.ts +1 -1
  19. package/dist/src/index.d.ts.map +1 -1
  20. package/dist/src/index.js +2 -3
  21. package/dist/src/index.js.map +1 -1
  22. package/dist/src/internal.d.ts.map +1 -1
  23. package/dist/src/internal.js +2 -1
  24. package/dist/src/internal.js.map +1 -1
  25. package/dist/src/style/typography.d.ts +18 -18
  26. package/dist/src/style/typography.js +18 -18
  27. package/dist/test/collections/index.d.ts +0 -1
  28. package/dist/test/collections/index.d.ts.map +1 -1
  29. package/dist/test/collections/index.js +0 -1
  30. package/dist/test/collections/index.js.map +1 -1
  31. package/dist/test/collections/sheet.examples.d.ts +0 -7
  32. package/dist/test/collections/sheet.examples.d.ts.map +1 -1
  33. package/dist/test/collections/sheet.examples.js +4 -31
  34. package/dist/test/collections/sheet.examples.js.map +1 -1
  35. package/dist/test/collections/table.examples.d.ts +0 -7
  36. package/dist/test/collections/table.examples.d.ts.map +1 -1
  37. package/dist/test/collections/table.examples.js +1 -22
  38. package/dist/test/collections/table.examples.js.map +1 -1
  39. package/package.json +5 -9
  40. package/dist/test/collections/paged-source.examples.d.ts +0 -14
  41. package/dist/test/collections/paged-source.examples.d.ts.map +0 -1
  42. package/dist/test/collections/paged-source.examples.js +0 -252
  43. package/dist/test/collections/paged-source.examples.js.map +0 -1
@@ -9,11 +9,14 @@
9
9
  * source, and this module is the one place that vocabulary is spelled. Both are
10
10
  * parameterised on the COLLECTION the component speaks — an `Array<Row>` for a
11
11
  * positional surface, a `Dict<String, Row>` for a keyed one (#568). east-ui
12
- * declares the shape; whoever can actually fetch windows produces a value of
13
- * it. `Data.bindPaged` in `@elaraai/e3-ui` is the production implementation
14
- * (dataset windows over an e3 workspace) and {@link Paged.of} /
15
- * {@link Paged.pinned} are the in-memory ones; e3 is never named here, and
16
- * **nothing in this file imports e3**.
12
+ * declares the shape; the platform that owns the data produces a value of it.
13
+ * Paged data is BOUND: `Data.bindPaged` in `@elaraai/e3-ui` is the producer
14
+ * (dataset windows over an e3 workspace), and east-ui produces none. e3 is
15
+ * never named here, and **nothing in this file imports e3**.
16
+ *
17
+ * A component recognises a paged source by its East TYPE: a subtype of the
18
+ * contract over the collection its `page` serves, never a struct that merely
19
+ * has fields of the right names ({@link resolveRowSource}).
17
20
  *
18
21
  * # Why a variant, not a narrow struct
19
22
  *
@@ -29,7 +32,7 @@
29
32
  *
30
33
  * @packageDocumentation
31
34
  */
32
- import { East, Expr, ArrayType, BooleanType, DictType, FunctionType, IntegerType, NullType, OptionType, SetType, StringType, StructType, VariantType, isTypeEqual, variant, some, none, } from "@elaraai/east";
35
+ import { East, Expr, ArrayType, BooleanType, DictType, FunctionType, IntegerType, NullType, OptionType, SetType, StringType, StructType, VariantType, isSubtype, printType, variant, some, none, } from "@elaraai/east";
33
36
  // ============================================================================
34
37
  // Seek — where a key lands in a source's row space
35
38
  // ============================================================================
@@ -155,7 +158,10 @@ export const SeekQueryType = VariantType({
155
158
  * once: the same `id` serves the same rows for as long as the component holds
156
159
  * it. A source whose content moves under one id — a dataset the view follows,
157
160
  * a source the view writes — is a {@link PinnedSourceType}, which names the
158
- * snapshot its windows, total and searches belong to.
161
+ * snapshot its windows, total and searches belong to, and is what
162
+ * `Data.bindPaged` returns. This shape PREDATES `revision` / `refresh`: it is
163
+ * the handle `data_bind_paged` returned before them, which a UI exported then
164
+ * still carries, so a component takes it as it always did.
159
165
  *
160
166
  * @typeParam C - The collection type one window carries.
161
167
  * @param c - The collection type value.
@@ -196,8 +202,8 @@ export const PagedSourceType = (c) => StructType({
196
202
  * (#880) — which is why the Sheet and the Plan edit a paged source only
197
203
  * through this contract.
198
204
  *
199
- * `Data.bindPaged` in `@elaraai/e3-ui` returns one, its revision the dataset's
200
- * content hash, and {@link Paged.pinned} is the in-memory one.
205
+ * This is the contract: `Data.bindPaged` in `@elaraai/e3-ui` returns one, its
206
+ * revision the dataset's content hash.
201
207
  *
202
208
  * @typeParam C - The collection type one window carries.
203
209
  * @param c - The collection type value.
@@ -310,54 +316,81 @@ function keyTypeOf(t) {
310
316
  return type.key;
311
317
  return undefined;
312
318
  }
313
- /** The members a {@link PinnedSourceType} adds to a paged source, at their types. */
314
- const SNAPSHOT_MEMBERS = {
315
- revision: FunctionType([], OptionType(StringType)),
316
- refresh: FunctionType([OptionType(StringType)], NullType),
317
- };
318
319
  /**
319
- * Whether a windowed source names its snapshot — `revision` and `refresh`,
320
- * both, at {@link PinnedSourceType}'s types.
321
- *
322
- * @param fields - The source's struct fields
323
- * @param label - Component name for the error message
324
- * @returns Whether the source is pinned
325
- * @throws Error when the source has one of the two without the other, or
326
- * either at another type — it would build neither arm faithfully
320
+ * The collection a struct's `page` member serves — the `some` payload of the
321
+ * `Option` it returns — or `undefined` when `page` is not a function returning
322
+ * a variant with a `some` case.
323
+ *
324
+ * @remarks
325
+ * What {@link resolveRowSource} checks a struct against the contract AT: a
326
+ * paged source over `C` is one whose own `page` serves `C`.
327
327
  */
328
- function namesSnapshot(fields, label) {
329
- const revision = fields["revision"];
330
- const refresh = fields["refresh"];
331
- if (revision === undefined && refresh === undefined)
332
- return false;
333
- if (revision !== undefined && refresh !== undefined
334
- && isTypeEqual(revision, SNAPSHOT_MEMBERS.revision) && isTypeEqual(refresh, SNAPSHOT_MEMBERS.refresh)) {
335
- return true;
336
- }
337
- const found = revision === undefined ? "only `refresh`" : refresh === undefined ? "only `revision`" : "them at other types";
338
- throw new Error(`${label}: a paged source names its snapshot with both \`revision: () → Option<String>\` and ` +
339
- `\`refresh: (Option<String>) → Null\`, as Data.bindPaged's handle and Paged.pinned do — this one has ${found}`);
328
+ function pagePayload(fields) {
329
+ const page = fields["page"];
330
+ if (page === undefined || page.type !== "Function")
331
+ return undefined;
332
+ const output = page.output;
333
+ if (output.type !== "Variant")
334
+ return undefined;
335
+ return output.cases["some"];
336
+ }
337
+ /**
338
+ * Recognise a WINDOWED source by its East type, through East's own subtype
339
+ * relation: a subtype of {@link PinnedSourceType} over the collection its
340
+ * `page` serves — the contract, `Data.bindPaged`'s handle — or of
341
+ * {@link PagedSourceType}, the shape that predates `revision` / `refresh`.
342
+ *
343
+ * @param t - The rows prop's East type
344
+ * @param fields - Its struct fields
345
+ * @returns The collection the source serves, and whether it names its
346
+ * snapshot; `undefined` when it is neither shape
347
+ */
348
+ function windowedSource(t, fields) {
349
+ const c = pagePayload(fields);
350
+ if (c === undefined)
351
+ return undefined;
352
+ if (isSubtype(t, PinnedSourceType(c)))
353
+ return { collectionType: c, pinned: true };
354
+ if (isSubtype(t, PagedSourceType(c)))
355
+ return { collectionType: c, pinned: false };
356
+ return undefined;
340
357
  }
358
+ /**
359
+ * The contract's fields, as a refusal spells them — every field in its order,
360
+ * at its type.
361
+ */
362
+ const CONTRACT_FIELDS = "`{ id: String, page: (Integer, Integer) → Option<C>, total: () → Option<Integer>, " +
363
+ "seek: Option<(SeekQuery) → Option<SeekRange>>, revision: () → Option<String>, " +
364
+ "refresh: (Option<String>) → Null }`";
341
365
  /**
342
366
  * Classify a component's rows prop at BUILD time — the one dispatch every
343
367
  * collection shares, so no component re-sniffs shapes of its own.
344
368
  *
345
369
  * Accepted shapes, in order:
346
370
  * - a collection expression / value (`Array<R>`, `Dict<K, V>`, `Set<R>`) ⇒ `inline`;
347
- * - a struct carrying `page` + `total` ⇒ `paged` (a {@link PagedSourceType},
348
- * or anything structurally matching it), marked `pinned` when it also
349
- * carries `revision` + `refresh` (a {@link PinnedSourceType} —
350
- * `Data.bindPaged`'s handle, `Paged.pinned`);
371
+ * - a WINDOWED source, recognised by its East type: a subtype of
372
+ * {@link PinnedSourceType} over the collection its `page` serves (the
373
+ * contract — `Data.bindPaged`'s handle) builds the `pinned` arm, and a
374
+ * subtype of {@link PagedSourceType} (the shape that predates `revision` /
375
+ * `refresh`) builds `paged`. A window of index entries is `ordered`;
351
376
  * - a struct carrying `read` ⇒ a whole-value bind handle, which resolves by
352
377
  * CALLING `read()` and recursing. The call becomes part of the surrounding
353
378
  * East expression, so it is evaluated inside the component's reactive render
354
379
  * and re-fires like any other tracked read — `rows={handle}` and
355
380
  * `rows={handle.read()}` build the same IR.
356
381
  *
382
+ * A struct that carries `page` and `total` but is neither source shape — a
383
+ * field at another type, a field missing or out of order, one extra — is
384
+ * refused, naming the contract's fields: East struct subtyping is exact, and a
385
+ * lookalike read by its field names would be paged through members that do not
386
+ * mean what a component reads them as.
387
+ *
357
388
  * @param data - The rows prop as the author passed it
358
389
  * @param label - Component name for the error message (`"Plan"`, `"Table"`)
359
390
  * @returns The resolved arm, retaining a live handle for invocation-time reads — see {@link ResolvedRowSource}
360
- * @throws Error when the expression is none of the accepted shapes
391
+ * @throws Error when the expression is none of the accepted shapes — naming the
392
+ * contract's fields when it carries `page` and `total` — or when a source's
393
+ * `page` serves something other than a collection
361
394
  */
362
395
  export function resolveRowSource(data, label) {
363
396
  const expr = East.value(data);
@@ -367,33 +400,29 @@ export function resolveRowSource(data, label) {
367
400
  return { kind: "inline", rows: expr, collectionType: t, elementType: inlineElement, keyType: keyTypeOf(t) };
368
401
  }
369
402
  const fields = structFields(t);
370
- if (fields !== undefined && fields["page"] !== undefined && fields["total"] !== undefined) {
371
- // `page: Fn([Int, Int], Option<C>)` — recover the COLLECTION through
372
- // it, then its element type. An Option is a VARIANT (`cases.some`), not
373
- // a struct, so the collection hangs off the `some` case.
374
- const page = fields["page"];
375
- const collectionType = page.output?.cases?.some;
403
+ const windowed = fields === undefined ? undefined : windowedSource(t, fields);
404
+ if (windowed !== undefined) {
405
+ const { collectionType, pinned } = windowed;
376
406
  const elementType = elementTypeOf(collectionType);
377
- if (collectionType === undefined || elementType === undefined) {
378
- throw new Error(`${label}: the paged source's \`page\` must return \`Option<Collection>\` ` +
379
- `(an Array, Dict or Set) — got ${JSON.stringify(page)}`);
407
+ if (elementType === undefined) {
408
+ throw new Error(`${label}: a paged source's \`page\` serves a collection — an Array, Dict or Set — ` +
409
+ `and this one's serves ${printType(collectionType)}`);
380
410
  }
381
- // A source that names its snapshot builds the `pinned` arm.
382
- const pinned = namesSnapshot(fields, label);
411
+ const source = expr;
383
412
  // An ORDERED window: positional, but every row carries its own key.
384
413
  // Recognised by the element's shape rather than announced by a flag,
385
414
  // so a source that serves index entries needs no second vocabulary.
386
415
  const entry = collectionType.type === "Array" ? orderedEntry(elementType) : null;
387
416
  if (entry !== null) {
388
- return {
389
- kind: "ordered", source: expr,
390
- collectionType, elementType, keyType: entry.key, orderKeyType: entry.ik, pinned,
391
- };
417
+ return { kind: "ordered", source, collectionType, elementType, keyType: entry.key, orderKeyType: entry.ik, pinned };
392
418
  }
393
- return {
394
- kind: "paged", source: expr,
395
- collectionType, elementType, keyType: keyTypeOf(collectionType), pinned,
396
- };
419
+ return { kind: "paged", source, collectionType, elementType, keyType: keyTypeOf(collectionType), pinned };
420
+ }
421
+ if (fields !== undefined && fields["page"] !== undefined && fields["total"] !== undefined) {
422
+ throw new Error(`${label}: a paged source is a platform bind's handle (Data.bindPaged) — ${CONTRACT_FIELDS}, ` +
423
+ `its fields in that order at those types, or the same without \`revision\` and \`refresh\` ` +
424
+ `(the handle a UI exported before them carries); this struct has \`page\` and \`total\` but is ` +
425
+ `neither: ${printType(t)}`);
397
426
  }
398
427
  if (fields !== undefined && fields["read"] !== undefined) {
399
428
  // A whole-value bind handle (`Data.bind`) — read it here, in the
@@ -402,7 +431,7 @@ export function resolveRowSource(data, label) {
402
431
  const resolved = resolveRowSource(handle.read(), label);
403
432
  return resolved.kind === "inline" ? { ...resolved, live: expr } : resolved;
404
433
  }
405
- throw new Error(`${label}: rows must be a collection, a paged source (\`{ id, page, total }\` — e.g. Data.bindPaged), ` +
434
+ throw new Error(`${label}: rows must be a collection, a paged source (a platform bind's handle — e.g. Data.bindPaged), ` +
406
435
  `or a bound value (\`{ read }\` — e.g. Data.bind); got a ${t.type}`);
407
436
  }
408
437
  /**
@@ -485,18 +514,16 @@ export function buildRowSource(resolved, outType, make) {
485
514
  });
486
515
  return result;
487
516
  });
488
- // A source predating the contract carries no `id` / `seek`; fall back to a
489
- // constant identity (it still compares equal to itself) and no seek.
490
- const fields = structFields(Expr.type(resolved.source)) ?? {};
491
- const id = fields["id"] !== undefined ? handle.id : East.value("", StringType);
492
- const seek = fields["seek"] !== undefined
493
- ? handle.seek
494
- : East.value(none, OptionType(FunctionType([SeekQueryType], OptionType(SeekRangeType))));
517
+ // Either shape carries `id` and `seek` — the type check that resolved it
518
+ // says so — so the derived source forwards them as the handle has them.
495
519
  if (!resolved.pinned) {
496
- return East.value(variant("paged", { id, page, total: handle.total, seek }), sourceType);
520
+ return East.value(variant("paged", { id: handle.id, page, total: handle.total, seek: handle.seek }), sourceType);
497
521
  }
498
522
  const snapshot = resolved.source;
499
- return East.value(variant("pinned", { id, page, total: handle.total, seek, revision: snapshot.revision, refresh: snapshot.refresh }), sourceType);
523
+ return East.value(variant("pinned", {
524
+ id: handle.id, page, total: handle.total, seek: handle.seek,
525
+ revision: snapshot.revision, refresh: snapshot.refresh,
526
+ }), sourceType);
500
527
  }
501
528
  /** A resolved paged source's handle, at the contract's shape. */
502
529
  function pagedHandleOf(resolved) {
@@ -578,317 +605,75 @@ export function buildPagedWindow(resolved) {
578
605
  return result;
579
606
  });
580
607
  }
581
- function createPagedOf(id, collection, options) {
582
- return createInMemory(id, collection, options, false);
583
- }
584
- function createPagedPinned(id, collection, options) {
585
- return createInMemory(id, collection, options, true);
586
- }
587
- /**
588
- * The in-memory source over a collection — {@link Paged.of}'s, or, `pinned`,
589
- * {@link Paged.pinned}'s.
590
- *
591
- * @param id - The source's identity
592
- * @param collection - The whole collection
593
- * @param options - {@link PagedOfOptions}
594
- * @param pinned - Whether the source names its snapshot
595
- * @returns The source, at the collection it was given
596
- * @throws {Error} When `pageLimit` is not a positive integer
597
- */
598
- function createInMemory(id, collection, options, pinned) {
599
- const pageLimit = options?.pageLimit;
600
- if (pageLimit !== undefined && !(Number.isInteger(pageLimit) && pageLimit > 0)) {
601
- throw new Error(`${pinned ? "Paged.pinned" : "Paged.of"}: \`pageLimit\` must be a positive integer — the most elements one window serves (got ${pageLimit})`);
602
- }
603
- const collectionExpr = East.value(collection);
604
- const collectionType = Expr.type(collectionExpr);
605
- const keyed = collectionType.type === "Dict";
606
- const sourceType = pinned ? PinnedSourceType(collectionType) : PagedSourceType(collectionType);
607
- // Evaluate the input once when creating the source, then detach nested
608
- // mutable values. Page and seek closures share that captured snapshot;
609
- // passing a live read expression cannot make later pages drift. The copy
610
- // is beast v2: v1 writes neither a recursive type nor a function, and an
611
- // entry may be either (a Plan's entries, #822).
612
- const capture = East.function([StringType, collectionType], sourceType, ($, snapshotId, input) => {
613
- const snapshot = $.const(East.Blob.encodeBeast(input, "v2").decodeBeast(collectionType, "v2"), collectionType);
614
- return keyed ? keyedPagedOf(snapshotId, snapshot, pageLimit, pinned)
615
- : arrayPagedOf(snapshotId, snapshot, options, pinned);
616
- });
617
- return East.value(capture)(id, collectionExpr);
618
- }
619
- /**
620
- * An in-memory source's lifecycle when it is pinned: its one snapshot is its
621
- * id. Built as real `East.function`s over the id, never spliced.
622
- *
623
- * @param id - The source's identity — its snapshot
624
- * @returns `revision` and `refresh`, at {@link PinnedSourceType}'s types
625
- */
626
- function fixedSnapshot(id) {
627
- const revision = East.function([], OptionType(StringType), ($) => {
628
- const snapshot = $.const(id, StringType);
629
- return some(snapshot);
630
- });
631
- const refresh = East.function([OptionType(StringType)], NullType, ($, target) => {
632
- const snapshot = $.const(id, StringType);
633
- $.match(target, {
634
- some: ($2, hash) => {
635
- $2.if(hash.notEqual(snapshot), ($3) => {
636
- $3.error("Paged.pinned: an immutable snapshot — create a source with the requested snapshot id");
637
- });
638
- },
639
- none: () => { },
640
- });
641
- return null;
642
- });
643
- return { revision, refresh };
644
- }
645
- /**
646
- * The window size a source actually serves for a requested `limit` — the
647
- * request itself, or `pageLimit` when that is smaller. Reified once as a real
648
- * East function (the `shared/reify` rule), so a page body CALLS it instead of
649
- * branching on host state. `none` ⇒ the source never trims.
650
- */
651
- function servedLimitFn(pageLimit) {
652
- const capValue = pageLimit === undefined ? none : some(BigInt(pageLimit));
653
- return East.function([IntegerType], IntegerType, ($, limit) => {
654
- const cap = $.const(capValue, OptionType(IntegerType));
655
- return cap.match({
656
- some: (_$, c) => limit.less(c).ifElse(() => limit, () => c),
657
- none: (_$) => limit,
658
- });
659
- });
660
- }
661
- /**
662
- * The KEYED in-memory source — dict windows in canonical key order, with
663
- * `seek` derived from the keys themselves (a keyed collection needs no key
664
- * accessor: it already is one). `pinned` adds its {@link fixedSnapshot}.
665
- */
666
- function keyedPagedOf(id, collection, pageLimit, pinned) {
667
- const all = collection;
668
- const valueType = Expr.type(all).value;
669
- const dictType = DictType(StringType, valueType);
670
- const entryType = StructType({ key: StringType, value: valueType });
671
- const entriesType = ArrayType(entryType);
672
- // Built OUTSIDE every block (an East macro inside one splices per use).
673
- const keyOfEntry = East.function([entryType], StringType, (_$, e) => e.key);
674
- const servedLimit = servedLimitFn(pageLimit);
675
- const page = East.function([IntegerType, IntegerType], OptionType(dictType), ($, offset, limit) => {
676
- const src = $.const(all, dictType);
677
- const served = $.const(servedLimit);
678
- // `toArray` walks the dictionary in key order, so the window a given
679
- // offset serves is the window a keyed dataset would serve.
680
- const entries = $.let(src.toArray(($2, v, k) => $2.const({ key: k, value: v }, entryType)), entriesType);
681
- const n = $.let(entries.length(), IntegerType);
682
- const start = $.let(offset.less(n).ifElse(() => offset, () => n), IntegerType);
683
- const rawEnd = $.let(offset.add(served(limit)), IntegerType);
684
- const end = $.let(rawEnd.less(n).ifElse(() => rawEnd, () => n), IntegerType);
685
- // In-memory windows are never in flight; an exhausted offset yields
686
- // the EMPTY window, which is how a walking reader terminates.
687
- const win = $.let(entries.slice(start, end), entriesType);
688
- return some(win.toDict((_$, e) => e.key, (_$, e) => e.value));
689
- });
690
- const total = East.function([], OptionType(IntegerType), ($) => {
691
- const src = $.const(all, dictType);
692
- return some(src.size());
693
- });
694
- // Key order ⇒ the first entry at-or-after the query starts the matching
695
- // run, and the run is contiguous, so counting the entries that still carry
696
- // the prefix counts exactly the matches. Both ranges are real
697
- // `East.function`s built OUTSIDE every block, then CALLED per arm.
698
- const prefixRange = East.function([entriesType, StringType], SeekRangeType, ($, entries, p) => {
699
- const by = $.const(keyOfEntry);
700
- const first = $.let(entries.findSortedFirst(p, by), IntegerType);
701
- const tail = $.let(entries.slice(first, entries.length()), entriesType);
702
- const matched = $.let(tail.filter((_$, e) => e.key.startsWith(p)), entriesType);
703
- const count = $.let(matched.length(), IntegerType);
704
- return $.let({ found: count.greater(0n), row: first, count }, SeekRangeType);
705
- });
706
- const exactRange = East.function([entriesType, StringType], SeekRangeType, ($, entries, k) => {
707
- const by = $.const(keyOfEntry);
708
- const first = $.let(entries.findSortedFirst(k, by), IntegerType);
709
- const tail = $.let(entries.slice(first, entries.length()), entriesType);
710
- const matched = $.let(tail.filter((_$, e) => East.equal(e.key, k)), entriesType);
711
- const count = $.let(matched.length(), IntegerType);
712
- return $.let({ found: count.greater(0n), row: first, count }, SeekRangeType);
713
- });
714
- // A half-open bound. These keys are Strings, which flatten to ONE leaf, so
715
- // only the first literal of each side applies; an empty side is open.
716
- const boundedRange = East.function([entriesType, ArrayType(StringType), ArrayType(StringType)], SeekRangeType, ($, entries, from, to) => {
717
- const by = $.const(keyOfEntry);
718
- const n = $.let(entries.length(), IntegerType);
719
- const first = $.let(0n, IntegerType);
720
- $.if(from.length().greater(0n), ($2) => {
721
- $2.assign(first, entries.findSortedFirst(from.get(0n).parse(StringType), by));
722
- });
723
- const last = $.let(n, IntegerType);
724
- $.if(to.length().greater(0n), ($2) => {
725
- $2.assign(last, entries.findSortedFirst(to.get(0n).parse(StringType), by));
726
- });
727
- const count = $.let(0n, IntegerType);
728
- $.if(last.greater(first), ($2) => {
729
- $2.assign(count, last.subtract(first));
730
- });
731
- return $.let({ found: count.greater(0n), row: first, count }, SeekRangeType);
732
- });
733
- const find = East.function([SeekQueryType], OptionType(SeekRangeType), ($, query) => {
734
- const src = $.const(all, dictType);
735
- const entries = $.let(src.toArray(($2, v, k) => $2.const({ key: k, value: v }, entryType)), entriesType);
736
- const prefixOf = $.const(prefixRange);
737
- const exactOf = $.const(exactRange);
738
- const boundedOf = $.const(boundedRange);
739
- // In-memory sources are never in flight, so every arm resolves to
740
- // `some` immediately — `none` is reserved for a fetch in progress.
741
- return query.match({
742
- prefix: ($2, p) => $2.const(some(prefixOf(entries, p)), OptionType(SeekRangeType)),
743
- range: ($2, r) => $2.const(some(boundedOf(entries, r.from, r.to)), OptionType(SeekRangeType)),
744
- // The whole-key `.east` literal of a String key is its quoted text.
745
- key: ($2, literal) => $2.const(some(exactOf(entries, literal.parse(StringType))), OptionType(SeekRangeType)),
746
- // Leading FIELDS address a struct key; these keys are Strings, so a
747
- // leading-field query can only match when it names none of them —
748
- // then it is just its prefix.
749
- fields: ($2, f) => {
750
- const empty = $2.const({ found: false, row: 0n, count: 0n }, SeekRangeType);
751
- const range = $2.let(f.values.length().equal(0n).ifElse(() => f.prefix.match({
752
- some: ($3, p) => $3.const(prefixOf(entries, p), SeekRangeType),
753
- none: ($3) => $3.const(prefixOf(entries, ""), SeekRangeType),
754
- }), () => empty), SeekRangeType);
755
- return $2.const(some(range), OptionType(SeekRangeType));
756
- },
757
- });
758
- });
759
- const seek = some(find);
760
- const members = { id, page, total, seek };
761
- return (pinned
762
- ? East.value({ ...members, ...fixedSnapshot(id) }, PinnedSourceType(dictType))
763
- : East.value(members, PagedSourceType(dictType)));
764
- }
765
- /** The POSITIONAL in-memory source — array windows in stream order.
766
- * `pinned` adds its {@link fixedSnapshot}. */
767
- function arrayPagedOf(id, rows, options, pinned) {
768
- const all = rows;
769
- const rowType = Expr.type(all).value;
770
- const rowsType = ArrayType(rowType);
771
- // Built OUTSIDE every block: constructing East IR inside one is an
772
- // authoring-time macro. Inside, each body binds it once with `$.const`.
773
- const keyOf = options?.key;
774
- const byFn = keyOf === undefined
775
- ? undefined
776
- : East.function([rowType], StringType, (_$, r) => keyOf(r));
777
- const servedLimit = servedLimitFn(options?.pageLimit);
778
- const page = East.function([IntegerType, IntegerType], OptionType(rowsType), ($, offset, limit) => {
779
- const src = $.const(all, rowsType);
780
- const served = $.const(servedLimit);
781
- const n = $.let(src.length(), IntegerType);
782
- const start = $.let(offset.less(n).ifElse(() => offset, () => n), IntegerType);
783
- const rawEnd = $.let(offset.add(served(limit)), IntegerType);
784
- const end = $.let(rawEnd.less(n).ifElse(() => rawEnd, () => n), IntegerType);
785
- // In-memory windows are never in flight; an exhausted offset yields
786
- // the EMPTY window, which is how a walking reader terminates.
787
- const win = $.let(src.slice(start, end), rowsType);
788
- return some(win);
789
- });
790
- const total = East.function([], OptionType(IntegerType), ($) => {
791
- const src = $.const(all, rowsType);
792
- return some(src.length());
793
- });
794
- // Sorted by key ⇒ the first row at-or-after the query starts the matching
795
- // run, and the run is contiguous, so counting the rows that still carry the
796
- // prefix counts exactly the matches. Built as real `East.function`s outside
797
- // every block, then CALLED per query arm.
798
- const prefixRange = byFn === undefined ? undefined
799
- : East.function([rowsType, StringType], SeekRangeType, ($, rows, p) => {
800
- const by = $.const(byFn);
801
- const first = $.let(rows.findSortedFirst(p, by), IntegerType);
802
- const tail = $.let(rows.slice(first, rows.length()), rowsType);
803
- const matched = $.let(tail.filter((_$, r) => by(r).startsWith(p)), rowsType);
804
- const count = $.let(matched.length(), IntegerType);
805
- return $.let({ found: count.greater(0n), row: first, count }, SeekRangeType);
806
- });
807
- const exactRange = byFn === undefined ? undefined
808
- : East.function([rowsType, StringType], SeekRangeType, ($, rows, k) => {
809
- const by = $.const(byFn);
810
- const first = $.let(rows.findSortedFirst(k, by), IntegerType);
811
- const tail = $.let(rows.slice(first, rows.length()), rowsType);
812
- const matched = $.let(tail.filter((_$, r) => East.equal(by(r), k)), rowsType);
813
- const count = $.let(matched.length(), IntegerType);
814
- return $.let({ found: count.greater(0n), row: first, count }, SeekRangeType);
815
- });
816
- // A half-open bound; the key accessor yields a String, which flattens to
817
- // ONE leaf, so only the first literal of each side applies.
818
- const boundedRange = byFn === undefined ? undefined
819
- : East.function([rowsType, ArrayType(StringType), ArrayType(StringType)], SeekRangeType, ($, rows, from, to) => {
820
- const by = $.const(byFn);
821
- const n = $.let(rows.length(), IntegerType);
822
- const first = $.let(0n, IntegerType);
823
- $.if(from.length().greater(0n), ($2) => {
824
- $2.assign(first, rows.findSortedFirst(from.get(0n).parse(StringType), by));
825
- });
826
- const last = $.let(n, IntegerType);
827
- $.if(to.length().greater(0n), ($2) => {
828
- $2.assign(last, rows.findSortedFirst(to.get(0n).parse(StringType), by));
829
- });
830
- const count = $.let(0n, IntegerType);
831
- $.if(last.greater(first), ($2) => {
832
- $2.assign(count, last.subtract(first));
833
- });
834
- return $.let({ found: count.greater(0n), row: first, count }, SeekRangeType);
835
- });
836
- const seek = prefixRange === undefined || exactRange === undefined || boundedRange === undefined
837
- ? East.value(none, OptionType(FunctionType([SeekQueryType], OptionType(SeekRangeType))))
838
- : some(East.function([SeekQueryType], OptionType(SeekRangeType), ($, query) => {
839
- const src = $.const(all, rowsType);
840
- const prefixOf = $.const(prefixRange);
841
- const exactOf = $.const(exactRange);
842
- const boundedOf = $.const(boundedRange);
843
- return query.match({
844
- prefix: ($2, p) => $2.const(some(prefixOf(src, p)), OptionType(SeekRangeType)),
845
- range: ($2, r) => $2.const(some(boundedOf(src, r.from, r.to)), OptionType(SeekRangeType)),
846
- key: ($2, literal) => $2.const(some(exactOf(src, literal.parse(StringType))), OptionType(SeekRangeType)),
847
- fields: ($2, f) => {
848
- const empty = $2.const({ found: false, row: 0n, count: 0n }, SeekRangeType);
849
- const range = $2.let(f.values.length().equal(0n).ifElse(() => f.prefix.match({
850
- some: ($3, p) => $3.const(prefixOf(src, p), SeekRangeType),
851
- none: ($3) => $3.const(prefixOf(src, ""), SeekRangeType),
852
- }), () => empty), SeekRangeType);
853
- return $2.const(some(range), OptionType(SeekRangeType));
854
- },
855
- });
856
- }));
857
- // Two-step cast (the `Data.bindPaged` idiom): the members are built
858
- // against the row type recovered from the expression, which TS sees as the
859
- // erased `EastType` rather than the caller's `R`. The East-side type —
860
- // `PagedSourceType(rowsType)`, or `PinnedSourceType(rowsType)` — is what
861
- // actually types the value.
862
- const members = { id, page, total, seek };
863
- return (pinned
864
- ? East.value({ ...members, ...fixedSnapshot(id) }, PinnedSourceType(rowsType))
865
- : East.value(members, PagedSourceType(rowsType)));
866
- }
608
+ // ============================================================================
609
+ // The namespace — the contract's types
610
+ // ============================================================================
867
611
  /**
868
- * The `Paged` namespace — building a {@link PagedSourceType} or a
869
- * {@link PinnedSourceType} without a server.
612
+ * The `Paged` namespace — the row-source contract's East types.
870
613
  *
871
614
  * @remarks
872
- * The contract itself is what components consume; this namespace is the
873
- * in-memory producer. Production sources come from the platform that owns the
874
- * data (`Data.bindPaged` in `@elaraai/e3-ui`).
615
+ * Paged data is BOUND: a component's paged `data` comes from the platform
616
+ * that owns the data — `Data.bindPaged` in `@elaraai/e3-ui`, whose handle is a
617
+ * {@link PinnedSourceType} — and east-ui produces none. These are the types
618
+ * that producer meets, for annotating a `$.let` / `$.const` that holds a
619
+ * handle, or a component prop that takes one; a component recognises a source
620
+ * by them ({@link resolveRowSource}).
875
621
  */
876
622
  export const Paged = {
877
- /** Build an in-memory paged source over a collection already in hand. */
878
- of: createPagedOf,
879
- /** Build an in-memory PINNED source — `of`, naming its one snapshot, for a fixture a view edits or follows. */
880
- pinned: createPagedPinned,
881
623
  /** East types — the contract, for `$.const` / `$.let` annotations. */
882
624
  Types: {
883
- /** A windowed row source over a collection type. */
625
+ /**
626
+ * A windowed row source over a collection type that names no
627
+ * snapshot — the shape that predates `revision` / `refresh`.
628
+ *
629
+ * @remarks See {@link PagedSourceType} for its fields.
630
+ * @property id - The source's comparable identity
631
+ * @property page - `(offset, limit)` → that window, `none` while in flight
632
+ * @property total - The element count, once known
633
+ * @property seek - Key search, `none` when the source is not key-ordered
634
+ */
884
635
  Source: PagedSourceType,
885
- /** A windowed row source that names its snapshot, over a collection type. */
636
+ /**
637
+ * The contract — a windowed row source that names its snapshot, over a
638
+ * collection type: what `Data.bindPaged` returns.
639
+ *
640
+ * @remarks See {@link PinnedSourceType} for its fields.
641
+ * @property id - The logical source's identity
642
+ * @property page - `(offset, limit)` → that window at the current revision
643
+ * @property total - The element count at the current revision
644
+ * @property seek - Key search at the current revision
645
+ * @property revision - The snapshot the source serves
646
+ * @property refresh - Move the source to a snapshot, or to its current one
647
+ */
886
648
  PinnedSource: PinnedSourceType,
887
- /** Where a key query landed in a source's row order. */
649
+ /**
650
+ * Where a key query landed in a source's row order.
651
+ *
652
+ * @remarks See {@link SeekRangeType}.
653
+ * @property found - Whether any row matched
654
+ * @property row - The first matched row, or the insertion row on a miss
655
+ * @property count - The number of matched rows
656
+ */
888
657
  SeekRange: SeekRangeType,
889
- /** A key query — exact literal, String prefix, leading struct fields, or a range. */
658
+ /**
659
+ * A key query — exact literal, String prefix, leading struct fields, or
660
+ * a range.
661
+ *
662
+ * @remarks See {@link SeekQueryType}.
663
+ * @property key - A whole-key `.east` literal
664
+ * @property prefix - A String prefix
665
+ * @property fields - Exact leading struct-key fields, then an optional prefix
666
+ * @property range - A half-open bound on the key's flattened fields
667
+ */
890
668
  SeekQuery: SeekQueryType,
891
- /** How a component's rows arrive (inline / paged / pinned), at a collection type. */
669
+ /**
670
+ * How a component's rows arrive, at a collection type.
671
+ *
672
+ * @remarks See {@link RowSourceType}.
673
+ * @property inline - The whole collection, already in hand
674
+ * @property paged - A {@link PagedSourceType}
675
+ * @property pinned - A {@link PinnedSourceType}
676
+ */
892
677
  RowSource: RowSourceType,
893
678
  },
894
679
  };