@orkestrel/table 0.0.3 → 0.0.5

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.
@@ -1,35 +1,40 @@
1
1
  import { arrayOf, attempt, cloneJSONRecord, isArray, isBoolean, isBoundedJSONRecord, isContractError, isFiniteNumber, isRecord, isString, parseNumber, readArrayEntries, recordOf, unionOf } from "@orkestrel/contract";
2
2
  import { Emitter } from "@orkestrel/emitter";
3
3
  //#region src/core/constants.ts
4
- /** Every column cell, in the order declared by the public contract. */
4
+ /** Lists every column cell, in the order declared by the public contract. */
5
5
  var COLUMN_CELLS = Object.freeze([
6
6
  "text",
7
7
  "number",
8
8
  "flag",
9
9
  "choice"
10
10
  ]);
11
- /** The maximum number of columns one schema may declare. */
11
+ /** Names the maximum number of columns one schema may declare: 256. */
12
12
  var COLUMN_LIMIT = 256;
13
- /** The maximum number of choices one `choice` column may offer. */
13
+ /** Names the maximum number of choices one `choice` column may offer: 1024. */
14
14
  var CHOICE_LIMIT = 1024;
15
- /** The maximum length, in UTF-16 code units, of a schema name or column key. */
15
+ /** Names the maximum length of a schema name or column key: 128 UTF-16 code units. */
16
16
  var NAME_LIMIT = 128;
17
- /** The maximum length, in UTF-16 code units, of any single retained string. */
17
+ /** Names the maximum length of any single retained string: 65536 UTF-16 code units. */
18
18
  var STRING_LIMIT = 65536;
19
- /** The maximum total length, in UTF-16 code units, of every string one schema retains. */
19
+ /**
20
+ * Names the maximum total length of every string one schema retains: 1048576 UTF-16 code units.
21
+ */
20
22
  var TEXT_LIMIT = 1048576;
21
- /** The maximum total number of records, arrays, and leaves one schema retains. */
23
+ /** Names the maximum total number of records, arrays, and leaves one schema retains: 16384. */
22
24
  var NODE_LIMIT = 16384;
23
25
  //#endregion
24
26
  //#region src/core/errors.ts
25
- /** An error raised by the table domain. */
27
+ /**
28
+ * Represents an error raised by the table domain — a machine-readable `code` and optional
29
+ * structured `context`.
30
+ */
26
31
  var TableError = class extends Error {
27
- /** The machine-readable reason for this failure. */
32
+ /** Holds the machine-readable reason for this failure. */
28
33
  code;
29
- /** Structured values that locate or explain this failure. */
34
+ /** Holds structured values that locate or explain this failure. */
30
35
  context;
31
36
  /**
32
- * Create a table error.
37
+ * Creates a table error.
33
38
  *
34
39
  * @param code - The machine-readable reason.
35
40
  * @param message - The human-readable failure text.
@@ -43,10 +48,11 @@ var TableError = class extends Error {
43
48
  }
44
49
  };
45
50
  /**
46
- * Determine whether an unknown value is a table error.
51
+ * Determines whether a caught value is a table error, so a `catch` branches on `code` without an
52
+ * assertion.
47
53
  *
48
54
  * @param input - The value to inspect.
49
- * @returns Whether the value is a {@link TableError} instance.
55
+ * @returns True if the value is a {@link TableError} instance; false otherwise.
50
56
  */
51
57
  function isTableError(input) {
52
58
  return input instanceof TableError;
@@ -54,7 +60,7 @@ function isTableError(input) {
54
60
  //#endregion
55
61
  //#region src/core/helpers.ts
56
62
  /**
57
- * Find one column by key.
63
+ * Finds one column by key; `undefined` when the schema declares no such column.
58
64
  *
59
65
  * @param schema - The schema whose columns to search.
60
66
  * @param key - The column key to find.
@@ -64,7 +70,8 @@ function extractColumn(schema, key) {
64
70
  return schema.columns.find((column) => column.key === key);
65
71
  }
66
72
  /**
67
- * Read one row's declared identity.
73
+ * Reads one row's declared identity; `undefined` when its key cell is missing, empty, or not a
74
+ * string.
68
75
  *
69
76
  * @param schema - The schema that names the identity column.
70
77
  * @param row - The row whose identity to read.
@@ -76,7 +83,8 @@ function extractKey(schema, row) {
76
83
  return isString(key) && key.length > 0 ? key : void 0;
77
84
  }
78
85
  /**
79
- * Compute one atomic 0/1/N membership change over known keys.
86
+ * Computes one atomic 0/1/N membership change over the keys a caller may address — the engine
87
+ * selection and expansion share.
80
88
  *
81
89
  * @param known - Every key the caller may change.
82
90
  * @param current - The current key set.
@@ -95,11 +103,59 @@ function computeKeys(known, current, input, include) {
95
103
  return next.size !== current.size || [...next].some((key) => !current.has(key)) ? next : current;
96
104
  }
97
105
  /**
98
- * Check whether a value has the shape required by one column cell.
106
+ * Merges lens terms into a column-keyed list, replacing the entry that names the same column
107
+ * the `set` write.
108
+ *
109
+ * @param current - The list as it stands.
110
+ * @param requested - The terms to write, in the order they are written.
111
+ * @returns A frozen list holding one owned entry per column, in the order the columns first
112
+ * appeared.
113
+ */
114
+ function mergeTerms(current, requested) {
115
+ const next = [...current];
116
+ for (const term of requested) {
117
+ const owned = Object.freeze({ ...term });
118
+ const index = next.findIndex((candidate) => candidate.column === term.column);
119
+ if (index === -1) next.push(owned);
120
+ else next[index] = owned;
121
+ }
122
+ return Object.freeze(next);
123
+ }
124
+ /**
125
+ * Removes every lens term naming one of the given columns — the drop `sort.remove` and
126
+ * `filter.remove` share.
127
+ *
128
+ * @param current - The list as it stands.
129
+ * @param columns - The column keys to drop.
130
+ * @returns A frozen list holding the entries no named column matched, in their original order.
131
+ */
132
+ function removeTerms(current, columns) {
133
+ const removed = new Set(columns);
134
+ return Object.freeze(current.filter((term) => !removed.has(term.column)));
135
+ }
136
+ /**
137
+ * Checks whether two lens lists hold the same terms in the same order, with the supplied test
138
+ * deciding the operands.
139
+ *
140
+ * @param left - The first list.
141
+ * @param right - The second list.
142
+ * @param equal - Decide whether two terms naming one column carry the same operands.
143
+ * @returns True if the lists are the same length and every position names the same column and
144
+ * carries the same operands; false otherwise.
145
+ */
146
+ function matchesTerms(left, right, equal) {
147
+ return left.length === right.length && left.every((term, index) => {
148
+ const other = right[index];
149
+ return other !== void 0 && term.column === other.column && equal(term, other);
150
+ });
151
+ }
152
+ /**
153
+ * Checks whether one column can hold a value — the shape gate every write and every seed passes
154
+ * through.
99
155
  *
100
156
  * @param column - The column that owns the cell.
101
157
  * @param value - The unknown value to inspect.
102
- * @returns Whether the column can hold the value.
158
+ * @returns True if the column can hold the value; false otherwise.
103
159
  */
104
160
  function matchesCell(column, value) {
105
161
  if (isString(value) && value.length > 65536) return false;
@@ -111,7 +167,7 @@ function matchesCell(column, value) {
111
167
  }
112
168
  }
113
169
  /**
114
- * Compare two cells in ascending order according to one column.
170
+ * Compares two of one column's cells the way its `cell` fixes, describing ascending order.
115
171
  *
116
172
  * @param column - The column that fixes the comparison.
117
173
  * @param left - The first cell, or absence.
@@ -137,11 +193,12 @@ function compareCells(column, left, right) {
137
193
  }
138
194
  }
139
195
  /**
140
- * Check whether one column admits a filter and all its operands.
196
+ * Checks whether one column admits a filter and every operand it carries — the gate `filter.set`
197
+ * and {@link matchesFilter} share.
141
198
  *
142
199
  * @param column - The column that fixes the accepted operators and cell shapes.
143
200
  * @param filter - The filter to inspect.
144
- * @returns Whether the filter belongs to the column and the column can apply it.
201
+ * @returns True if the filter belongs to the column and the column can apply it; false otherwise.
145
202
  */
146
203
  function admitsFilter(column, filter) {
147
204
  if (filter.column !== column.key) return false;
@@ -152,12 +209,12 @@ function admitsFilter(column, filter) {
152
209
  }
153
210
  }
154
211
  /**
155
- * Test one cell against a filter according to its column.
212
+ * Tests one of a column's cells against one filter the way its `cell` fixes.
156
213
  *
157
214
  * @param column - The column that fixes the accepted operators.
158
215
  * @param cell - The cell to test, or absence.
159
216
  * @param filter - The filter to apply.
160
- * @returns Whether the filter accepts the cell.
217
+ * @returns True if the filter accepts the cell; false otherwise.
161
218
  */
162
219
  function matchesFilter(column, cell, filter) {
163
220
  if (cell === void 0 || !admitsFilter(column, filter) || !matchesCell(column, cell)) return false;
@@ -168,7 +225,8 @@ function matchesFilter(column, cell, filter) {
168
225
  }
169
226
  }
170
227
  /**
171
- * Keep the rows accepted by every filter.
228
+ * Keeps the rows every filter accepts, in the order given; a supplied {@link CellMatcher}
229
+ * replaces the default per column.
172
230
  *
173
231
  * @param schema - The schema that declares the filtered columns.
174
232
  * @param rows - The rows to filter.
@@ -186,7 +244,8 @@ function filterRows(schema, rows, filters, matchers) {
186
244
  })));
187
245
  }
188
246
  /**
189
- * Order rows stably by a sequence of terms.
247
+ * Orders rows by the terms given, stably; a supplied {@link CellComparator} replaces the default
248
+ * per column.
190
249
  *
191
250
  * @param schema - The schema that declares the sorted columns.
192
251
  * @param rows - The rows to order.
@@ -214,7 +273,8 @@ function sortRows(schema, rows, orders, comparators) {
214
273
  return Object.freeze(indexed.map((entry) => entry.row));
215
274
  }
216
275
  /**
217
- * Audit a structurally valid schema for domain and budget faults.
276
+ * Audits a structurally valid schema for domain faults and budget breaches, returning human
277
+ * diagnostics.
218
278
  *
219
279
  * @param schema - The table schema to audit.
220
280
  * @returns Frozen human-readable diagnostics, or an empty list when the schema is sound.
@@ -313,7 +373,8 @@ function auditTable(schema) {
313
373
  return Object.freeze(faults);
314
374
  }
315
375
  /**
316
- * Project a schema into declaration-ordered JSON.
376
+ * Projects a schema into JSON in declaration order, dropping every absent member; raises `SCHEMA`
377
+ * for a `meta` it cannot own.
317
378
  *
318
379
  * @param schema - The schema to project.
319
380
  * @returns A deeply owned JSON record with absent members omitted.
@@ -352,7 +413,8 @@ function serializeTable(schema) {
352
413
  }
353
414
  }
354
415
  /**
355
- * Project rows into schema-column-ordered JSON.
416
+ * Projects rows into JSON with each row's cells in the schema's column order, dropping every
417
+ * absent cell.
356
418
  *
357
419
  * @param schema - The schema that fixes cell order.
358
420
  * @param rows - The rows to project.
@@ -380,19 +442,21 @@ function serializeRows(schema, rows) {
380
442
  //#endregion
381
443
  //#region src/core/validators.ts
382
444
  /**
383
- * Determine whether an unknown value has a table cell shape.
445
+ * Determines whether an unknown value has a table cell shape — a string, a finite number, or a
446
+ * boolean.
384
447
  *
385
448
  * @param input - The value to inspect.
386
- * @returns Whether the value is a string, finite number, or boolean.
449
+ * @returns True if the value is a string, finite number, or boolean; false otherwise.
387
450
  */
388
451
  function isTableCell(input) {
389
452
  return unionOf(isString, isFiniteNumber, isBoolean)(input);
390
453
  }
391
454
  /**
392
- * Determine whether an unknown value is a record of table cells.
455
+ * Determines whether an unknown value is a record whose every own key is a string and every value
456
+ * a {@link TableCell}.
393
457
  *
394
458
  * @param input - The value to inspect.
395
- * @returns Whether every own key is a string and every value is a table cell.
459
+ * @returns True if every own key is a string and every value is a table cell; false otherwise.
396
460
  */
397
461
  function isTableRow(input) {
398
462
  const outcome = attempt(() => {
@@ -402,19 +466,20 @@ function isTableRow(input) {
402
466
  return outcome.success && outcome.value;
403
467
  }
404
468
  /**
405
- * Determine whether an unknown value is a declared column cell.
469
+ * Determines whether an unknown value is a declared column cell.
406
470
  *
407
471
  * @param input - The value to inspect.
408
- * @returns Whether the value is one of the four column cells.
472
+ * @returns True if the value is a declared column cell; false otherwise.
409
473
  */
410
474
  function isColumnCell(input) {
411
475
  return COLUMN_CELLS.some((cell) => cell === input);
412
476
  }
413
477
  /**
414
- * Determine whether an unknown value is one exact column choice record.
478
+ * Determines whether an unknown value is one exact {@link ColumnChoice} record; an unknown member
479
+ * refuses it.
415
480
  *
416
481
  * @param input - The value to inspect.
417
- * @returns Whether the value is a column choice.
482
+ * @returns True if the value is a column choice; false otherwise.
418
483
  */
419
484
  function isColumnChoice(input) {
420
485
  const outcome = attempt(() => {
@@ -428,10 +493,11 @@ function isColumnChoice(input) {
428
493
  return outcome.success && outcome.value;
429
494
  }
430
495
  /**
431
- * Determine whether an unknown value is one exact discriminated table column.
496
+ * Determines whether an unknown value is one exact discriminated {@link TableColumn}, checked
497
+ * against its cell's own options.
432
498
  *
433
499
  * @param input - The value to inspect.
434
- * @returns Whether the value is a structurally valid table column.
500
+ * @returns True if the value is a structurally valid table column; false otherwise.
435
501
  */
436
502
  function isTableColumn(input) {
437
503
  const outcome = attempt(() => {
@@ -470,10 +536,11 @@ function isTableColumn(input) {
470
536
  return outcome.success && outcome.value;
471
537
  }
472
538
  /**
473
- * Determine whether an unknown value has one exact structural table-schema shape.
539
+ * Determines whether an unknown value has the exact shape of a {@link TableSchema} — the shape
540
+ * alone, with no domain check.
474
541
  *
475
542
  * @param input - The value to inspect.
476
- * @returns Whether the value has the exact structure of a table schema.
543
+ * @returns True if the value has the exact structure of a table schema; false otherwise.
477
544
  */
478
545
  function isStructuralTableSchema(input) {
479
546
  const outcome = attempt(() => {
@@ -493,10 +560,12 @@ function isStructuralTableSchema(input) {
493
560
  return outcome.success && outcome.value;
494
561
  }
495
562
  /**
496
- * Determine whether an unknown value is one semantically sound table schema.
563
+ * Determines whether an unknown value is a {@link TableSchema} a table can be opened against —
564
+ * the exact shape, and an audit that finds nothing.
497
565
  *
498
566
  * @param input - The value to inspect.
499
- * @returns Whether the value has valid structure, domain relationships, and budgets.
567
+ * @returns True if the value has valid structure, domain relationships, and
568
+ * budgets; false otherwise.
500
569
  */
501
570
  function isTableSchema(input) {
502
571
  const outcome = attempt(() => isStructuralTableSchema(input) && auditTable(input).length === 0);
@@ -505,7 +574,7 @@ function isTableSchema(input) {
505
574
  //#endregion
506
575
  //#region src/core/cloners.ts
507
576
  /**
508
- * Clone one row into an owned frozen snapshot.
577
+ * Clones one row into an owned frozen snapshot.
509
578
  *
510
579
  * @param row - The row to own.
511
580
  * @returns A frozen copy of the row's cells.
@@ -514,7 +583,8 @@ function cloneRow(row) {
514
583
  return Object.freeze({ ...row });
515
584
  }
516
585
  /**
517
- * Clone a table schema into an owned frozen snapshot.
586
+ * Clones a whole schema into an owned frozen snapshot, freezing every nested column, choice list,
587
+ * choice, and `meta`; raises `SCHEMA` for a `meta` it cannot own.
518
588
  *
519
589
  * @param schema - The schema to own.
520
590
  * @returns A frozen schema with every nested column, choice, list, and metadata record owned.
@@ -545,7 +615,7 @@ function cloneSchema(schema) {
545
615
  //#endregion
546
616
  //#region src/core/parsers.ts
547
617
  /**
548
- * Parse unknown wire data into an owned, semantically sound table schema.
618
+ * Parses unknown wire data into an owned, structurally valid, semantically sound table schema.
549
619
  *
550
620
  * @param input - The unknown schema value to parse.
551
621
  * @returns An owned table schema, or `undefined` on refusal.
@@ -559,7 +629,8 @@ function parseTable(input) {
559
629
  return outcome.success ? outcome.value : void 0;
560
630
  }
561
631
  /**
562
- * Parse unknown wire rows against one table schema.
632
+ * Parses unknown wire data into owned rows against one table schema, coercing a numeric string
633
+ * and `'true'` / `'false'`.
563
634
  *
564
635
  * @param schema - The schema that declares the accepted keys and cell shapes.
565
636
  * @param input - The unknown row-list value to parse.
@@ -605,63 +676,94 @@ function parseRows(schema, input) {
605
676
  return outcome.success ? outcome.value : void 0;
606
677
  }
607
678
  //#endregion
608
- //#region src/core/tables/ExpansionManager.ts
609
- /** The keys of the rows somebody has opened. */
610
- var ExpansionManager = class {
679
+ //#region src/core/tables/KeyManager.ts
680
+ /** Manages the key set one table axis holds, and the event it announces when that set moves. */
681
+ var KeyManager = class {
611
682
  #emitter;
683
+ #event;
612
684
  #gate;
613
685
  #rows;
614
686
  #read;
615
687
  #write;
616
688
  /**
617
- * Create an expansion manager over one table's private stores.
689
+ * Creates a key-set shell over one table's private store.
618
690
  *
619
691
  * @param emitter - The table's event emitter.
692
+ * @param event - The event this axis announces when its key set moves.
620
693
  * @param gate - The table lifecycle gate.
621
694
  * @param rows - A read of every row key.
622
- * @param read - A read of the expanded keys.
623
- * @param write - The expanded-key commit boundary.
695
+ * @param read - A read of the held keys.
696
+ * @param write - The held-key commit boundary.
624
697
  */
625
- constructor(emitter, gate, rows, read, write) {
698
+ constructor(emitter, event, gate, rows, read, write) {
626
699
  this.#emitter = emitter;
700
+ this.#event = event;
627
701
  this.#gate = gate;
628
702
  this.#rows = rows;
629
703
  this.#read = read;
630
704
  this.#write = write;
631
705
  }
632
- /** The keys of the rows opened right now. */
706
+ /** Returns the keys held right now. */
633
707
  get keys() {
634
708
  return new Set(this.#read());
635
709
  }
636
- /** Open one or more rows. */
637
- expand(input) {
638
- this.#gate();
639
- return this.#change(input, () => true);
640
- }
641
- /** Close one or more rows. */
642
- clear(input) {
643
- this.#gate();
644
- return this.#change(input, () => false);
645
- }
646
- /** Turn one or more rows around independently. */
647
- toggle(input) {
710
+ /**
711
+ * Applies one atomic 0/1/N membership change, announcing it only when the set moves.
712
+ *
713
+ * @param input - Every known key, one key, or a key list.
714
+ * @param include - Decides the next membership from each key's membership at that step.
715
+ * @returns `undefined` for the no-argument form, `false` when any requested key names no row
716
+ * the table holds, and `true` otherwise.
717
+ */
718
+ change(input, include) {
648
719
  this.#gate();
649
- return this.#change(input, (included) => !included) === true;
650
- }
651
- #change(input, include) {
652
720
  const previous = this.#read();
653
721
  const next = computeKeys(this.#rows(), previous, input, include);
654
722
  if (next === void 0) return false;
655
723
  if (next !== previous) {
656
724
  this.#write(next);
657
- this.#emitter.emit("expand", new Set(next));
725
+ this.#emitter.emit(this.#event, new Set(next));
658
726
  }
659
727
  return input === void 0 ? void 0 : true;
660
728
  }
661
729
  };
662
730
  //#endregion
731
+ //#region src/core/tables/ExpansionManager.ts
732
+ /** Manages the keys of the rows somebody has opened. */
733
+ var ExpansionManager = class {
734
+ #keys;
735
+ /**
736
+ * Creates an expansion manager over one table's private stores.
737
+ *
738
+ * @param emitter - The table's event emitter.
739
+ * @param gate - The table lifecycle gate.
740
+ * @param rows - A read of every row key.
741
+ * @param read - A read of the expanded keys.
742
+ * @param write - The expanded-key commit boundary.
743
+ */
744
+ constructor(emitter, gate, rows, read, write) {
745
+ this.#keys = new KeyManager(emitter, "expand", gate, rows, read, write);
746
+ }
747
+ /** Returns the keys of the rows opened right now. */
748
+ get keys() {
749
+ return this.#keys.keys;
750
+ }
751
+ /** Opens one or more rows. */
752
+ expand(input) {
753
+ return this.#keys.change(input, () => true);
754
+ }
755
+ /** Closes one or more rows. */
756
+ clear(input) {
757
+ return this.#keys.change(input, () => false);
758
+ }
759
+ /** Turns one or more rows around independently. */
760
+ toggle(input) {
761
+ return this.#keys.change(input, (included) => !included) === true;
762
+ }
763
+ };
764
+ //#endregion
663
765
  //#region src/core/tables/FilterManager.ts
664
- /** The filters one table applies with and-only composition. */
766
+ /** Manages the filters one table applies with and-only composition. */
665
767
  var FilterManager = class {
666
768
  #schema;
667
769
  #emitter;
@@ -670,7 +772,7 @@ var FilterManager = class {
670
772
  #write;
671
773
  #clamp;
672
774
  /**
673
- * Create a filter manager over one table's private filter store.
775
+ * Creates a filter manager over one table's private filter store.
674
776
  *
675
777
  * @param schema - The table schema.
676
778
  * @param emitter - The table's event emitter.
@@ -687,42 +789,35 @@ var FilterManager = class {
687
789
  this.#write = write;
688
790
  this.#clamp = clamp;
689
791
  }
690
- /** Find one column's filter. */
792
+ /** Finds one column's filter. */
691
793
  filter(column) {
692
794
  const filter = this.#read().find((candidate) => candidate.column === column);
693
795
  return filter === void 0 ? void 0 : Object.freeze({ ...filter });
694
796
  }
695
- /** Read every filter as an owned frozen snapshot. */
797
+ /** Reads every filter as an owned frozen snapshot. */
696
798
  filters() {
697
799
  return Object.freeze(this.#read().map((filter) => Object.freeze({ ...filter })));
698
800
  }
699
- /** Filter one column or several. */
801
+ /** Filters one column or several. */
700
802
  set(input) {
701
803
  this.#gate();
702
804
  const requested = Array.isArray(input) ? input : [input];
703
805
  for (const filter of requested) this.#validate(filter);
704
- const next = [...this.#read()];
705
- for (const filter of requested) {
706
- const owned = Object.freeze({ ...filter });
707
- const index = next.findIndex((candidate) => candidate.column === filter.column);
708
- if (index === -1) next.push(owned);
709
- else next[index] = owned;
710
- }
711
- if (this.#same(next, this.#read())) return;
712
- this.#write(Object.freeze(next));
806
+ const next = mergeTerms(this.#read(), requested);
807
+ if (matchesTerms(next, this.#read(), (filter, other) => this.#operands(filter, other))) return;
808
+ this.#write(next);
713
809
  const page = this.#clamp();
714
810
  this.#emitter.emit("filter", this.filters());
715
811
  if (page !== void 0) this.#emitter.emit("paginate", page);
716
812
  }
717
- /** Stop filtering by one or more columns. */
813
+ /** Stops filtering by one or more columns. */
718
814
  remove(input) {
719
815
  this.#gate();
720
816
  const columns = input === void 0 ? this.#schema.columns.map((column) => column.key) : Array.isArray(input) ? input : [input];
721
817
  for (const column of columns) if (extractColumn(this.#schema, column) === void 0) return false;
722
- const removed = new Set(columns);
723
- const next = this.#read().filter((filter) => !removed.has(filter.column));
818
+ const next = removeTerms(this.#read(), columns);
724
819
  if (next.length !== this.#read().length) {
725
- this.#write(Object.freeze(next));
820
+ this.#write(next);
726
821
  const page = this.#clamp();
727
822
  this.#emitter.emit("filter", this.filters());
728
823
  if (page !== void 0) this.#emitter.emit("paginate", page);
@@ -734,19 +829,16 @@ var FilterManager = class {
734
829
  if (column === void 0) throw new TableError("COLUMN", `The schema declares no column named "${filter.column}"`, { column: filter.column });
735
830
  if (!admitsFilter(column, filter)) throw new TableError("CELL", `Column "${filter.column}" cannot apply that filter`, { column: filter.column });
736
831
  }
737
- #same(left, right) {
738
- return left.length === right.length && left.every((filter, index) => {
739
- const other = right[index];
740
- if (other === void 0 || filter.column !== other.column || filter.operator !== other.operator) return false;
741
- if (filter.operator === "contains" && other.operator === "contains") return filter.text === other.text;
742
- if (filter.operator === "between" && other.operator === "between") return filter.minimum === other.minimum && filter.maximum === other.maximum;
743
- return filter.operator === "equals" && other.operator === "equals" && filter.value === other.value;
744
- });
832
+ #operands(left, right) {
833
+ if (left.operator !== right.operator) return false;
834
+ if (left.operator === "contains" && right.operator === "contains") return left.text === right.text;
835
+ if (left.operator === "between" && right.operator === "between") return left.minimum === right.minimum && left.maximum === right.maximum;
836
+ return left.operator === "equals" && right.operator === "equals" && left.value === right.value;
745
837
  }
746
838
  };
747
839
  //#endregion
748
840
  //#region src/core/tables/PaginationManager.ts
749
- /** The page arithmetic over one table's filtered rows. */
841
+ /** Manages the page arithmetic over one table's filtered rows. */
750
842
  var PaginationManager = class {
751
843
  #emitter;
752
844
  #gate;
@@ -756,7 +848,7 @@ var PaginationManager = class {
756
848
  #readLimit;
757
849
  #writeLimit;
758
850
  /**
759
- * Create a pagination manager over one table's private stores.
851
+ * Creates a pagination manager over one table's private stores.
760
852
  *
761
853
  * @param emitter - The table's event emitter.
762
854
  * @param gate - The table lifecycle gate.
@@ -777,33 +869,33 @@ var PaginationManager = class {
777
869
  const limit = this.#readLimit();
778
870
  if (limit !== void 0) this.#writeLimit(this.#normalize(limit));
779
871
  }
780
- /** The page shown, counted from one. */
872
+ /** Returns the page shown, counted from one. */
781
873
  get page() {
782
874
  return this.#readLimit() === void 0 ? 1 : this.#readPage();
783
875
  }
784
- /** The number of rows one page holds. */
876
+ /** Returns the number of rows one page holds. */
785
877
  get limit() {
786
878
  return this.#readLimit();
787
879
  }
788
- /** The number of filtered rows skipped before this page. */
880
+ /** Returns the number of filtered rows skipped before this page. */
789
881
  get offset() {
790
882
  const limit = this.#readLimit();
791
883
  return limit === void 0 ? 0 : (this.#readPage() - 1) * limit;
792
884
  }
793
- /** The number of pages filled by the filtered rows. */
885
+ /** Returns the number of pages filled by the filtered rows. */
794
886
  get count() {
795
887
  const limit = this.#readLimit();
796
888
  return limit === void 0 ? 1 : Math.max(1, Math.ceil(this.#rows() / limit));
797
889
  }
798
- /** Show another page, clamped to the pages that exist. */
890
+ /** Shows another page, clamped to the pages that exist. */
799
891
  move(page) {
800
892
  this.#gate();
801
- const next = this.#readLimit() === void 0 ? 1 : Math.min(this.count, this.#normalize(page));
893
+ const next = this.#readLimit() === void 0 || Number.isNaN(page) ? 1 : Math.min(this.count, Math.max(1, Math.trunc(page)));
802
894
  if (next === this.#readPage()) return;
803
895
  this.#writePage(next);
804
896
  this.#emitter.emit("paginate", next);
805
897
  }
806
- /** Change the page size while keeping the first row previously shown. */
898
+ /** Changes the page size while keeping the first row previously shown. */
807
899
  resize(limit) {
808
900
  this.#gate();
809
901
  const previous = this.#readLimit();
@@ -821,7 +913,7 @@ var PaginationManager = class {
821
913
  };
822
914
  //#endregion
823
915
  //#region src/core/tables/RowManager.ts
824
- /** The rows one table holds in its own order. */
916
+ /** Manages the rows one table holds in its own order. */
825
917
  var RowManager = class {
826
918
  #schema;
827
919
  #emitter;
@@ -830,7 +922,7 @@ var RowManager = class {
830
922
  #write;
831
923
  #settle;
832
924
  /**
833
- * Create a row manager over one table's private row store.
925
+ * Creates a row manager over one table's private row store.
834
926
  *
835
927
  * @param schema - The table schema.
836
928
  * @param emitter - The table's event emitter.
@@ -850,16 +942,16 @@ var RowManager = class {
850
942
  const seeded = this.#prepare(rows, /* @__PURE__ */ new Set());
851
943
  if (seeded.length > 0) this.#write(Object.freeze(seeded));
852
944
  }
853
- /** Find one row by key as an owned frozen snapshot. */
945
+ /** Finds one row by key as an owned frozen snapshot. */
854
946
  row(key) {
855
947
  const row = this.#read().find((candidate) => extractKey(this.#schema, candidate) === key);
856
948
  return row === void 0 ? void 0 : cloneRow(row);
857
949
  }
858
- /** Read every row as owned frozen snapshots in table order. */
950
+ /** Reads every row as owned frozen snapshots in table order. */
859
951
  rows() {
860
952
  return Object.freeze(this.#read().map((row) => cloneRow(row)));
861
953
  }
862
- /** Append one row or several. */
954
+ /** Appends one row or several. */
863
955
  add(input) {
864
956
  this.#gate();
865
957
  const rows = Array.isArray(input) ? input : [input];
@@ -878,7 +970,7 @@ var RowManager = class {
878
970
  }
879
971
  });
880
972
  }
881
- /** Merge one row or several into the rows their keys name. */
973
+ /** Merges one row or several into the rows their keys name. */
882
974
  update(input) {
883
975
  this.#gate();
884
976
  const updates = (Array.isArray(input) ? input : [input]).map((row) => cloneRow(row));
@@ -917,13 +1009,13 @@ var RowManager = class {
917
1009
  });
918
1010
  return true;
919
1011
  }
920
- /** Move one row to a clamped index in table order. */
1012
+ /** Moves one row to a clamped index in table order. */
921
1013
  move(key, index) {
922
1014
  this.#gate();
923
1015
  const current = this.#read();
924
1016
  const origin = current.findIndex((row) => extractKey(this.#schema, row) === key);
925
1017
  if (origin === -1) return false;
926
- const target = Math.min(current.length - 1, Number.isFinite(index) ? Math.max(0, Math.trunc(index)) : 0);
1018
+ const target = Math.min(current.length - 1, Number.isNaN(index) ? 0 : Math.max(0, Math.trunc(index)));
927
1019
  if (origin === target) return true;
928
1020
  const row = current[origin];
929
1021
  if (row === void 0) return false;
@@ -934,7 +1026,7 @@ var RowManager = class {
934
1026
  this.#settle([], () => this.#emitter.emit("write", key));
935
1027
  return true;
936
1028
  }
937
- /** Remove one or more rows. */
1029
+ /** Removes one or more rows. */
938
1030
  remove(input) {
939
1031
  this.#gate();
940
1032
  const current = this.#read();
@@ -995,15 +1087,11 @@ var RowManager = class {
995
1087
  };
996
1088
  //#endregion
997
1089
  //#region src/core/tables/SelectionManager.ts
998
- /** The keys of the rows somebody has picked. */
1090
+ /** Manages the keys of the rows somebody has picked. */
999
1091
  var SelectionManager = class {
1000
- #emitter;
1001
- #gate;
1002
- #rows;
1003
- #read;
1004
- #write;
1092
+ #keys;
1005
1093
  /**
1006
- * Create a selection manager over one table's private stores.
1094
+ * Creates a selection manager over one table's private stores.
1007
1095
  *
1008
1096
  * @param emitter - The table's event emitter.
1009
1097
  * @param gate - The table lifecycle gate.
@@ -1012,45 +1100,28 @@ var SelectionManager = class {
1012
1100
  * @param write - The selected-key commit boundary.
1013
1101
  */
1014
1102
  constructor(emitter, gate, rows, read, write) {
1015
- this.#emitter = emitter;
1016
- this.#gate = gate;
1017
- this.#rows = rows;
1018
- this.#read = read;
1019
- this.#write = write;
1103
+ this.#keys = new KeyManager(emitter, "select", gate, rows, read, write);
1020
1104
  }
1021
- /** The keys of the rows picked right now. */
1105
+ /** Returns the keys of the rows picked right now. */
1022
1106
  get keys() {
1023
- return new Set(this.#read());
1107
+ return this.#keys.keys;
1024
1108
  }
1025
- /** Pick one or more rows. */
1109
+ /** Picks one or more rows. */
1026
1110
  select(input) {
1027
- this.#gate();
1028
- return this.#change(input, () => true);
1111
+ return this.#keys.change(input, () => true);
1029
1112
  }
1030
- /** Drop one or more picks. */
1113
+ /** Drops one or more picks. */
1031
1114
  clear(input) {
1032
- this.#gate();
1033
- return this.#change(input, () => false);
1115
+ return this.#keys.change(input, () => false);
1034
1116
  }
1035
- /** Turn one or more rows around independently. */
1117
+ /** Turns one or more rows around independently. */
1036
1118
  toggle(input) {
1037
- this.#gate();
1038
- return this.#change(input, (included) => !included) === true;
1039
- }
1040
- #change(input, include) {
1041
- const previous = this.#read();
1042
- const next = computeKeys(this.#rows(), previous, input, include);
1043
- if (next === void 0) return false;
1044
- if (next !== previous) {
1045
- this.#write(next);
1046
- this.#emitter.emit("select", new Set(next));
1047
- }
1048
- return input === void 0 ? void 0 : true;
1119
+ return this.#keys.change(input, (included) => !included) === true;
1049
1120
  }
1050
1121
  };
1051
1122
  //#endregion
1052
1123
  //#region src/core/tables/SortManager.ts
1053
- /** The ordered sort terms of one table. */
1124
+ /** Manages the ordered sort terms of one table. */
1054
1125
  var SortManager = class {
1055
1126
  #schema;
1056
1127
  #emitter;
@@ -1058,7 +1129,7 @@ var SortManager = class {
1058
1129
  #read;
1059
1130
  #write;
1060
1131
  /**
1061
- * Create a sort manager over one table's private term store.
1132
+ * Creates a sort manager over one table's private term store.
1062
1133
  *
1063
1134
  * @param schema - The table schema.
1064
1135
  * @param emitter - The table's event emitter.
@@ -1073,41 +1144,33 @@ var SortManager = class {
1073
1144
  this.#read = read;
1074
1145
  this.#write = write;
1075
1146
  }
1076
- /** Find one column's sort term. */
1147
+ /** Finds one column's sort term. */
1077
1148
  order(column) {
1078
1149
  const order = this.#read().find((candidate) => candidate.column === column);
1079
1150
  return order === void 0 ? void 0 : Object.freeze({ ...order });
1080
1151
  }
1081
- /** Read every sort term as an owned frozen snapshot. */
1152
+ /** Reads every sort term as an owned frozen snapshot. */
1082
1153
  orders() {
1083
1154
  return Object.freeze(this.#read().map((order) => Object.freeze({ ...order })));
1084
1155
  }
1085
- /** Sort by one column or several. */
1156
+ /** Sorts by one column or several. */
1086
1157
  set(input) {
1087
1158
  this.#gate();
1088
1159
  const requested = Array.isArray(input) ? input : [input];
1089
1160
  for (const order of requested) this.#require(order.column);
1090
- const next = [...this.#read()];
1091
- for (const order of requested) {
1092
- const owned = Object.freeze({ ...order });
1093
- const index = next.findIndex((candidate) => candidate.column === order.column);
1094
- if (index === -1) next.push(owned);
1095
- else next[index] = owned;
1096
- }
1097
- if (this.#same(next, this.#read())) return;
1098
- const committed = Object.freeze(next);
1099
- this.#write(committed);
1161
+ const next = mergeTerms(this.#read(), requested);
1162
+ if (matchesTerms(next, this.#read(), (order, other) => order.direction === other.direction)) return;
1163
+ this.#write(next);
1100
1164
  this.#emitter.emit("sort", this.orders());
1101
1165
  }
1102
- /** Stop sorting by one or more columns. */
1166
+ /** Stops sorting by one or more columns. */
1103
1167
  remove(input) {
1104
1168
  this.#gate();
1105
1169
  const columns = input === void 0 ? this.#schema.columns.map((column) => column.key) : Array.isArray(input) ? input : [input];
1106
1170
  for (const column of columns) if (extractColumn(this.#schema, column) === void 0) return false;
1107
- const removed = new Set(columns);
1108
- const next = this.#read().filter((order) => !removed.has(order.column));
1171
+ const next = removeTerms(this.#read(), columns);
1109
1172
  if (next.length !== this.#read().length) {
1110
- this.#write(Object.freeze(next));
1173
+ this.#write(next);
1111
1174
  this.#emitter.emit("sort", this.orders());
1112
1175
  }
1113
1176
  return input === void 0 ? void 0 : true;
@@ -1115,16 +1178,13 @@ var SortManager = class {
1115
1178
  #require(column) {
1116
1179
  if (extractColumn(this.#schema, column) === void 0) throw new TableError("COLUMN", `The schema declares no column named "${column}"`, { column });
1117
1180
  }
1118
- #same(left, right) {
1119
- return left.length === right.length && left.every((order, index) => {
1120
- const other = right[index];
1121
- return other !== void 0 && order.column === other.column && order.direction === other.direction;
1122
- });
1123
- }
1124
1181
  };
1125
1182
  //#endregion
1126
1183
  //#region src/core/Table.ts
1127
- /** A schema, its rows, and the lens through which they are read. */
1184
+ /**
1185
+ * Holds a schema, its rows, and the lens through which they are read, implementing
1186
+ * {@link TableInterface} exactly.
1187
+ */
1128
1188
  var Table = class {
1129
1189
  #emitter;
1130
1190
  #schema;
@@ -1146,7 +1206,7 @@ var Table = class {
1146
1206
  #expansion;
1147
1207
  #pagination;
1148
1208
  /**
1149
- * Open a table against a schema.
1209
+ * Opens a table against a schema.
1150
1210
  *
1151
1211
  * @param schema - The table declaration to own.
1152
1212
  * @param options - Initial rows, lens overrides, pagination, and emitter wiring.
@@ -1154,9 +1214,13 @@ var Table = class {
1154
1214
  * identity is unusable or repeated, and `CELL` when a seeded cell is invalid.
1155
1215
  */
1156
1216
  constructor(schema, options) {
1157
- const problems = isStructuralTableSchema(schema) ? auditTable(schema) : ["The schema is not a table schema"];
1217
+ const unusable = "The schema is not a table schema";
1218
+ const owned = isStructuralTableSchema(schema) ? attempt(() => cloneSchema(schema)) : void 0;
1219
+ if (owned !== void 0 && !owned.success && isTableError(owned.error)) throw owned.error;
1220
+ if (owned === void 0 || !owned.success) throw new TableError("SCHEMA", `The table schema is unusable: ${unusable}`, { problems: [unusable] });
1221
+ const problems = isStructuralTableSchema(owned.value) ? auditTable(owned.value) : [unusable];
1158
1222
  if (problems.length > 0) throw new TableError("SCHEMA", `The table schema is unusable: ${problems.join("; ")}`, { problems: [...problems] });
1159
- this.#schema = cloneSchema(schema);
1223
+ this.#schema = owned.value;
1160
1224
  this.#comparators = options?.comparators === void 0 ? void 0 : Object.freeze({ ...options.comparators });
1161
1225
  this.#matchers = options?.matchers === void 0 ? void 0 : Object.freeze({ ...options.matchers });
1162
1226
  this.#limit = options?.limit;
@@ -1186,54 +1250,54 @@ var Table = class {
1186
1250
  this.#rowStore = rows;
1187
1251
  }, (removed, announce) => this.#settle(removed, announce), options?.rows);
1188
1252
  }
1189
- /** The table's event emitter. */
1253
+ /** Holds the table's event emitter. */
1190
1254
  get emitter() {
1191
1255
  return this.#emitter;
1192
1256
  }
1193
- /** The owned frozen schema. */
1257
+ /** Holds the owned frozen schema. */
1194
1258
  get schema() {
1195
1259
  return this.#schema;
1196
1260
  }
1197
- /** The rows the table holds. */
1261
+ /** Manages the rows the table holds. */
1198
1262
  get rows() {
1199
1263
  return this.#rows;
1200
1264
  }
1201
- /** The ordered sort terms. */
1265
+ /** Manages the ordered sort terms. */
1202
1266
  get sort() {
1203
1267
  return this.#sort;
1204
1268
  }
1205
- /** The filters applied with and-only composition. */
1269
+ /** Manages the filters applied with and-only composition. */
1206
1270
  get filter() {
1207
1271
  return this.#filter;
1208
1272
  }
1209
- /** The selected row keys. */
1273
+ /** Manages the selected row keys. */
1210
1274
  get selection() {
1211
1275
  return this.#selection;
1212
1276
  }
1213
- /** The expanded row keys. */
1277
+ /** Manages the expanded row keys. */
1214
1278
  get expansion() {
1215
1279
  return this.#expansion;
1216
1280
  }
1217
- /** The page arithmetic. */
1281
+ /** Manages the page arithmetic. */
1218
1282
  get pagination() {
1219
1283
  return this.#pagination;
1220
1284
  }
1221
- /** The filtered, sorted, and paged rows as owned frozen snapshots. */
1285
+ /** Returns the filtered, sorted, and paged rows as owned frozen snapshots. */
1222
1286
  get view() {
1223
1287
  const ordered = sortRows(this.#schema, this.#filtered(), this.#orderStore, this.#comparators);
1224
1288
  const limit = this.#limit;
1225
1289
  const page = limit === void 0 ? ordered : ordered.slice(this.#pagination.offset, this.#pagination.offset + limit);
1226
1290
  return Object.freeze(page.map((row) => cloneRow(row)));
1227
1291
  }
1228
- /** The number of rows admitted by the filters. */
1292
+ /** Returns the number of rows admitted by the filters. */
1229
1293
  get count() {
1230
1294
  return this.#filtered().length;
1231
1295
  }
1232
- /** Whether the table has been torn down. */
1296
+ /** Reports whether the table has been torn down. */
1233
1297
  get destroyed() {
1234
1298
  return this.#destroyed;
1235
1299
  }
1236
- /** Reset every moving axis to its opening state. */
1300
+ /** Resets every moving axis to its opening state. */
1237
1301
  clear() {
1238
1302
  this.#gate();
1239
1303
  if (!(this.#rowStore.length > 0 || this.#orderStore.length > 0 || this.#filterStore.length > 0 || this.#selected.size > 0 || this.#expanded.size > 0 || this.#page !== 1 || this.#limit !== this.#initialLimit)) return;
@@ -1246,7 +1310,7 @@ var Table = class {
1246
1310
  this.#limit = this.#initialLimit;
1247
1311
  this.#emitter.emit("clear");
1248
1312
  }
1249
- /** Tear the table down while leaving every getter readable. */
1313
+ /** Tears the table down while leaving every getter readable. */
1250
1314
  destroy() {
1251
1315
  if (this.#destroyed) return;
1252
1316
  this.#destroyed = true;
@@ -1288,23 +1352,49 @@ var Table = class {
1288
1352
  //#endregion
1289
1353
  //#region src/core/factories.ts
1290
1354
  /**
1291
- * Open a table against a schema.
1355
+ * Opens a table against a schema. The schema is copied, and the copy is what the table declares.
1292
1356
  *
1293
1357
  * @param schema - The table declaration to own.
1294
1358
  * @param options - Initial rows, lens overrides, pagination, and emitter wiring.
1295
1359
  * @returns A live table interface.
1296
1360
  * @throws A {@link TableError} coded `SCHEMA` when the schema is unusable, `KEY` when a seeded
1297
1361
  * identity is unusable or repeated, and `CELL` when a seeded cell is invalid.
1298
- * @example
1362
+ * @example Open a table
1299
1363
  * ```ts
1300
- * const table = createTable({ key: 'id', columns: [{ cell: 'text', key: 'id' }] })
1301
- * table.rows.add({ id: '1' })
1364
+ * import { createTable } from '@orkestrel/table'
1365
+ *
1366
+ * const table = createTable(
1367
+ * {
1368
+ * label: 'People',
1369
+ * key: 'id',
1370
+ * columns: [
1371
+ * { cell: 'text', key: 'id', label: 'Reference' },
1372
+ * { cell: 'text', key: 'name', label: 'Name' },
1373
+ * { cell: 'number', key: 'age', label: 'Age' },
1374
+ * ],
1375
+ * },
1376
+ * {
1377
+ * rows: [
1378
+ * { id: '1', name: 'Ada', age: 36 },
1379
+ * { id: '2', name: 'Grace', age: 45 },
1380
+ * { id: '3', name: 'Alan', age: 41 },
1381
+ * ],
1382
+ * limit: 2,
1383
+ * },
1384
+ * )
1385
+ *
1386
+ * table.filter.set({ column: 'name', operator: 'contains', text: 'a' })
1387
+ * table.sort.set({ column: 'age', direction: 'descending' })
1388
+ *
1389
+ * table.count // 3 — every name holds a lowercase 'a'
1390
+ * table.pagination.count // 2 — two pages of two
1391
+ * table.view.map((row) => row.name) // ['Grace', 'Alan'] — page one, oldest first
1302
1392
  * ```
1303
1393
  */
1304
1394
  function createTable(schema, options) {
1305
1395
  return new Table(schema, options);
1306
1396
  }
1307
1397
  //#endregion
1308
- export { CHOICE_LIMIT, COLUMN_CELLS, COLUMN_LIMIT, ExpansionManager, FilterManager, NAME_LIMIT, NODE_LIMIT, PaginationManager, RowManager, STRING_LIMIT, SelectionManager, SortManager, TEXT_LIMIT, Table, TableError, admitsFilter, auditTable, cloneRow, cloneSchema, compareCells, computeKeys, createTable, extractColumn, extractKey, filterRows, isColumnCell, isColumnChoice, isStructuralTableSchema, isTableCell, isTableColumn, isTableError, isTableRow, isTableSchema, matchesCell, matchesFilter, parseRows, parseTable, serializeRows, serializeTable, sortRows };
1398
+ export { CHOICE_LIMIT, COLUMN_CELLS, COLUMN_LIMIT, NAME_LIMIT, NODE_LIMIT, STRING_LIMIT, TEXT_LIMIT, Table, TableError, admitsFilter, auditTable, cloneRow, cloneSchema, compareCells, computeKeys, createTable, extractColumn, extractKey, filterRows, isColumnCell, isColumnChoice, isStructuralTableSchema, isTableCell, isTableColumn, isTableError, isTableRow, isTableSchema, matchesCell, matchesFilter, matchesTerms, mergeTerms, parseRows, parseTable, removeTerms, serializeRows, serializeTable, sortRows };
1309
1399
 
1310
1400
  //# sourceMappingURL=index.js.map