@elaraai/east-ui 1.0.97 → 1.0.98

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 (99) hide show
  1. package/dist/src/collections/index.d.ts +0 -1
  2. package/dist/src/collections/index.d.ts.map +1 -1
  3. package/dist/src/collections/index.js +0 -1
  4. package/dist/src/collections/index.js.map +1 -1
  5. package/dist/src/component.d.ts +0 -999
  6. package/dist/src/component.d.ts.map +1 -1
  7. package/dist/src/component.js +0 -44
  8. package/dist/src/component.js.map +1 -1
  9. package/dist/src/contracts/drag.d.ts +2 -2
  10. package/dist/src/contracts/drag.js +2 -2
  11. package/dist/src/contracts/editing.d.ts +4 -0
  12. package/dist/src/contracts/editing.d.ts.map +1 -1
  13. package/dist/src/contracts/editing.js +2 -1
  14. package/dist/src/contracts/editing.js.map +1 -1
  15. package/dist/src/index.d.ts +2 -1
  16. package/dist/src/index.d.ts.map +1 -1
  17. package/dist/src/index.js +4 -1
  18. package/dist/src/index.js.map +1 -1
  19. package/dist/src/internal.d.ts +4 -1
  20. package/dist/src/internal.d.ts.map +1 -1
  21. package/dist/src/internal.js +5 -2
  22. package/dist/src/internal.js.map +1 -1
  23. package/dist/src/layout/snap-grid/index.d.ts +1 -0
  24. package/dist/src/layout/snap-grid/index.d.ts.map +1 -1
  25. package/dist/src/pick/panel/index.js +1 -1
  26. package/dist/src/pick/panel/index.js.map +1 -1
  27. package/dist/src/platform/slice/index.d.ts +4 -7
  28. package/dist/src/platform/slice/index.d.ts.map +1 -1
  29. package/dist/src/platform/slice/index.js +5 -8
  30. package/dist/src/platform/slice/index.js.map +1 -1
  31. package/dist/src/platform/slice/record-area.d.ts +181 -0
  32. package/dist/src/platform/slice/record-area.d.ts.map +1 -0
  33. package/dist/src/platform/slice/record-area.js +304 -0
  34. package/dist/src/platform/slice/record-area.js.map +1 -0
  35. package/dist/src/platform/slice/record-chrome.d.ts +138 -0
  36. package/dist/src/platform/slice/record-chrome.d.ts.map +1 -1
  37. package/dist/src/platform/slice/record-plan.d.ts +34 -1
  38. package/dist/src/platform/slice/record-plan.d.ts.map +1 -1
  39. package/dist/src/platform/slice/record-plan.js +126 -3
  40. package/dist/src/platform/slice/record-plan.js.map +1 -1
  41. package/dist/src/platform/slice/record-series.d.ts +166 -0
  42. package/dist/src/platform/slice/record-series.d.ts.map +1 -0
  43. package/dist/src/platform/slice/record-series.js +258 -0
  44. package/dist/src/platform/slice/record-series.js.map +1 -0
  45. package/dist/src/platform/slice/record-types.d.ts +1862 -13
  46. package/dist/src/platform/slice/record-types.d.ts.map +1 -1
  47. package/dist/src/platform/slice/record-types.js +50 -4
  48. package/dist/src/platform/slice/record-types.js.map +1 -1
  49. package/dist/src/platform/slice/record.d.ts +17 -2
  50. package/dist/src/platform/slice/record.d.ts.map +1 -1
  51. package/dist/src/platform/slice/record.js +62 -9
  52. package/dist/src/platform/slice/record.js.map +1 -1
  53. package/dist/src/runtime/collections/index.d.ts +0 -1
  54. package/dist/src/runtime/collections/index.d.ts.map +1 -1
  55. package/dist/src/runtime/collections/index.js +0 -1
  56. package/dist/src/runtime/collections/index.js.map +1 -1
  57. package/dist/src/runtime/record-slice-source.d.ts +298 -0
  58. package/dist/src/runtime/record-slice-source.d.ts.map +1 -1
  59. package/dist/src/runtime/record-slice-source.js +554 -16
  60. package/dist/src/runtime/record-slice-source.js.map +1 -1
  61. package/dist/src/runtime/record-slice.d.ts +87 -3
  62. package/dist/src/runtime/record-slice.d.ts.map +1 -1
  63. package/dist/src/runtime/record-slice.js +175 -12
  64. package/dist/src/runtime/record-slice.js.map +1 -1
  65. package/dist/src/slice/record/index.d.ts +69 -0
  66. package/dist/src/slice/record/index.d.ts.map +1 -1
  67. package/dist/src/style/color.d.ts +237 -0
  68. package/dist/src/style/color.d.ts.map +1 -0
  69. package/dist/src/style/color.js +105 -0
  70. package/dist/src/style/color.js.map +1 -0
  71. package/dist/src/style/namespace.d.ts +151 -1
  72. package/dist/src/style/namespace.d.ts.map +1 -1
  73. package/dist/src/style/namespace.js +9 -1
  74. package/dist/src/style/namespace.js.map +1 -1
  75. package/dist/src/style.d.ts +3 -0
  76. package/dist/src/style.d.ts.map +1 -1
  77. package/dist/src/style.js +3 -0
  78. package/dist/src/style.js.map +1 -1
  79. package/dist/test/style.examples.d.ts +1 -0
  80. package/dist/test/style.examples.d.ts.map +1 -1
  81. package/dist/test/style.examples.js +17 -2
  82. package/dist/test/style.examples.js.map +1 -1
  83. package/package.json +4 -4
  84. package/dist/src/collections/schematic/index.d.ts +0 -1390
  85. package/dist/src/collections/schematic/index.d.ts.map +0 -1
  86. package/dist/src/collections/schematic/index.js +0 -815
  87. package/dist/src/collections/schematic/index.js.map +0 -1
  88. package/dist/src/collections/schematic/types.d.ts +0 -1183
  89. package/dist/src/collections/schematic/types.d.ts.map +0 -1
  90. package/dist/src/collections/schematic/types.js +0 -724
  91. package/dist/src/collections/schematic/types.js.map +0 -1
  92. package/dist/src/runtime/collections/schematic.d.ts +0 -73
  93. package/dist/src/runtime/collections/schematic.d.ts.map +0 -1
  94. package/dist/src/runtime/collections/schematic.js +0 -69
  95. package/dist/src/runtime/collections/schematic.js.map +0 -1
  96. package/dist/test/collections/schematic.examples.d.ts +0 -23
  97. package/dist/test/collections/schematic.examples.d.ts.map +0 -1
  98. package/dist/test/collections/schematic.examples.js +0 -602
  99. package/dist/test/collections/schematic.examples.js.map +0 -1
@@ -3,21 +3,59 @@
3
3
  * Dual-licensed under AGPL-3.0 and commercial license. See LICENSE for details.
4
4
  */
5
5
  /** Bounded windows over disjoint index ranges; never examines a record row. */
6
- import { ArrayType, none, printFor, some } from "@elaraai/east";
6
+ import { ArrayType, IntegerType, compareFor, none, printFor, some } from "@elaraai/east";
7
7
  import { SeekQueryType } from "../contracts/source.js";
8
8
  const printQueries = printFor(ArrayType(SeekQueryType));
9
9
  const printQuery = printFor(SeekQueryType);
10
+ const rowOrder = compareFor(IntegerType);
10
11
  const MAX_CACHED_RUNS = 128;
11
- /** Searches whose bounds are kept per revision; a window over an interval index is some fifty-five. */
12
- const MAX_LANDED_SEARCHES = 1024;
12
+ /** Cells whose runs are kept per revision (#1039): a reader wanting more than a batch of searches reads a summary. */
13
+ const MAX_CACHED_CELLS = 1024;
14
+ /**
15
+ * Searches whose bounds are kept per revision; a window over an interval index is some fifty-five. A reader that keeps
16
+ * its searches within it — the Schematic's window across a led area index's choices (§10.4) — never asks one twice.
17
+ */
18
+ export const MAX_LANDED_SEARCHES = 1024;
19
+ /**
20
+ * Batches of cells' searches, and reads of their runs, kept while in flight per
21
+ * revision (#1039): past it the oldest is let go, and a cell waiting on it is
22
+ * asked again.
23
+ */
24
+ const MAX_ASKED = 32;
13
25
  const MAX_CACHED_PAGES = 32;
14
26
  const MAX_PAGE_ROWS = 2048n;
27
+ /** The most searches one batch asks (#1351): e3 answers at most 10,000 in a
28
+ * request, so a plan with more is asked as several batches, all at once. */
29
+ export const MAX_RECORD_SLICE_BATCH = 10_000;
15
30
  function cache(map, key, value, cap) {
16
31
  map.delete(key);
17
32
  map.set(key, value);
18
33
  if (map.size > cap)
19
34
  map.delete(map.keys().next().value);
20
35
  }
36
+ /**
37
+ * A window's part of each run it covers: elements `[offset, offset + limit)`
38
+ * of the union, at most {@link MAX_PAGE_ROWS} of them, as ranges of the
39
+ * source's rows.
40
+ */
41
+ function windowRuns(result, offset, limit) {
42
+ const runs = [];
43
+ let skip = offset;
44
+ let remaining = limit < MAX_PAGE_ROWS ? limit : MAX_PAGE_ROWS;
45
+ for (const run of result.runs) {
46
+ if (remaining === 0n)
47
+ break;
48
+ if (skip >= run.count) {
49
+ skip -= run.count;
50
+ continue;
51
+ }
52
+ const count = run.count - skip < remaining ? run.count - skip : remaining;
53
+ runs.push({ found: true, row: run.row + skip, count });
54
+ remaining -= count;
55
+ skip = 0n;
56
+ }
57
+ return runs;
58
+ }
21
59
  /**
22
60
  * Adapts an indexed source to the pinned-source paging contract. Work depends
23
61
  * on the selected ranges and requested window, not the record's size. The
@@ -28,6 +66,20 @@ function cache(map, key, value, cap) {
28
66
  * bounds are kept once they land, per revision, so a selection read again
29
67
  * while some of its searches are pending asks the source only for those.
30
68
  *
69
+ * A reader that batches (#1351) is asked a plan's searches in one request
70
+ * (`seekAll`, a batch per {@link MAX_RECORD_SLICE_BATCH} of them, all at once)
71
+ * and a window across several runs in one (`runsAll`): a window costs two
72
+ * requests however many ranges its narrowing selects. A window inside one run
73
+ * is the page it is, read as one.
74
+ *
75
+ * The cells of a series or a summary index (#1039) are read the same way: the
76
+ * searches of every cell a reader wants in one request, and the entries of
77
+ * every cell it reads in one more, each cell's runs kept per revision. A cell
78
+ * whose searches, or a run whose entries, a request still in flight asks waits
79
+ * on that request — asked again as it was, which asks the reader nothing new —
80
+ * so a reader wanting other cells while one read is in flight asks only for
81
+ * what no request asks.
82
+ *
31
83
  * @typeParam T - An entry; its contents are opaque to this adapter.
32
84
  */
33
85
  export class RecordSliceSource {
@@ -35,23 +87,70 @@ export class RecordSliceSource {
35
87
  revision;
36
88
  resolved = new Map();
37
89
  landed = new Map();
90
+ cells = new Map();
91
+ askedCells = [];
92
+ askedRuns = [];
38
93
  pages = new Map();
39
94
  pageCount = 0;
40
95
  /** @param reader - Existing indexed-source reads, sharing one revision. */
41
96
  constructor(reader) {
42
97
  this.reader = reader;
43
98
  }
99
+ /**
100
+ * The record's revision now, and what is kept let go when it moves. A
101
+ * revision not known for the moment — read again, say — lets nothing go:
102
+ * what was kept is the revision's it was kept at, and stands for it again
103
+ * when that revision is known again.
104
+ */
44
105
  current() {
45
106
  const revision = this.reader.revision();
46
- const next = revision.type === "some" ? revision.value : undefined;
47
- if (next !== this.revision) {
107
+ if (revision.type !== "some")
108
+ return undefined;
109
+ if (revision.value !== this.revision) {
48
110
  this.resolved.clear();
49
111
  this.landed.clear();
112
+ this.cells.clear();
113
+ this.askedCells.splice(0);
114
+ this.askedRuns.splice(0);
50
115
  this.pages.clear();
51
116
  this.pageCount = 0;
52
- this.revision = next;
117
+ this.revision = revision.value;
118
+ }
119
+ return revision.value;
120
+ }
121
+ /**
122
+ * Several searches of one source, in order: in one request when the reader
123
+ * batches them (a batch for each {@link MAX_RECORD_SLICE_BATCH}, all asked
124
+ * in this pass), else each its own search, all started together.
125
+ *
126
+ * @returns Each search's bounds, in order; none while any is pending
127
+ */
128
+ searchAll(index, queries) {
129
+ const ranges = [];
130
+ let pending = false;
131
+ if (this.reader.seekAll !== undefined) {
132
+ for (let at = 0; at < queries.length; at += MAX_RECORD_SLICE_BATCH) {
133
+ const batch = this.reader.seekAll(index, queries.slice(at, at + MAX_RECORD_SLICE_BATCH));
134
+ if (batch.type === "none")
135
+ pending = true;
136
+ else
137
+ ranges.push(...batch.value);
138
+ }
139
+ }
140
+ else {
141
+ for (const query of queries) {
142
+ const range = this.search(index, query);
143
+ if (range.type === "none")
144
+ pending = true;
145
+ else
146
+ ranges.push(range.value);
147
+ }
53
148
  }
54
- return next;
149
+ if (pending)
150
+ return none;
151
+ if (ranges.length !== queries.length)
152
+ throw new Error("The source answered a different number of searches than it was asked");
153
+ return some(ranges);
55
154
  }
56
155
  /** One search's bounds: kept once they land, so a read again asks the source only while they are pending. */
57
156
  search(index, query) {
@@ -88,15 +187,13 @@ export class RecordSliceSource {
88
187
  ranges.push({ found: total.value > 0n, row: 0n, count: total.value });
89
188
  }
90
189
  else {
91
- // Start independent requests together; never wait for the first
92
- // request before starting the next selected value's search.
93
- for (const query of plan.queries) {
94
- const range = this.search(plan.index, query);
95
- if (range.type === "none")
96
- pending = true;
97
- else
98
- ranges.push(range.value);
99
- }
190
+ // One request for every search the source does not hold (#1351), or
191
+ // independent requests started together, never one after another.
192
+ const found = this.searchAll(plan.index, plan.queries);
193
+ if (found.type === "none")
194
+ pending = true;
195
+ else
196
+ ranges.push(...found.value);
100
197
  }
101
198
  if (this.current() !== revision || pending)
102
199
  return none;
@@ -120,6 +217,369 @@ export class RecordSliceSource {
120
217
  cache(this.resolved, key, result, MAX_CACHED_RUNS);
121
218
  return some(result);
122
219
  }
220
+ /**
221
+ * Find several cells of a series or a summary index (#1039): each cell's
222
+ * searches' bounds and how many entries they hold — exact, before one is
223
+ * read. The searches of every cell not yet found go in one request (a batch
224
+ * for each {@link MAX_RECORD_SLICE_BATCH} of them, all asked in this pass),
225
+ * and a cell found is kept for its revision, so one asked again asks
226
+ * nothing. A cell a request still in flight asks waits on it: that request
227
+ * is asked again as it was, and only the cells no request asks go in a new
228
+ * one.
229
+ *
230
+ * @param index - The index the cells are of
231
+ * @param cells - The cells
232
+ * @returns Each cell's runs, by its name; none while any is pending
233
+ * @throws {Error} When two cells share a name, or the source answers a search with bounds no search has
234
+ */
235
+ cellRuns(index, cells) {
236
+ const revision = this.current();
237
+ if (revision === undefined)
238
+ return none;
239
+ const found = new Map();
240
+ // The cells not yet found, by their searches' text: each one's searches, and the names it is asked under.
241
+ const missing = new Map();
242
+ const named = new Set();
243
+ for (const cell of cells) {
244
+ if (named.has(cell.id))
245
+ throw new Error(`Two cells are named '${cell.id}'`);
246
+ named.add(cell.id);
247
+ const key = `${index}:${printQueries([...cell.queries])}`;
248
+ const known = this.cells.get(key);
249
+ if (known !== undefined) {
250
+ cache(this.cells, key, known, MAX_CACHED_CELLS);
251
+ found.set(cell.id, known);
252
+ continue;
253
+ }
254
+ const other = missing.get(key);
255
+ if (other === undefined)
256
+ missing.set(key, { queries: cell.queries, ids: [cell.id] });
257
+ else
258
+ other.ids.push(cell.id);
259
+ }
260
+ if (missing.size === 0)
261
+ return some(found);
262
+ // Each waits on a request in flight that asks it; the rest are asked together, in one more.
263
+ const batches = new Set();
264
+ const fresh = [];
265
+ for (const [key, { queries }] of missing) {
266
+ const asked = this.askedCells.find((batch) => batch.index === index && batch.cells.some((cell) => cell.key === key));
267
+ if (asked !== undefined)
268
+ batches.add(asked);
269
+ else
270
+ fresh.push({ key, queries });
271
+ }
272
+ if (fresh.length > 0) {
273
+ const batch = {
274
+ index, cells: fresh.map(({ key, queries }) => ({ key, size: queries.length })), queries: fresh.flatMap(({ queries }) => queries),
275
+ };
276
+ this.askedCells.push(batch);
277
+ if (this.askedCells.length > MAX_ASKED)
278
+ this.askedCells.shift();
279
+ batches.add(batch);
280
+ }
281
+ let pending = false;
282
+ for (const batch of batches) {
283
+ const ranges = this.searchAll(index, batch.queries);
284
+ if (this.current() !== revision)
285
+ return none;
286
+ if (ranges.type === "none") {
287
+ pending = true;
288
+ continue;
289
+ }
290
+ // Landed: every cell it asks is found, and kept for the revision.
291
+ const at = this.askedCells.indexOf(batch);
292
+ if (at >= 0)
293
+ this.askedCells.splice(at, 1);
294
+ let offset = 0;
295
+ for (const { key, size } of batch.cells) {
296
+ const own = ranges.value.slice(offset, offset + size);
297
+ offset += size;
298
+ let total = 0n;
299
+ for (const range of own) {
300
+ if (range.row < 0n || range.count < 0n || range.found !== (range.count > 0n))
301
+ throw new Error("The source returned invalid search bounds");
302
+ total += range.count;
303
+ }
304
+ const runs = { ranges: own, total };
305
+ cache(this.cells, key, runs, MAX_CACHED_CELLS);
306
+ for (const id of missing.get(key)?.ids ?? [])
307
+ found.set(id, runs);
308
+ }
309
+ }
310
+ return pending ? none : some(found);
311
+ }
312
+ /**
313
+ * Find the bounds of several searches of one source (#1393): where each one's
314
+ * entries sit and how many it holds, in one request (a batch for each
315
+ * {@link MAX_RECORD_SLICE_BATCH} of them, all asked in this pass), no entry
316
+ * read and nothing kept here — what a density raster asks of an area
317
+ * index's cells, its counted ranges and the coarser levels' keys
318
+ * (`areaRaster`), whose entries {@link RecordSliceSource.runEntries} reads
319
+ * while they are few. A read again answers from the reader's own channels
320
+ * for the searches that landed.
321
+ *
322
+ * @param index - The source searched
323
+ * @param queries - The searches
324
+ * @returns Each search's bounds, in order; none while any is pending
325
+ * @throws {Error} When the source answers a search with bounds no search has
326
+ */
327
+ bounds(index, queries) {
328
+ const revision = this.current();
329
+ if (revision === undefined)
330
+ return none;
331
+ const found = this.searchAll(index, queries);
332
+ if (this.current() !== revision || found.type === "none")
333
+ return none;
334
+ for (const range of found.value) {
335
+ if (range.row < 0n || range.count < 0n || range.found !== (range.count > 0n))
336
+ throw new Error("The source returned invalid search bounds");
337
+ }
338
+ return found;
339
+ }
340
+ /**
341
+ * Read the entries of some runs of one source (#1393): the runs as searches
342
+ * found them at the record's revision now — ascending, disjoint and none
343
+ * empty — read in one request however many there are, a run a read still in
344
+ * flight asks waiting on it. Nothing is kept: the reader holds what it read.
345
+ *
346
+ * @param index - The source
347
+ * @param runs - The runs
348
+ * @returns Their entries, run after run; none while the read is pending
349
+ * @throws {Error} When the runs are not ascending, disjoint and each non-empty, or the entries read disagree with them
350
+ */
351
+ runEntries(index, runs) {
352
+ return this.runsRead(index, runs, undefined, (asked) => this.readRuns(index, asked));
353
+ }
354
+ /**
355
+ * Read the rows another Dict holds under the keys the entries of some runs
356
+ * of one source name (#1393): the runs as {@link RecordSliceSource.runEntries}
357
+ * takes them, read in one request however many there are, a run a read
358
+ * still in flight asks waiting on it — each entry's key and the Dict's row
359
+ * under it, run after run. The keys are read where the source's entries
360
+ * are kept: none is sent. Nothing is kept: the reader holds what it read.
361
+ *
362
+ * @param index - The source
363
+ * @param runs - The runs
364
+ * @param dict - The Dict read: the `id` of its paged read
365
+ * @returns Each entry's key and its row there, run after run; none while the read is pending
366
+ * @throws {Error} When the reader reads no Dict by runs, the runs are not ascending, disjoint and each non-empty, or
367
+ * the rows read disagree with them
368
+ */
369
+ runJoins(index, runs, dict) {
370
+ return this.runsRead(index, runs, dict, (asked) => this.readJoins(index, asked, dict));
371
+ }
372
+ /** Some runs' entries, or their rows of a Dict, by `read`: the runs checked, each asked once (see {@link RecordSliceSource.runEntries}). */
373
+ runsRead(index, runs, dict, read) {
374
+ const revision = this.current();
375
+ if (revision === undefined)
376
+ return none;
377
+ for (const [i, run] of runs.entries()) {
378
+ if (run.row < 0n || run.count <= 0n)
379
+ throw new Error("A run read holds entries: its first row is in the source, and it holds at least one");
380
+ const before = runs[i - 1];
381
+ if (before !== undefined && before.row + before.count > run.row)
382
+ throw new Error("The runs read are ascending and disjoint");
383
+ }
384
+ const byStart = this.readAsked(index, dict, runs, revision, read);
385
+ if (byStart === undefined)
386
+ return none;
387
+ return some(runs.flatMap((run) => byStart.get(run.row)));
388
+ }
389
+ /**
390
+ * Read several cells' entries (#1039): the entries of every cell's runs in
391
+ * one request — each run once, ascending, however many cells share it —
392
+ * handed back cell by cell, each in its searches' order. A run a read still
393
+ * in flight asks waits on it: that read is asked again as it was, and only
394
+ * the runs no read asks go in a new one. Nothing is kept once read: the
395
+ * reader holds the cells it draws.
396
+ *
397
+ * @param index - The index the cells are of
398
+ * @param cells - The cells
399
+ * @returns Each cell's entries, by its name; none while its runs or the read are pending
400
+ * @throws {Error} As {@link RecordSliceSource.cellRuns} does; when two of the
401
+ * cells' searches overlap without being one, or the entries read disagree
402
+ * with their bounds
403
+ */
404
+ cellEntries(index, cells) {
405
+ return this.cellsRead(index, cells, undefined, (runs) => this.readRuns(index, runs));
406
+ }
407
+ /**
408
+ * Read the rows another Dict holds under the keys several cells' entries
409
+ * name (#1393): every cell's runs in one request — each run once,
410
+ * ascending, however many cells share it — the keys read where the
411
+ * entries are kept, none sent, handed back cell by cell, each in its
412
+ * searches' order: each entry's key and the Dict's row under it. A run a
413
+ * read still in flight asks waits on it, as {@link RecordSliceSource.cellEntries}'s
414
+ * does. Nothing is kept once read: the reader holds the cells it draws —
415
+ * a Schematic's details, for the cells its window reads of its area index.
416
+ *
417
+ * @param index - The index the cells are of
418
+ * @param cells - The cells
419
+ * @param dict - The Dict read: the `id` of its paged read
420
+ * @returns Each cell's rows of the Dict, by its name; none while its runs or the read are pending
421
+ * @throws {Error} As {@link RecordSliceSource.cellEntries} does, and when the reader reads no Dict by runs
422
+ */
423
+ cellJoins(index, cells, dict) {
424
+ return this.cellsRead(index, cells, dict, (runs) => this.readJoins(index, runs, dict));
425
+ }
426
+ /** Several cells' entries, or their rows of a Dict, by `read`: every cell's runs read once (see {@link RecordSliceSource.cellEntries}). */
427
+ cellsRead(index, cells, dict, read) {
428
+ const found = this.cellRuns(index, cells);
429
+ if (found.type === "none")
430
+ return none;
431
+ const revision = this.revision;
432
+ const byRow = new Map();
433
+ for (const runs of found.value.values()) {
434
+ for (const range of runs.ranges) {
435
+ if (range.count === 0n)
436
+ continue;
437
+ const same = byRow.get(range.row);
438
+ if (same !== undefined && same.count !== range.count)
439
+ throw new Error("The cells' searches overlap; refresh the view and retry");
440
+ byRow.set(range.row, range);
441
+ }
442
+ }
443
+ const runs = [...byRow.values()].sort((a, b) => rowOrder(a.row, b.row));
444
+ for (let i = 1; i < runs.length; i++) {
445
+ if (runs[i - 1].row + runs[i - 1].count > runs[i].row)
446
+ throw new Error("The cells' searches overlap; refresh the view and retry");
447
+ }
448
+ const byStart = this.readAsked(index, dict, runs, revision, read);
449
+ if (byStart === undefined)
450
+ return none;
451
+ const got = new Map();
452
+ for (const cell of cells) {
453
+ const own = [];
454
+ for (const range of found.value.get(cell.id).ranges) {
455
+ if (range.count === 0n)
456
+ continue;
457
+ own.push(...byStart.get(range.row));
458
+ }
459
+ got.set(cell.id, own);
460
+ }
461
+ return some(got);
462
+ }
463
+ /**
464
+ * Some runs of one source, read by `read` — their entries, or their rows of
465
+ * a Dict — each by its first row: a run a read of the same kind still in
466
+ * flight asks waits on that read — asked again as it was, which asks the
467
+ * reader nothing new — and the rest are read together, in one more.
468
+ *
469
+ * @param index - The source
470
+ * @param dict - The Dict whose rows are read; `undefined` for the entries
471
+ * @param runs - The runs: ascending, disjoint, none empty
472
+ * @param revision - The revision they were found at
473
+ * @param read - The read of some of the runs, in order
474
+ * @returns Each run's part, by its first row; `undefined` while a read is pending, or once the revision moved
475
+ */
476
+ readAsked(index, dict, runs, revision, read) {
477
+ const reads = new Set();
478
+ const fresh = [];
479
+ for (const run of runs) {
480
+ const asked = this.askedRuns.find((one) => one.index === index && one.dict === dict
481
+ && one.runs.some((r) => r.row === run.row && r.count === run.count));
482
+ if (asked !== undefined)
483
+ reads.add(asked);
484
+ else
485
+ fresh.push(run);
486
+ }
487
+ if (fresh.length > 0) {
488
+ const asked = { index, runs: fresh, ...(dict !== undefined && { dict }) };
489
+ this.askedRuns.push(asked);
490
+ if (this.askedRuns.length > MAX_ASKED)
491
+ this.askedRuns.shift();
492
+ reads.add(asked);
493
+ }
494
+ const byStart = new Map();
495
+ let pending = false;
496
+ for (const asked of reads) {
497
+ const parts = read(asked.runs);
498
+ if (this.current() !== revision)
499
+ return undefined;
500
+ if (parts.type === "none") {
501
+ pending = true;
502
+ continue;
503
+ }
504
+ const at = this.askedRuns.indexOf(asked);
505
+ if (at >= 0)
506
+ this.askedRuns.splice(at, 1);
507
+ let offset = 0;
508
+ for (const run of asked.runs) {
509
+ byStart.set(run.row, parts.value.slice(offset, offset + Number(run.count)));
510
+ offset += Number(run.count);
511
+ }
512
+ }
513
+ return pending ? undefined : byStart;
514
+ }
515
+ /**
516
+ * The rows a Dict holds under the keys the entries of some runs of one
517
+ * source name, in order: in one request through the reader, the keys read
518
+ * where the entries are kept.
519
+ *
520
+ * @param index - The source
521
+ * @param runs - The runs: ascending, disjoint, none empty
522
+ * @param dict - The Dict read: the `id` of its paged read
523
+ * @returns Each entry's key and its row there, in order; none while pending
524
+ * @throws {Error} When the reader reads no Dict by runs, or its rows disagree with the runs' entries
525
+ */
526
+ readJoins(index, runs, dict) {
527
+ if (this.reader.joinAll === undefined)
528
+ throw new Error("This record's binding cannot read another record's rows by its runs");
529
+ if (runs.length === 0)
530
+ return some([]);
531
+ const expected = runs.reduce((sum, run) => sum + run.count, 0n);
532
+ const read = this.reader.joinAll(index, runs, dict);
533
+ if (read.type === "none")
534
+ return none;
535
+ if (BigInt(read.value.length) !== expected)
536
+ throw new Error("The rows read disagree with the runs' entries; refresh the view and retry");
537
+ return read;
538
+ }
539
+ /**
540
+ * The entries of some runs of one source, in order: in one request when the
541
+ * reader batches its reads, else a page at a time, every run's pages asked
542
+ * together.
543
+ *
544
+ * @param index - The source
545
+ * @param runs - The runs: ascending, disjoint, none empty
546
+ * @returns Their entries, in order; none while any is pending
547
+ * @throws {Error} When the source's entries disagree with the runs
548
+ */
549
+ readRuns(index, runs) {
550
+ const expected = runs.reduce((sum, run) => sum + run.count, 0n);
551
+ if (runs.length === 0)
552
+ return some([]);
553
+ if (this.reader.runsAll !== undefined) {
554
+ const read = this.reader.runsAll(index, runs);
555
+ if (read.type === "none")
556
+ return none;
557
+ if (BigInt(read.value.length) !== expected)
558
+ throw new Error("The source page disagrees with its search bounds; refresh the view and retry");
559
+ return read;
560
+ }
561
+ const entries = [];
562
+ let pending = false;
563
+ for (const run of runs) {
564
+ let position = run.row;
565
+ let available = run.count;
566
+ while (available > 0n) {
567
+ const wanted = available < MAX_PAGE_ROWS ? available : MAX_PAGE_ROWS;
568
+ const page = this.reader.page(index, position, wanted);
569
+ if (page.type === "none") {
570
+ pending = true;
571
+ break;
572
+ }
573
+ const count = BigInt(page.value.length);
574
+ if (count === 0n || count > wanted)
575
+ throw new Error("The source page disagrees with its search bounds; refresh the view and retry");
576
+ entries.push(...page.value);
577
+ position += count;
578
+ available -= count;
579
+ }
580
+ }
581
+ return pending ? none : some(entries);
582
+ }
123
583
  /**
124
584
  * Read only the requested part of the union, filling short provider pages.
125
585
  * A bounded short result is valid under the shared paged-source contract.
@@ -155,6 +615,12 @@ export class RecordSliceSource {
155
615
  }
156
616
  if (window.complete)
157
617
  return some(window.rows);
618
+ if (this.reader.runsAll !== undefined) {
619
+ // A window across several runs reads them in one request (#1351); one inside a run is the page it is.
620
+ const runs = windowRuns(result, offset, limit);
621
+ if (runs.length > 1)
622
+ return this.pageByRuns(result, window, runs);
623
+ }
158
624
  const rows = window.rows;
159
625
  let skip = offset + BigInt(rows.length);
160
626
  let remaining = (limit < MAX_PAGE_ROWS ? limit : MAX_PAGE_ROWS) - BigInt(rows.length);
@@ -185,6 +651,78 @@ export class RecordSliceSource {
185
651
  window.complete = true;
186
652
  return some(rows);
187
653
  }
654
+ /**
655
+ * Read a window of the union as the entries of the runs it covers, in one
656
+ * request (#1351): the window's part of each run, read together, however
657
+ * many runs it spans.
658
+ */
659
+ pageByRuns(result, window, runs) {
660
+ const expected = runs.reduce((sum, run) => sum + run.count, 0n);
661
+ const rows = this.reader.runsAll(result.index, runs);
662
+ if (this.current() !== result.revision || rows.type === "none")
663
+ return none;
664
+ if (BigInt(rows.value.length) !== expected)
665
+ throw new Error("The source page disagrees with its search bounds; refresh the view and retry");
666
+ window.rows = rows.value;
667
+ window.complete = true;
668
+ return some(window.rows);
669
+ }
670
+ /**
671
+ * Where a narrowing's rows sit in the record (#1349): its resolved runs
672
+ * placed by the reader, a window at a time, as far as they are placed.
673
+ *
674
+ * @param plan - Current narrowing
675
+ * @returns The placement so far, or none while the runs or the first window are pending
676
+ * @throws {Error} When the reader reads no placements
677
+ */
678
+ place(plan) {
679
+ if (this.reader.placeAll === undefined)
680
+ throw new Error("This record's binding cannot place a narrowing's rows");
681
+ const resolved = this.resolve(plan);
682
+ if (resolved.type === "none")
683
+ return none;
684
+ const placement = this.reader.placeAll(resolved.value.index, resolved.value.runs);
685
+ if (this.current() !== resolved.value.revision || placement.type === "none")
686
+ return none;
687
+ if (placement.value.total !== resolved.value.total)
688
+ throw new Error("The record's placement disagrees with its search bounds; refresh the view and retry");
689
+ return placement;
690
+ }
691
+ /**
692
+ * Which of some entries a narrowing's resolved runs hold (#1349), each
693
+ * answered by the reader from the runs: no row is examined here. Past
694
+ * {@link MAX_RECORD_SLICE_BATCH} entries, they are asked as several reads,
695
+ * all in this pass, as `resolve` asks a plan's searches.
696
+ *
697
+ * @param plan - Current narrowing
698
+ * @param entries - The entries asked of: each one's index key text and primary key
699
+ * @returns Whether each entry lies in the narrowing's runs, or none while pending
700
+ * @throws {Error} When the reader reads no memberships
701
+ */
702
+ within(plan, entries) {
703
+ if (this.reader.withinAll === undefined)
704
+ throw new Error("This record's binding cannot read which rows a narrowing holds");
705
+ const resolved = this.resolve(plan);
706
+ if (resolved.type === "none")
707
+ return none;
708
+ if (entries.length === 0)
709
+ return some([]);
710
+ const answers = [];
711
+ let pending = false;
712
+ for (let at = 0; at < entries.length; at += MAX_RECORD_SLICE_BATCH) {
713
+ // Entries that fit one read are handed on as they are, the same array whenever they are asked again, so a
714
+ // reader keeping what it made of them by the array finds it.
715
+ const batch = entries.length <= MAX_RECORD_SLICE_BATCH ? entries : entries.slice(at, at + MAX_RECORD_SLICE_BATCH);
716
+ const part = this.reader.withinAll(resolved.value.index, resolved.value.runs, batch);
717
+ if (part.type === "none")
718
+ pending = true;
719
+ else
720
+ answers.push(...part.value);
721
+ }
722
+ if (this.current() !== resolved.value.revision || pending)
723
+ return none;
724
+ return some(answers);
725
+ }
188
726
  /**
189
727
  * Intersect a key search with the selected ranges, translating both hits
190
728
  * and misses to positions in the narrowed view. No row reads are needed.