@elaraai/east-ui 1.0.84 → 1.0.85
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/dist/src/collections/plan/root.js +5 -7
- package/dist/src/collections/plan/root.js.map +1 -1
- package/dist/src/collections/plan/types.d.ts +1 -1
- package/dist/src/collections/plan/types.js +1 -1
- package/dist/src/collections/sheet/editing-bridge.js +1 -1
- package/dist/src/collections/sheet/editing-bridge.js.map +1 -1
- package/dist/src/collections/sheet/root.d.ts +1 -1
- package/dist/src/collections/sheet/root.js +1 -1
- package/dist/src/collections/sheet/root.js.map +1 -1
- package/dist/src/contracts/index.d.ts +1 -1
- package/dist/src/contracts/index.d.ts.map +1 -1
- package/dist/src/contracts/pick.js +4 -4
- package/dist/src/contracts/pick.js.map +1 -1
- package/dist/src/contracts/source.d.ts +87 -131
- package/dist/src/contracts/source.d.ts.map +1 -1
- package/dist/src/contracts/source.js +153 -368
- package/dist/src/contracts/source.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +2 -3
- package/dist/src/index.js.map +1 -1
- package/dist/src/internal.d.ts.map +1 -1
- package/dist/src/internal.js +2 -1
- package/dist/src/internal.js.map +1 -1
- package/dist/src/style/typography.d.ts +18 -18
- package/dist/src/style/typography.js +18 -18
- package/dist/test/collections/index.d.ts +0 -1
- package/dist/test/collections/index.d.ts.map +1 -1
- package/dist/test/collections/index.js +0 -1
- package/dist/test/collections/index.js.map +1 -1
- package/dist/test/collections/sheet.examples.d.ts +0 -7
- package/dist/test/collections/sheet.examples.d.ts.map +1 -1
- package/dist/test/collections/sheet.examples.js +4 -31
- package/dist/test/collections/sheet.examples.js.map +1 -1
- package/dist/test/collections/table.examples.d.ts +0 -7
- package/dist/test/collections/table.examples.d.ts.map +1 -1
- package/dist/test/collections/table.examples.js +1 -22
- package/dist/test/collections/table.examples.js.map +1 -1
- package/package.json +5 -9
- package/dist/test/collections/paged-source.examples.d.ts +0 -14
- package/dist/test/collections/paged-source.examples.d.ts.map +0 -1
- package/dist/test/collections/paged-source.examples.js +0 -252
- 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;
|
|
13
|
-
*
|
|
14
|
-
* (dataset windows over an e3 workspace) and
|
|
15
|
-
*
|
|
16
|
-
*
|
|
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,
|
|
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
|
|
200
|
-
*
|
|
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
|
-
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
* @
|
|
324
|
-
* @
|
|
325
|
-
*
|
|
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
|
|
329
|
-
const
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
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
|
|
348
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
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
|
-
|
|
371
|
-
|
|
372
|
-
|
|
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 (
|
|
378
|
-
throw new Error(`${label}:
|
|
379
|
-
`
|
|
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
|
-
|
|
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
|
-
|
|
395
|
-
|
|
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 (
|
|
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
|
-
//
|
|
489
|
-
//
|
|
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", {
|
|
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
|
-
|
|
582
|
-
|
|
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 —
|
|
869
|
-
* {@link PinnedSourceType} without a server.
|
|
612
|
+
* The `Paged` namespace — the row-source contract's East types.
|
|
870
613
|
*
|
|
871
614
|
* @remarks
|
|
872
|
-
*
|
|
873
|
-
*
|
|
874
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
};
|