@toclocoinc/lattice-grid 1.67.0 → 1.68.1

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 (163) hide show
  1. package/README.md +1 -1
  2. package/angular/package.json +1 -1
  3. package/docs/API.html +5334 -3788
  4. package/docs/api-detail.html +40 -5
  5. package/lattice-grid.d.ts +3368 -2
  6. package/lattice-grid.esm.min.js +32 -10
  7. package/lattice-grid.min.cjs +32 -10
  8. package/lattice-grid.min.js +32 -10
  9. package/modules/ai.d.ts +64 -1
  10. package/modules/ai.esm.min.js +3 -3
  11. package/modules/ai.min.cjs +3 -3
  12. package/modules/ai.min.js +3 -3
  13. package/modules/angular.d.ts +1 -1
  14. package/modules/angular.esm.min.js +3 -3
  15. package/modules/angular.min.cjs +3 -3
  16. package/modules/angular.min.js +3 -3
  17. package/modules/chart-alluvial.d.ts +1 -1
  18. package/modules/chart-alluvial.esm.min.js +1 -1
  19. package/modules/chart-alluvial.min.cjs +1 -1
  20. package/modules/chart-alluvial.min.js +1 -1
  21. package/modules/chart-arc.d.ts +1 -1
  22. package/modules/chart-arc.esm.min.js +1 -1
  23. package/modules/chart-arc.min.cjs +1 -1
  24. package/modules/chart-arc.min.js +1 -1
  25. package/modules/chart-bubblemap.d.ts +1 -1
  26. package/modules/chart-bubblemap.esm.min.js +1 -1
  27. package/modules/chart-bubblemap.min.cjs +1 -1
  28. package/modules/chart-bubblemap.min.js +1 -1
  29. package/modules/chart-bump.d.ts +1 -1
  30. package/modules/chart-bump.esm.min.js +1 -1
  31. package/modules/chart-bump.min.cjs +1 -1
  32. package/modules/chart-bump.min.js +1 -1
  33. package/modules/chart-calendar.d.ts +1 -1
  34. package/modules/chart-calendar.esm.min.js +1 -1
  35. package/modules/chart-calendar.min.cjs +1 -1
  36. package/modules/chart-calendar.min.js +1 -1
  37. package/modules/chart-decomposition.d.ts +1 -1
  38. package/modules/chart-decomposition.esm.min.js +1 -1
  39. package/modules/chart-decomposition.min.cjs +1 -1
  40. package/modules/chart-decomposition.min.js +1 -1
  41. package/modules/chart-diverging.d.ts +1 -1
  42. package/modules/chart-diverging.esm.min.js +1 -1
  43. package/modules/chart-diverging.min.cjs +1 -1
  44. package/modules/chart-diverging.min.js +1 -1
  45. package/modules/chart-dumbbell.d.ts +1 -1
  46. package/modules/chart-dumbbell.esm.min.js +1 -1
  47. package/modules/chart-dumbbell.min.cjs +1 -1
  48. package/modules/chart-dumbbell.min.js +1 -1
  49. package/modules/chart-fan.d.ts +1 -1
  50. package/modules/chart-fan.esm.min.js +1 -1
  51. package/modules/chart-fan.min.cjs +1 -1
  52. package/modules/chart-fan.min.js +1 -1
  53. package/modules/chart-hexbin.d.ts +1 -1
  54. package/modules/chart-hexbin.esm.min.js +1 -1
  55. package/modules/chart-hexbin.min.cjs +1 -1
  56. package/modules/chart-hexbin.min.js +1 -1
  57. package/modules/chart-hexmap.d.ts +1 -1
  58. package/modules/chart-hexmap.esm.min.js +1 -1
  59. package/modules/chart-hexmap.min.cjs +1 -1
  60. package/modules/chart-hexmap.min.js +1 -1
  61. package/modules/chart-icicle.d.ts +1 -1
  62. package/modules/chart-icicle.esm.min.js +1 -1
  63. package/modules/chart-icicle.min.cjs +1 -1
  64. package/modules/chart-icicle.min.js +1 -1
  65. package/modules/chart-markermap.d.ts +1 -1
  66. package/modules/chart-markermap.esm.min.js +1 -1
  67. package/modules/chart-markermap.min.cjs +1 -1
  68. package/modules/chart-markermap.min.js +1 -1
  69. package/modules/chart-parallel.d.ts +1 -1
  70. package/modules/chart-parallel.esm.min.js +1 -1
  71. package/modules/chart-parallel.min.cjs +1 -1
  72. package/modules/chart-parallel.min.js +1 -1
  73. package/modules/chart-ridgeline.d.ts +1 -1
  74. package/modules/chart-ridgeline.esm.min.js +1 -1
  75. package/modules/chart-ridgeline.min.cjs +1 -1
  76. package/modules/chart-ridgeline.min.js +1 -1
  77. package/modules/chart-roc.d.ts +1 -1
  78. package/modules/chart-roc.esm.min.js +1 -1
  79. package/modules/chart-roc.min.cjs +1 -1
  80. package/modules/chart-roc.min.js +1 -1
  81. package/modules/chart-slope.d.ts +1 -1
  82. package/modules/chart-slope.esm.min.js +1 -1
  83. package/modules/chart-slope.min.cjs +1 -1
  84. package/modules/chart-slope.min.js +1 -1
  85. package/modules/chart-splom.d.ts +1 -1
  86. package/modules/chart-splom.esm.min.js +1 -1
  87. package/modules/chart-splom.min.cjs +1 -1
  88. package/modules/chart-splom.min.js +1 -1
  89. package/modules/chart-waffle.d.ts +1 -1
  90. package/modules/chart-waffle.esm.min.js +1 -1
  91. package/modules/chart-waffle.min.cjs +1 -1
  92. package/modules/chart-waffle.min.js +1 -1
  93. package/modules/charts.d.ts +16 -1
  94. package/modules/charts.esm.min.js +1391 -1257
  95. package/modules/charts.min.cjs +1391 -1257
  96. package/modules/charts.min.js +1391 -1257
  97. package/modules/data-router.d.ts +227 -1
  98. package/modules/data-router.esm.min.js +204 -8
  99. package/modules/data-router.min.cjs +204 -8
  100. package/modules/data-router.min.js +204 -8
  101. package/modules/devtools.d.ts +1 -1
  102. package/modules/devtools.esm.min.js +1 -1
  103. package/modules/devtools.min.cjs +1 -1
  104. package/modules/devtools.min.js +1 -1
  105. package/modules/dhtmlx-compat.d.ts +1 -1
  106. package/modules/dhtmlx-compat.esm.min.js +3 -3
  107. package/modules/dhtmlx-compat.min.cjs +3 -3
  108. package/modules/dhtmlx-compat.min.js +3 -3
  109. package/modules/gantt.d.ts +302 -1
  110. package/modules/gantt.esm.min.js +32 -7
  111. package/modules/gantt.min.cjs +32 -7
  112. package/modules/gantt.min.js +32 -7
  113. package/modules/geo-europe-nuts.d.ts +1 -1
  114. package/modules/geo-europe-nuts.esm.min.js +1 -1
  115. package/modules/geo-uk.d.ts +1 -1
  116. package/modules/geo-uk.esm.min.js +1 -1
  117. package/modules/geo-us-states.d.ts +1 -1
  118. package/modules/geo-us-states.esm.min.js +1 -1
  119. package/modules/geo-world-110m.d.ts +1 -1
  120. package/modules/geo-world-110m.esm.min.js +1 -1
  121. package/modules/geo-world-50m.d.ts +1 -1
  122. package/modules/geo-world-50m.esm.min.js +1 -1
  123. package/modules/htmx.d.ts +1 -1
  124. package/modules/htmx.esm.min.js +32 -10
  125. package/modules/htmx.min.cjs +32 -10
  126. package/modules/htmx.min.js +32 -10
  127. package/modules/kanban.d.ts +233 -1
  128. package/modules/kanban.esm.min.js +3 -3
  129. package/modules/kanban.min.cjs +3 -3
  130. package/modules/kanban.min.js +3 -3
  131. package/modules/kpi.d.ts +196 -8
  132. package/modules/kpi.esm.min.js +32 -7
  133. package/modules/kpi.min.cjs +32 -7
  134. package/modules/kpi.min.js +32 -7
  135. package/modules/layout.d.ts +117 -1
  136. package/modules/layout.esm.min.js +3 -3
  137. package/modules/layout.min.cjs +3 -3
  138. package/modules/layout.min.js +3 -3
  139. package/modules/mock-socket.d.ts +6 -1
  140. package/modules/mock-socket.esm.min.js +1 -1
  141. package/modules/mock-socket.min.cjs +1 -1
  142. package/modules/mock-socket.min.js +1 -1
  143. package/modules/react.d.ts +14 -1
  144. package/modules/react.esm.min.js +3 -3
  145. package/modules/react.min.cjs +3 -3
  146. package/modules/react.min.js +3 -3
  147. package/modules/svelte.d.ts +1 -1
  148. package/modules/svelte.esm.min.js +3 -3
  149. package/modules/svelte.min.cjs +3 -3
  150. package/modules/svelte.min.js +3 -3
  151. package/modules/tabs.d.ts +30 -1
  152. package/modules/tabs.esm.min.js +13 -14
  153. package/modules/tabs.min.cjs +13 -14
  154. package/modules/tabs.min.js +13 -14
  155. package/modules/vue.d.ts +8 -1
  156. package/modules/vue.esm.min.js +3 -3
  157. package/modules/vue.min.cjs +3 -3
  158. package/modules/vue.min.js +3 -3
  159. package/modules/webcomponent.d.ts +113 -1
  160. package/modules/webcomponent.esm.min.js +32 -10
  161. package/modules/webcomponent.min.cjs +32 -10
  162. package/modules/webcomponent.min.js +32 -10
  163. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.67.0, data-router module type declarations
2
+ * Lattice Grid 1.68.1, data-router module type declarations
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -53,7 +53,16 @@ type RouteWhere = Record<string, unknown>;
53
53
  * shorthand (`sum`, `avg`, `min`, `max`, `count`).
54
54
  */
55
55
  interface RouteRollup {
56
+ /**
57
+ * What identifies a group: a property name, a `fn(row)`, or a list of either for a
58
+ * composite. Named properties are carried onto the summary row.
59
+ */
56
60
  groupBy: RouterKey | RouterKey[];
61
+ /**
62
+ * The summary fields, each a reducer over the group's rows: a function handed the rows,
63
+ * or `{ op, field }` for `count`, `sum`, `avg`, `min` or `max` over the numeric values
64
+ * of `field`. `avg` over no numeric values is 0; `min` and `max` are undefined.
65
+ */
57
66
  aggregate?: Record<string, ((rows: RouterRecord[]) => unknown) | { op: string; field?: string }>;
58
67
  }
59
68
 
@@ -81,16 +90,68 @@ type RouterWrite = Record<string, unknown>;
81
90
  * refresh under load (v13).
82
91
  */
83
92
  interface RouteOptions {
93
+ /**
94
+ * What identifies a row on this route — a field name or a `fn(row)` — overriding the
95
+ * router's own `rowKey`.
96
+ */
84
97
  rowKey?: RouterKey;
98
+ /**
99
+ * Reshapes each row before the viewer sees it. The row's identity is still taken from
100
+ * the original, so the keyed diff is unaffected; a transform route is derived and its
101
+ * edits are always reverted.
102
+ */
85
103
  transform?: (row: RouterRecord) => RouterRecord;
104
+ /**
105
+ * Admits a subset of the route's partition to the viewer. It runs on the original row,
106
+ * before any transform and before a rollup groups them.
107
+ */
86
108
  filter?: (row: RouterRecord) => boolean;
109
+ /**
110
+ * Orders the rows the viewer receives — a comparator, or `{ key, dir }`. A viewer with
111
+ * a sort model of its own still governs the final display; for a rollup route this
112
+ * sorts the summary rows.
113
+ */
87
114
  sort?: RouteSort;
115
+ /**
116
+ * Feeds the viewer one summary row per group instead of the raw rows. A rollup route is
117
+ * derived and its edits are always reverted.
118
+ */
88
119
  rollup?: RouteRollup;
120
+ /**
121
+ * A filter-wire condition for this route's slice, read only by `query()`: the pushable
122
+ * part goes to the engine and the rest is finished in the browser. It has no effect on
123
+ * rows arriving through `load`, `apply` or `push`.
124
+ */
89
125
  where?: RouteWhere;
126
+ /**
127
+ * Captures the viewer's committed edits and routes them to `onWrite` instead of leaving
128
+ * them local. Defaults to false; on a derived (rollup or transform) route the edit is
129
+ * reverted with a warning whatever this says.
130
+ */
90
131
  writable?: boolean;
132
+ /**
133
+ * Receives each captured edit as `{ key, colId, value, before, row }`. Return `false`
134
+ * or `{ ok: false }` (or reject, or throw) to revert the cell, `{ conflict, row }` to
135
+ * fire `onConflict` and take the winning row, anything else to accept; a promise is
136
+ * awaited with the optimistic value standing. Overrides the router-wide handler. The
137
+ * context's `route` is the viewer; `source` is always null.
138
+ */
91
139
  onWrite?: (change: RouterWrite, ctx: { route: unknown; source: unknown }) => unknown;
140
+ /**
141
+ * Called when `onWrite` returns a `conflict`, with the server's row, so the host can
142
+ * tell the user. The router itself is last-write-wins: the returned row (or the
143
+ * optimistic one) re-enters regardless. Overrides the router-wide handler.
144
+ */
92
145
  onConflict?: (change: RouterWrite, ctx: { serverRow: RouterRecord }) => void;
146
+ /**
147
+ * Names this route in `metrics()` and the devtools panel. Defaults to null — the
148
+ * default route reports `default`.
149
+ */
93
150
  label?: string;
151
+ /**
152
+ * Throttles how often this route's viewer is refreshed under load. Omitted, every
153
+ * change repaints the viewer at once.
154
+ */
94
155
  backpressure?: RouteBackpressure;
95
156
  }
96
157
 
@@ -120,9 +181,26 @@ interface RouterEdge { from: unknown; to: unknown; on?: SelectionRelation; relat
120
181
  * spec. Desugars to those calls and composes with them.
121
182
  */
122
183
  interface RouterConfig {
184
+ /**
185
+ * The routes to open, each `{ grid, when, ...routeOptions }`, or `{ default: grid }`,
186
+ * or `{ subscribe: handler, when }`, or `{ alert: handler, when, condition }`. `when`
187
+ * is the partition value or predicate `attach` takes.
188
+ */
123
189
  routes?: Record<string, unknown>[];
190
+ /**
191
+ * Selection links to make, each naming the `from` and `to` grids and the relation as
192
+ * `on` (or `relation`) — the arguments of `link()`.
193
+ */
124
194
  links?: { from: unknown; to: unknown; on?: SelectionRelation; relation?: SelectionRelation }[];
195
+ /**
196
+ * Relationship-graph edges to register, as `relate()` takes them; an edge may be
197
+ * `mutual` to work in both directions.
198
+ */
125
199
  relate?: RouterEdge[];
200
+ /**
201
+ * Turns on the time-travel buffer, bounded by `window` (ms of feed time) and/or `max`
202
+ * deltas. An empty object applies a default cap of 10,000 deltas and warns.
203
+ */
126
204
  buffer?: { window?: number; max?: number };
127
205
  }
128
206
 
@@ -136,13 +214,37 @@ interface RouterConfig {
136
214
  * `passthrough` it unjoined, or fill the fields with `null`.
137
215
  */
138
216
  interface RouterJoin {
217
+ /** The id of the registered source holding the lookup rows. */
139
218
  from: string;
219
+ /**
220
+ * Reads the joining value off this source's row — a field name or a `fn(row)`.
221
+ * Required: without it (or `on`) the join is ignored with a warning.
222
+ */
140
223
  localKey?: RouterKey;
224
+ /** An alias for `localKey`, read when `localKey` is absent. */
141
225
  on?: RouterKey;
226
+ /**
227
+ * Reads the joining value off the lookup row — a field name or a `fn(row)`. Defaults to
228
+ * a string `localKey`; with a function `localKey` and no `foreignKey`, the join is
229
+ * ignored with a warning.
230
+ */
142
231
  foreignKey?: RouterKey;
232
+ /** An alias for `foreignKey`, read when `foreignKey` is absent. */
143
233
  fromKey?: RouterKey;
234
+ /**
235
+ * Which lookup fields to carry onto the row: a list of names, a `{ from: to }` rename
236
+ * map, or a `fn(lookupRow, leftRow)` returning the fields to merge. With none, rows
237
+ * pass through unenriched and the router warns.
238
+ */
144
239
  fields?: string[] | Record<string, string> | ((lookupRow: RouterRecord | null, leftRow: RouterRecord) => RouterRecord);
240
+ /** An alias for `fields`, read when `fields` is absent. */
145
241
  select?: string[] | Record<string, string> | ((lookupRow: RouterRecord | null, leftRow: RouterRecord) => RouterRecord);
242
+ /**
243
+ * What happens while the lookup row has not arrived: `hold` keeps the row from viewers
244
+ * until it does, `passthrough` sends it unenriched, `null` fills the declared fields
245
+ * with null. Defaults to `passthrough`, which an unrecognised value also falls back to,
246
+ * with a warning.
247
+ */
146
248
  missing?: 'hold' | 'passthrough' | 'null';
147
249
  }
148
250
 
@@ -178,9 +280,19 @@ interface RouterSourceHandle {
178
280
 
179
281
  /** One route's figures in a `metrics()` snapshot (v10). */
180
282
  interface RouterRouteMetrics {
283
+ /** The route's `label`, or `default` for the default route, or null when it has neither. */
181
284
  label: string | null;
285
+ /** How many rows the route's partition holds, before its filter, links and rollup. */
182
286
  rows: number;
287
+ /**
288
+ * How many rows the viewer currently holds — the partition after the route's filter,
289
+ * any cross-grid links, and a rollup's grouping.
290
+ */
183
291
  shown: number;
292
+ /**
293
+ * Rows routed to this route per second since the previous metrics read. The first read
294
+ * of a route reports 0, having no interval to measure.
295
+ */
184
296
  throughput: number;
185
297
  [key: string]: unknown;
186
298
  }
@@ -194,12 +306,31 @@ interface RouterSourceMetrics { id: string; rows: number; throughput: number; [k
194
306
  * dropped (duplicate), buffered and lag figures.
195
307
  */
196
308
  interface RouterMetrics {
309
+ /** One entry per route, in attach order, the default route last. */
197
310
  routes: RouterRouteMetrics[];
311
+ /** One entry per registered fan-in source. */
198
312
  sources: RouterSourceMetrics[];
313
+ /**
314
+ * How many arriving records matched no route (and went to the default sink, if there is
315
+ * one). Counted since the last `load()` or `query()`, each of which resets it.
316
+ */
199
317
  unrouted: number;
318
+ /**
319
+ * How many deltas the dedupe gate discarded as stale or already seen, cumulative for
320
+ * the router's life.
321
+ */
200
322
  dropped: number;
323
+ /** How many deltas the time-travel ring currently holds. Zero when not buffering. */
201
324
  buffered: number;
325
+ /**
326
+ * How many buffered deltas the viewers are behind the live head, in deltas. Nonzero
327
+ * only while scrubbed into the past.
328
+ */
202
329
  lag: number;
330
+ /**
331
+ * Rows routed across every route per second since the previous metrics read; 0 on the
332
+ * first read.
333
+ */
203
334
  throughput: number;
204
335
  }
205
336
 
@@ -220,7 +351,17 @@ interface RouterDevtoolsPanel { refresh(): void; destroy(): void }
220
351
  * `capabilities` the planner consults to decide what it may push down.
221
352
  */
222
353
  interface RouterQueryAdapter {
354
+ /**
355
+ * What the engine can evaluate, as the pushdown capability model reads it; the planner
356
+ * consults it to decide how much of a filter to push down. Omitted, the conservative
357
+ * defaults apply.
358
+ */
223
359
  capabilities?: Record<string, unknown>;
360
+ /**
361
+ * Runs one planned query and resolves to its rows (`total` optional and unused by the
362
+ * router). Called once per `where` route, plus once for the base query shared by the
363
+ * routes without one.
364
+ */
224
365
  execute: (query: Record<string, unknown>, request?: Record<string, unknown>) => Promise<{ rows: RouterRecord[]; total?: number }>;
225
366
  }
226
367
 
@@ -230,11 +371,24 @@ interface RouterQueryAdapter {
230
371
  * `indexedDB` / `dbName` / `storeName` select the browser store.
231
372
  */
232
373
  interface RouterPersistOptions {
374
+ /** The record the snapshot is written under. Defaults to `lattice-router`. */
233
375
  key?: string;
376
+ /**
377
+ * How long to wait after a change before writing, in ms, so a burst costs one write.
378
+ * Defaults to 250; zero or less writes on every change.
379
+ */
234
380
  debounce?: number;
381
+ /** An async key/value backend of your own. Given one, IndexedDB is never opened. */
235
382
  storage?: { get: (key: string) => Promise<unknown>; set: (key: string, value: unknown) => Promise<void> };
383
+ /**
384
+ * The `IDBFactory` to open the database with. Defaults to the global `indexedDB`; where
385
+ * none is reachable the router warns once and keeps running in memory with no durable
386
+ * resume.
387
+ */
236
388
  indexedDB?: unknown;
389
+ /** The IndexedDB database to open. Defaults to `lattice-router`. */
237
390
  dbName?: string;
391
+ /** The object store inside the database. Defaults to `snapshots`. */
238
392
  storeName?: string;
239
393
  }
240
394
 
@@ -356,20 +510,92 @@ interface DataRouter {
356
510
  * disables the timer) (v10).
357
511
  */
358
512
  interface DataRouterOptions {
513
+ /**
514
+ * How a record says which partition it belongs to: a property name, or a
515
+ * function of the record. This is the one thing the router needs to route.
516
+ */
359
517
  key?: RouterKey;
518
+ /**
519
+ * A record's identity within a route — a property name or a function. It
520
+ * is what makes an update an update rather than a second row, and what
521
+ * lets a record whose partition changed *move* between routes instead of
522
+ * being duplicated. Defaults to the record's `rowKey` property.
523
+ */
360
524
  rowKey?: RouterKey;
525
+ /**
526
+ * Send a record to every route whose predicate it matches, rather than to
527
+ * the first one only. Off by default.
528
+ */
361
529
  overlap?: boolean;
530
+ /**
531
+ * Called with each record that matched no route. They are never silently
532
+ * dropped: they are counted as well, and go to the default route when one
533
+ * is attached.
534
+ */
362
535
  onUnrouted?: (item: RouterRecord | RouterDelta) => void;
536
+ /**
537
+ * How long, in milliseconds, to wait before re-filtering linked grids
538
+ * after a selection changes. 16 by default; `0` re-filters synchronously.
539
+ */
363
540
  selectionDebounce?: number;
541
+ /**
542
+ * Where a record carries its version or sequence number — a property name
543
+ * or a function. Supplying it lets the router apply a feed in order and,
544
+ * unless `dedupe` says otherwise, drop a delta it has already seen.
545
+ */
364
546
  seq?: RouterKey;
547
+ /**
548
+ * Whether to drop a record whose sequence number is not newer than the
549
+ * last one applied for that identity. On whenever `seq` is given;
550
+ * meaningless without it.
551
+ */
365
552
  dedupe?: boolean;
553
+ /**
554
+ * Buffer incoming records and apply them on an interval rather than one at
555
+ * a time: a number of milliseconds, or `{ intervalMs }`. Batching also
556
+ * turns coalescing on.
557
+ */
366
558
  batch?: number | { intervalMs: number };
559
+ /**
560
+ * Settle repeated updates to the same identity inside one batch into a
561
+ * single apply, so a fast feed costs one update per row rather than one
562
+ * per message.
563
+ */
367
564
  coalesce?: boolean;
565
+ /**
566
+ * Where a record carries its timestamp — a property name or a function. It
567
+ * is the axis `scrubTo` and `replay` move along when time-travel is
568
+ * buffering.
569
+ */
368
570
  time?: RouterKey;
571
+ /**
572
+ * The clock the router stamps and expires by. `Date.now` unless you supply
573
+ * one, which is how a test drives time without faking the global.
574
+ */
369
575
  now?: () => number;
576
+ /**
577
+ * A whole router described as data — routes, links, buffering — applied
578
+ * through `configure()` as soon as the router is built.
579
+ */
370
580
  config?: RouterConfig;
581
+ /**
582
+ * Persist an edit committed in any route attached `{ writable: true }`
583
+ * that names no handler of its own. Return, or resolve, falsely to revert
584
+ * the edit; a route with neither this nor its own handler leaves the edit
585
+ * in place unpersisted and warns.
586
+ */
371
587
  onWrite?: (change: RouterWrite, ctx: { route: unknown; source: unknown }) => unknown;
588
+ /**
589
+ * Called when a write comes back reporting a conflict, with the server's
590
+ * version of the row. The router itself is last-write-wins; this is where
591
+ * a host resolves it differently.
592
+ */
372
593
  onConflict?: (change: RouterWrite, ctx: { serverRow: RouterRecord }) => void;
594
+ /**
595
+ * How often, in milliseconds, to emit the `metrics` event. 1000 by
596
+ * default; `0` stops the timer and leaves `metrics()` to be read on
597
+ * demand.
598
+ */
373
599
  metricsInterval?: number;
374
600
  }
375
601
 
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Lattice Grid 1.67.0, data-router module
2
+ * Lattice Grid 1.68.1, data-router module
3
3
  * Copyright (c) 2026 TOCLOCO Inc. All rights reserved.
4
4
  * https://latticegrid.dev
5
5
  */
@@ -52,12 +52,12 @@ Object.defineProperty(__exports,"frameBatched",{enumerable:true,get:function(){r
52
52
  Object.defineProperty(__exports,"settleDebounce",{enumerable:true,get:function(){return settleDebounce;}});
53
53
  Object.defineProperty(__exports,"whenIdle",{enumerable:true,get:function(){return whenIdle;}});
54
54
  Object.defineProperty(__exports,"uid",{enumerable:true,get:function(){return uid;}});
55
- const STAMPED_VERSION="1.67.0";
55
+ const STAMPED_VERSION="1.68.1";
56
56
  async function resolveVersion(){
57
57
  if(STAMPED_VERSION!=='0.0.0-source')return STAMPED_VERSION;
58
58
  return STAMPED_VERSION;
59
59
  }
60
- const VERSION="1.67.0";
60
+ const VERSION="1.68.1";
61
61
  const warned=new Set();
62
62
  const WARNED_LIMIT=2000;
63
63
  function rememberWarned(key){
@@ -1544,6 +1544,180 @@ grandTotals:()=>lastGrand,
1544
1544
  };
1545
1545
  }
1546
1546
  });
1547
+ __def("packages/core/src/model/rowkey.js",function(__exports,__req){
1548
+ 'use strict';
1549
+ Object.defineProperty(__exports,"KEY_DEPENDENT_FEATURES",{enumerable:true,get:function(){return KEY_DEPENDENT_FEATURES;}});
1550
+ Object.defineProperty(__exports,"compositeKey",{enumerable:true,get:function(){return compositeKey;}});
1551
+ Object.defineProperty(__exports,"createRowKey",{enumerable:true,get:function(){return createRowKey;}});
1552
+ Object.defineProperty(__exports,"warnMissingKeys",{enumerable:true,get:function(){return warnMissingKeys;}});
1553
+ Object.defineProperty(__exports,"groupPathKey",{enumerable:true,get:function(){return groupPathKey;}});
1554
+ Object.defineProperty(__exports,"splitPathKey",{enumerable:true,get:function(){return splitPathKey;}});
1555
+ const __m0=__req("packages/core/src/internal/util.js");
1556
+ const warnOnce=__m0["warnOnce"];
1557
+ const pathGetter=__m0["pathGetter"];
1558
+ const isFunction=__m0["isFunction"];
1559
+ const isBlank=__m0["isBlank"];
1560
+ const KEY_DEPENDENT_FEATURES=[
1561
+ 'changes (rows.apply / rows.queue)',
1562
+ 'streaming dedupe',
1563
+ 'selection persistence',
1564
+ 'remote reload',
1565
+ 'cell comments',
1566
+ ];
1567
+ const SEP='\u001f';
1568
+ function part(value){
1569
+ if(value===null)return'\u0000n';
1570
+ if(value===undefined)return'\u0000u';
1571
+ const t=typeof value;
1572
+ if(t==='string')return value;
1573
+ if(t==='number'||t==='bigint'||t==='boolean')return String(value);
1574
+ if(value instanceof Date)return`\u0000d${value.getTime()}`;
1575
+ try{
1576
+ return`\u0000o${JSON.stringify(value)}`;
1577
+ }catch{
1578
+ return'\u0000o?';
1579
+ }
1580
+ }
1581
+ function compositeKey(parts){
1582
+ if(parts.length===1)return part(parts[0]);
1583
+ let out='';
1584
+ for(let i=0;i<parts.length;i++){
1585
+ if(i)out+=SEP;
1586
+ out+=part(parts[i]);
1587
+ }
1588
+ return out;
1589
+ }
1590
+ function createRowKey(spec,opts={}){
1591
+ const warn=opts.warn!==false;
1592
+ if(isFunction(spec)){
1593
+ const fn=(spec);
1594
+ return{
1595
+ stable:true,
1596
+ fields:[],
1597
+ disabled:[],
1598
+ key(data,index){
1599
+ try{
1600
+ const value=fn(data);
1601
+ return Array.isArray(value)?compositeKey(value):part(value);
1602
+ }catch(err){
1603
+ warnOnce('rowKey.throw','rowKey function threw; falling back to index identity for the affected rows.',err);
1604
+ return`#${index??-1}`;
1605
+ }
1606
+ },
1607
+ keyEmpty(data){
1608
+ try{
1609
+ const value=fn(data);
1610
+ return Array.isArray(value)?value.length===0||value.some(isBlank):isBlank(value);
1611
+ }catch{
1612
+ return false;
1613
+ }
1614
+ },
1615
+ missingFields(){return[];},
1616
+ };
1617
+ }
1618
+ if(Array.isArray(spec)&&spec.length>0){
1619
+ const fields=spec.filter((f)=>typeof f==='string');
1620
+ const getters=fields.map((f)=>pathGetter(f));
1621
+ const n=getters.length;
1622
+ return{
1623
+ stable:true,
1624
+ fields,
1625
+ disabled:[],
1626
+ key(data){
1627
+ const parts=new Array(n);
1628
+ for(let i=0;i<n;i++)parts[i]=getters[i](data);
1629
+ return compositeKey(parts);
1630
+ },
1631
+ keyEmpty(data){
1632
+ for(let i=0;i<n;i++)if(isBlank(getters[i](data)))return true;
1633
+ return false;
1634
+ },
1635
+ missingFields(data){
1636
+ const out=[];
1637
+ for(let i=0;i<n;i++)if(isBlank(getters[i](data)))out.push(fields[i]);
1638
+ return out;
1639
+ },
1640
+ };
1641
+ }
1642
+ if(typeof spec==='string'&&spec.length>0){
1643
+ const get=pathGetter(spec);
1644
+ return{
1645
+ stable:true,
1646
+ fields:[spec],
1647
+ disabled:[],
1648
+ key(data){
1649
+ return part(get(data));
1650
+ },
1651
+ keyEmpty(data){
1652
+ return isBlank(get(data));
1653
+ },
1654
+ missingFields(data){
1655
+ return isBlank(get(data))?[spec]:[];
1656
+ },
1657
+ };
1658
+ }
1659
+ if(warn){
1660
+ warnOnce(
1661
+ 'rowKey.missing',
1662
+ `no rowKey configured, so rows are identified by object identity. This disables: ${KEY_DEPENDENT_FEATURES.join(', ')}. Set rowKey to a field name, an array of field names, or a function.`,
1663
+ );
1664
+ }
1665
+ return{
1666
+ stable:false,
1667
+ fields:[],
1668
+ disabled:KEY_DEPENDENT_FEATURES.slice(),
1669
+ key(data,index){
1670
+ if(data===null||typeof data!=='object'){
1671
+ return`#${index??0}:${String(data)}`;
1672
+ }
1673
+ let assigned=autoKeys.get(data);
1674
+ if(assigned===undefined){
1675
+ assigned=`#${autoKeySeq++}`;
1676
+ autoKeys.set(data,assigned);
1677
+ }
1678
+ return assigned;
1679
+ },
1680
+ };
1681
+ }
1682
+ const autoKeys=new WeakMap();
1683
+ let autoKeySeq=0;
1684
+ function warnMissingKeys(resolver,rows){
1685
+ if(!resolver||resolver.stable===false||!isFunction(resolver.keyEmpty))return;
1686
+ if(!Array.isArray(rows)||rows.length===0)return;
1687
+ let missing=0;
1688
+ let sampleFields=null;
1689
+ let sampleData;
1690
+ for(let i=0;i<rows.length;i++){
1691
+ const data=rows[i];
1692
+ if(!resolver.keyEmpty(data))continue;
1693
+ missing++;
1694
+ if(sampleFields===null){
1695
+ sampleFields=isFunction(resolver.missingFields)?resolver.missingFields(data):[];
1696
+ sampleData=data;
1697
+ }
1698
+ }
1699
+ if(missing===0)return;
1700
+ const dedupFields=resolver.fields.length?resolver.fields.join(','):'(function)';
1701
+ const named=sampleFields&&sampleFields.length
1702
+ ?sampleFields.join(', ')
1703
+ :(resolver.fields.length?resolver.fields.join(', '):'the rowKey function');
1704
+ const present=sampleData!==null&&typeof sampleData==='object'
1705
+ ?(Object.keys((sampleData)).join(', ')||'(no fields)')
1706
+ :String(sampleData);
1707
+ warnOnce(
1708
+ `rowKey.emptyValue:${dedupFields}`,
1709
+ `rowKey ${named} is missing on ${missing} of ${rows.length} rows; the first such row has: ${present}. `
1710
+ +'Rows without a key value collapse onto one key, so selection, edits and rows.apply updates land on '
1711
+ +'whichever row now shares it. Set rowKey to a field present on every row, or fix the source data.',
1712
+ );
1713
+ }
1714
+ function groupPathKey(path){
1715
+ return compositeKey(path);
1716
+ }
1717
+ function splitPathKey(key){
1718
+ return key.split(SEP);
1719
+ }
1720
+ });
1547
1721
  __def("packages/modules/data-router/devtools.js",function(__exports,__req){
1548
1722
  'use strict';
1549
1723
  Object.defineProperty(__exports,"mountRouterDevtools",{enumerable:true,get:function(){return mountRouterDevtools;}});
@@ -1629,8 +1803,10 @@ const warnOnce=__m0["warnOnce"];
1629
1803
  const __m1=__req("packages/core/src/source/pushdown.js");
1630
1804
  const planQuery=__m1["planQuery"];
1631
1805
  const capabilitiesOf=__m1["capabilitiesOf"];
1632
- const __m2=__req("packages/modules/data-router/devtools.js");
1633
- const mountRouterDevtools=__m2["mountRouterDevtools"];
1806
+ const __m2=__req("packages/core/src/model/rowkey.js");
1807
+ const compositeKey=__m2["compositeKey"];
1808
+ const __m3=__req("packages/modules/data-router/devtools.js");
1809
+ const mountRouterDevtools=__m3["mountRouterDevtools"];
1634
1810
  function resolver(spec,fallback){
1635
1811
  if(typeof spec==='function')return spec;
1636
1812
  if(typeof spec==='string')return(row)=>row[spec];
@@ -1683,6 +1859,8 @@ const maxLag=(Number.isFinite(spec.maxLag)&&spec.maxLag>=0)?Math.floor(Number(sp
1683
1859
  if(minInterval===0&&sample===1&&maxLag===null)return null;
1684
1860
  return{minInterval,sample,maxLag};
1685
1861
  }
1862
+ let routeSeq=0;
1863
+ function nextRouteId(){routeSeq+=1;return`r${routeSeq}`;}
1686
1864
  function grouper(groupBy){
1687
1865
  const specs=Array.isArray(groupBy)?groupBy:[groupBy];
1688
1866
  const parts=specs.map((s)=>({name:typeof s==='string'?s:null,get:resolver(s,()=>undefined)}));
@@ -1694,7 +1872,7 @@ const v=part.get(row);
1694
1872
  vals.push(v);
1695
1873
  if(part.name)fields[part.name]=v;
1696
1874
  }
1697
- return{key:vals.map((v)=>String(v)).join('\u0001'),fields};
1875
+ return{key:compositeKey(vals),fields};
1698
1876
  };
1699
1877
  }
1700
1878
  function aggregateOne(spec,rows){
@@ -1863,10 +2041,25 @@ return fallbackRoute?[fallbackRoute]:[];
1863
2041
  };
1864
2042
  const applyChange=(route,change)=>{
1865
2043
  if(change.add.length||change.update.length||change.remove.length){
1866
- route.sink(change);
2044
+ const result=route.sink(change);
2045
+ warnRejectedRemovals(route,result);
1867
2046
  }
1868
2047
  return{added:change.add.length,updated:change.update.length,removed:change.remove.length};
1869
2048
  };
2049
+ const warnRejectedRemovals=(route,result)=>{
2050
+ const rejected=(result)?.rejected;
2051
+ if(!Array.isArray(rejected)||!rejected.length)return;
2052
+ const removals=rejected.filter((r)=>r&&r.operation==='remove');
2053
+ if(!removals.length)return;
2054
+ const name=route.label||`route ${route.routeId}`;
2055
+ warnOnce(
2056
+ `data-router:remove-rejected:${route.routeId}`,
2057
+ `route "${name}" asked its target to remove ${removals.length} row(s) the target does not have`
2058
+ +` (for example ${JSON.stringify(String(removals[0].id))}). The rows are still on screen.`
2059
+ +' This is a key mismatch: the key the route derives for a row is not the key the grid derives'
2060
+ +' for it. Check that the route\'s `rowKey` names the same value as the grid\'s.',
2061
+ );
2062
+ };
1870
2063
  const passes=(route,row)=>{
1871
2064
  for(const link of links){
1872
2065
  if(link.to!==route.grid)continue;
@@ -1904,7 +2097,9 @@ shown=new Map(summaries.map((s)=>[s.gkey,s.row]));
1904
2097
  let rows=kept;
1905
2098
  if(route.sort)rows=rows.slice().sort(route.sort);
1906
2099
  shown=new Map();
1907
- for(const row of rows)shown.set(String(route.keyOf(row)),route.transform?route.transform(row):row);
2100
+ for(const row of rows){
2101
+ shown.set(compositeKey([route.keyOf(row)]),route.transform?route.transform(row):row);
2102
+ }
1908
2103
  }
1909
2104
  const add=[];
1910
2105
  const update=[];
@@ -2053,6 +2248,7 @@ return()=>{};
2053
2248
  };
2054
2249
  const makeRoute=(grid,match,o)=>({
2055
2250
  grid,
2251
+ routeId:nextRouteId(),
2056
2252
  sink:targetSink(grid),
2057
2253
  match,
2058
2254
  keyOf:resolver(assertRowKeySpec(o.rowKey,'routes'),defaultKeyOf),