@honua/app-platform 0.1.4-beta.0 → 0.1.9-beta.0

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 (209) hide show
  1. package/app/index.d.ts.map +1 -1
  2. package/app/index.js +4 -0
  3. package/contract/types.d.ts +1 -1
  4. package/contract/types.d.ts.map +1 -1
  5. package/contract/types.js +4 -0
  6. package/control-plane/admin-client.d.ts +87 -0
  7. package/control-plane/admin-client.d.ts.map +1 -0
  8. package/control-plane/admin-client.js +169 -0
  9. package/control-plane/client.d.ts.map +1 -1
  10. package/control-plane/client.js +21 -3
  11. package/control-plane/commands/catalog.d.ts +133 -0
  12. package/control-plane/commands/catalog.d.ts.map +1 -0
  13. package/control-plane/commands/catalog.js +353 -0
  14. package/control-plane/commands/errors.d.ts +96 -0
  15. package/control-plane/commands/errors.d.ts.map +1 -0
  16. package/control-plane/commands/errors.js +130 -0
  17. package/control-plane/commands/identity.d.ts +71 -0
  18. package/control-plane/commands/identity.d.ts.map +1 -0
  19. package/control-plane/commands/identity.js +0 -0
  20. package/control-plane/commands/index.d.ts +39 -0
  21. package/control-plane/commands/index.d.ts.map +1 -0
  22. package/control-plane/commands/index.js +34 -0
  23. package/control-plane/commands/runtime.d.ts +146 -0
  24. package/control-plane/commands/runtime.d.ts.map +1 -0
  25. package/control-plane/commands/runtime.js +318 -0
  26. package/control-plane/commands/types.d.ts +226 -0
  27. package/control-plane/commands/types.d.ts.map +1 -0
  28. package/control-plane/commands/types.js +30 -0
  29. package/control-plane/commands/validate.d.ts +27 -0
  30. package/control-plane/commands/validate.d.ts.map +1 -0
  31. package/control-plane/commands/validate.js +132 -0
  32. package/control-plane/generated/admin-api.d.ts +30263 -0
  33. package/control-plane/generated/admin-api.d.ts.map +1 -0
  34. package/control-plane/generated/admin-api.js +5 -0
  35. package/control-plane/generated/admin-operations.d.ts +4053 -0
  36. package/control-plane/generated/admin-operations.d.ts.map +1 -0
  37. package/control-plane/generated/admin-operations.js +820 -0
  38. package/control-plane/index.d.ts +11 -0
  39. package/control-plane/index.d.ts.map +1 -1
  40. package/control-plane/index.js +8 -0
  41. package/control-plane/types.d.ts +14 -0
  42. package/control-plane/types.d.ts.map +1 -1
  43. package/controls/catalog.d.ts.map +1 -1
  44. package/controls/catalog.js +12 -1
  45. package/controls/qualification.d.ts.map +1 -1
  46. package/controls/qualification.js +61 -2
  47. package/core/client.d.ts +6 -0
  48. package/core/client.d.ts.map +1 -1
  49. package/core/client.js +12 -10
  50. package/core/credential-redaction.d.ts +163 -0
  51. package/core/credential-redaction.d.ts.map +1 -0
  52. package/core/credential-redaction.js +389 -0
  53. package/core/error-classifications.d.ts +32 -0
  54. package/core/error-classifications.d.ts.map +1 -1
  55. package/core/error-classifications.js +32 -0
  56. package/core/error-code-registry.d.ts +33 -1
  57. package/core/error-code-registry.d.ts.map +1 -1
  58. package/core/error-code-registry.js +32 -0
  59. package/core/errors.d.ts +1 -1
  60. package/core/errors.d.ts.map +1 -1
  61. package/core/geoparquet-sql.d.ts +1 -1
  62. package/core/geoparquet-sql.js +2 -2
  63. package/core/grpc-adapter.d.ts +2 -2
  64. package/core/grpc-adapter.d.ts.map +1 -1
  65. package/core/grpc-adapter.js +30 -6
  66. package/core/job-run-errors.d.ts +10 -0
  67. package/core/job-run-errors.d.ts.map +1 -0
  68. package/core/job-run-errors.js +14 -0
  69. package/core/job-run-lifecycle.d.ts +34 -0
  70. package/core/job-run-lifecycle.d.ts.map +1 -0
  71. package/core/job-run-lifecycle.js +193 -0
  72. package/core/ogc-processes.d.ts +38 -31
  73. package/core/ogc-processes.d.ts.map +1 -1
  74. package/core/ogc-processes.js +123 -173
  75. package/core/process-runner.js +15 -100
  76. package/core/stac.d.ts +2 -1
  77. package/core/stac.d.ts.map +1 -1
  78. package/core/stac.js +29 -11
  79. package/core/surfaces.d.ts +2 -8
  80. package/core/surfaces.d.ts.map +1 -1
  81. package/core/surfaces.js +21 -112
  82. package/core/types.d.ts +22 -8
  83. package/core/types.d.ts.map +1 -1
  84. package/gen/geospatial/v1/common_pb.d.ts +514 -0
  85. package/gen/geospatial/v1/common_pb.d.ts.map +1 -0
  86. package/gen/geospatial/v1/common_pb.js +335 -0
  87. package/gen/geospatial/v1/execution_types_pb.d.ts +1186 -0
  88. package/gen/geospatial/v1/execution_types_pb.d.ts.map +1 -0
  89. package/gen/geospatial/v1/execution_types_pb.js +396 -0
  90. package/gen/geospatial/v1/feature_service_pb.d.ts +403 -0
  91. package/gen/geospatial/v1/feature_service_pb.d.ts.map +1 -0
  92. package/gen/geospatial/v1/feature_service_pb.js +47 -0
  93. package/gen/geospatial/v1/index.d.ts +5 -0
  94. package/gen/geospatial/v1/index.d.ts.map +1 -0
  95. package/gen/geospatial/v1/index.js +9 -0
  96. package/gen/geospatial/v1/spatial_types_pb.d.ts +285 -0
  97. package/gen/geospatial/v1/spatial_types_pb.d.ts.map +1 -0
  98. package/gen/geospatial/v1/spatial_types_pb.js +58 -0
  99. package/gen/geospatial/v1/workspace_artifact_types_pb.d.ts +379 -0
  100. package/gen/geospatial/v1/workspace_artifact_types_pb.d.ts.map +1 -0
  101. package/gen/geospatial/v1/workspace_artifact_types_pb.js +151 -0
  102. package/generated-app/projection.d.ts.map +1 -1
  103. package/generated-app/projection.js +10 -1
  104. package/interactions/declarative-contract.d.ts +149 -0
  105. package/interactions/declarative-contract.d.ts.map +1 -0
  106. package/interactions/declarative-contract.js +293 -0
  107. package/interactions/declarative.d.ts +198 -0
  108. package/interactions/declarative.d.ts.map +1 -0
  109. package/interactions/declarative.js +470 -0
  110. package/interactions/feature-state.d.ts +7 -0
  111. package/interactions/feature-state.d.ts.map +1 -1
  112. package/interactions/feature-state.js +10 -2
  113. package/map/maplibre-target.js +1 -1
  114. package/operate/client.d.ts +11 -1
  115. package/operate/client.d.ts.map +1 -1
  116. package/operate/client.js +39 -2
  117. package/operate/index.d.ts +1 -1
  118. package/operate/index.d.ts.map +1 -1
  119. package/operate/types.d.ts +40 -6
  120. package/operate/types.d.ts.map +1 -1
  121. package/package.json +2 -2
  122. package/query-planner/grpc.d.ts +4 -4
  123. package/query-planner/grpc.d.ts.map +1 -1
  124. package/query-planner/grpc.js +11 -11
  125. package/query-planner/types.d.ts +4 -4
  126. package/query-planner/types.d.ts.map +1 -1
  127. package/realtime/sse.d.ts.map +1 -1
  128. package/realtime/sse.js +7 -1
  129. package/runtime/errors.d.ts +1 -1
  130. package/runtime/errors.d.ts.map +1 -1
  131. package/runtime/errors.js +2 -0
  132. package/runtime/generated/map-package-schema-meta.d.ts +11 -0
  133. package/runtime/generated/map-package-schema-meta.d.ts.map +1 -0
  134. package/runtime/generated/map-package-schema-meta.js +12 -0
  135. package/runtime/generated/map-package-schema-validator.d.ts +16 -0
  136. package/runtime/generated/map-package-schema-validator.d.ts.map +1 -0
  137. package/runtime/generated/map-package-schema-validator.js +1422 -0
  138. package/runtime/index.d.ts +5 -1
  139. package/runtime/index.d.ts.map +1 -1
  140. package/runtime/index.js +6 -0
  141. package/runtime/map-package-export.d.ts +151 -0
  142. package/runtime/map-package-export.d.ts.map +1 -0
  143. package/runtime/map-package-export.js +636 -0
  144. package/runtime/map-package-validation.d.ts +21 -1
  145. package/runtime/map-package-validation.d.ts.map +1 -1
  146. package/runtime/map-package-validation.js +132 -95
  147. package/runtime/map-package.d.ts +80 -0
  148. package/runtime/map-package.d.ts.map +1 -1
  149. package/runtime/style-compose.d.ts.map +1 -1
  150. package/runtime/style-compose.js +78 -9
  151. package/scene-workspace/cesium-adapter.d.ts +8 -8
  152. package/scene-workspace/cesium-adapter.d.ts.map +1 -1
  153. package/scene-workspace/cesium-adapter.js +31 -18
  154. package/scene-workspace/primitives.d.ts.map +1 -1
  155. package/scene-workspace/primitives.js +9 -6
  156. package/scene-workspace/source-to-cesium.js +1 -1
  157. package/studio/chart-spec.d.ts +6 -6
  158. package/studio/chart-spec.d.ts.map +1 -1
  159. package/studio/chart-spec.js +6 -6
  160. package/studio/index.d.ts +17 -7
  161. package/studio/index.d.ts.map +1 -1
  162. package/studio/index.js +15 -5
  163. package/studio/lifecycle-client.d.ts +262 -22
  164. package/studio/lifecycle-client.d.ts.map +1 -1
  165. package/studio/lifecycle-client.js +524 -24
  166. package/studio/lifecycle-errors.d.ts +8 -2
  167. package/studio/lifecycle-errors.d.ts.map +1 -1
  168. package/studio/lifecycle-errors.js +6 -1
  169. package/studio/lifecycle-types.d.ts +228 -8
  170. package/studio/lifecycle-types.d.ts.map +1 -1
  171. package/studio/publication-status.d.ts +55 -0
  172. package/studio/publication-status.d.ts.map +1 -0
  173. package/studio/publication-status.js +103 -0
  174. package/web-components/application-context.d.ts +186 -0
  175. package/web-components/application-context.d.ts.map +1 -0
  176. package/web-components/application-context.js +429 -0
  177. package/web-components/elements.d.ts +10 -1
  178. package/web-components/elements.d.ts.map +1 -1
  179. package/web-components/elements.js +187 -7
  180. package/web-components/export-redaction.d.ts +11 -102
  181. package/web-components/export-redaction.d.ts.map +1 -1
  182. package/web-components/export-redaction.js +7 -260
  183. package/web-components/feature-editor-offline.d.ts +32 -0
  184. package/web-components/feature-editor-offline.d.ts.map +1 -0
  185. package/web-components/feature-editor-offline.js +115 -0
  186. package/web-components/feature-editor-workflow.d.ts +85 -22
  187. package/web-components/feature-editor-workflow.d.ts.map +1 -1
  188. package/web-components/feature-editor-workflow.js +240 -0
  189. package/web-components/feature-editor.d.ts +20 -6
  190. package/web-components/feature-editor.d.ts.map +1 -1
  191. package/web-components/feature-editor.js +159 -7
  192. package/web-components/feature-inspection.d.ts +287 -0
  193. package/web-components/feature-inspection.d.ts.map +1 -0
  194. package/web-components/feature-inspection.js +1464 -0
  195. package/web-components/feature-mutation.d.ts +77 -0
  196. package/web-components/feature-mutation.d.ts.map +1 -0
  197. package/web-components/feature-mutation.js +137 -0
  198. package/web-components/feature-table-engine.d.ts +94 -4
  199. package/web-components/feature-table-engine.d.ts.map +1 -1
  200. package/web-components/feature-table-engine.js +365 -18
  201. package/web-components/feature-table-view.d.ts +35 -1
  202. package/web-components/feature-table-view.d.ts.map +1 -1
  203. package/web-components/feature-table-view.js +128 -12
  204. package/web-components/index.d.ts +14 -6
  205. package/web-components/index.d.ts.map +1 -1
  206. package/web-components/index.js +8 -2
  207. package/gen/honua/v1/feature_service_pb.d.ts +0 -918
  208. package/gen/honua/v1/feature_service_pb.d.ts.map +0 -1
  209. package/gen/honua/v1/feature_service_pb.js +0 -375
@@ -29,6 +29,7 @@
29
29
  *
30
30
  * @module
31
31
  */
32
+ import { envelope } from "../core/spatial-filter.js";
32
33
  import { isSourceQualifiedSelectionTarget, sourceFeatureSelectionTarget } from "../exploration/selection.js";
33
34
  import { projectFilterRegistryToQuery } from "../filter-registry/index.js";
34
35
  import { reconcileRealtimeKeyedState, reconcileRealtimeViewport } from "../realtime/reconciliation.js";
@@ -43,12 +44,26 @@ export const DEFAULT_FEATURE_TABLE_BUDGETS = Object.freeze({
43
44
  maxCachedBytes: 4 * 1024 * 1024,
44
45
  maxRequests: 64,
45
46
  maxExportRows: 10_000,
47
+ maxFields: 64,
48
+ maxRenderedColumns: 12,
49
+ maxExportBytes: 16 * 1024 * 1024,
46
50
  windowOverscan: 20,
47
51
  });
48
52
  /** Select the work items attributed to one execution tier. */
49
53
  export function featureTableWorkByTier(evidence, tier) {
50
54
  return Object.freeze(evidence.work.filter((item) => item.tier === tier));
51
55
  }
56
+ /** Horizontal counterpart of {@link featureTableWindow}. */
57
+ export function featureTableColumnWindow(metrics, options) {
58
+ const width = metrics.columnWidth > 0 ? metrics.columnWidth : 1;
59
+ const overscan = Math.max(0, options.overscan ?? 1);
60
+ const total = Math.max(0, options.totalColumns);
61
+ const first = Math.max(0, Math.floor(Math.max(0, metrics.scrollLeft) / width) - overscan);
62
+ const visible = Math.max(1, Math.ceil(Math.max(0, metrics.viewportWidth) / width));
63
+ const span = Math.min(options.maxRenderedColumns, visible + overscan * 2);
64
+ const startIndex = Math.min(first, Math.max(0, total - span));
65
+ return Object.freeze({ startIndex, endIndex: Math.min(total, startIndex + span) });
66
+ }
52
67
  /**
53
68
  * Pure window computation: which absolute row indices a renderer must paint
54
69
  * for the given scroll geometry, widened by `overscan` and clamped to
@@ -117,11 +132,17 @@ export function createHonuaFeatureTable(options) {
117
132
  const selectionTargetIndex = new Map();
118
133
  const misconfiguredPageSize = budgets.pageSize > budgets.maxCachedRows;
119
134
  let columns = resolveColumns(options.columns ?? []);
135
+ let viewportMode = options.viewportMode ?? "independent";
136
+ let viewportExtent = options.viewportExtent ? Object.freeze({ ...options.viewportExtent }) : undefined;
120
137
  let sort = Object.freeze([...(options.sort ?? [])]);
121
138
  let filters = normalizeFilters(options.filters);
122
139
  let selection = Object.freeze([]);
123
140
  let focus;
124
141
  let window = Object.freeze({ startIndex: 0, endIndex: 0 });
142
+ let columnWindow = Object.freeze({
143
+ startIndex: 0,
144
+ endIndex: Math.min(columns.filter((column) => column.visible).length, budgets.maxRenderedColumns),
145
+ });
125
146
  let state = identityField && !misconfiguredPageSize && queryCapabilityAdvertised !== false ? "idle" : "unsupported";
126
147
  let stale = false;
127
148
  let message = !identityField
@@ -132,18 +153,21 @@ export function createHonuaFeatureTable(options) {
132
153
  ? `\`budgets.pageSize\` (${budgets.pageSize}) exceeds \`budgets.maxCachedRows\` (${budgets.maxCachedRows}), so ${PAGE_SIZE_HINT}`
133
154
  : undefined;
134
155
  /** Set when a bounded operation had to give something up; survives a successful run. */
135
- let budgetNotice;
156
+ let budgetNotice = columns.filter((column) => column.visible).length > budgets.maxFields
157
+ ? `Only the first ${budgets.maxFields} visible fields are projected and rendered.`
158
+ : undefined;
136
159
  let error;
137
160
  let conflicts = Object.freeze([]);
138
161
  let evidence = EMPTY_EVIDENCE;
139
162
  let count = Object.freeze({ kind: "unknown", loaded: 0, evidence: "none" });
163
+ const initialExhausted = columns.filter((column) => column.visible).length > budgets.maxFields ? ["fields"] : [];
140
164
  let ledger = Object.freeze({
141
165
  requests: 0,
142
166
  lifetimeRequests: 0,
143
167
  rows: 0,
144
168
  bytes: 0,
145
169
  evictedRows: 0,
146
- exhausted: Object.freeze([]),
170
+ exhausted: Object.freeze(initialExhausted),
147
171
  });
148
172
  let totalKnown;
149
173
  /** Which evidence produced `totalKnown`. Never inferred after the fact. */
@@ -159,11 +183,16 @@ export function createHonuaFeatureTable(options) {
159
183
  let inFlight;
160
184
  // ── Snapshot assembly ───────────────────────────────────────
161
185
  function buildSnapshot() {
186
+ const visibleColumns = columns.filter((column) => column.visible).slice(0, budgets.maxFields);
187
+ const boundedColumnWindow = clampColumnWindow(columnWindow, visibleColumns.length, budgets.maxRenderedColumns);
188
+ columnWindow = boundedColumnWindow;
162
189
  return Object.freeze({
163
190
  sourceId: options.sourceId,
164
191
  state,
165
192
  columns,
166
- visibleColumns: Object.freeze(columns.filter((column) => column.visible)),
193
+ visibleColumns: Object.freeze(visibleColumns),
194
+ renderedColumns: Object.freeze(visibleColumns.slice(boundedColumnWindow.startIndex, boundedColumnWindow.endIndex)),
195
+ columnWindow: boundedColumnWindow,
167
196
  rows: Object.freeze(rowsInWindow()),
168
197
  window,
169
198
  count,
@@ -177,6 +206,8 @@ export function createHonuaFeatureTable(options) {
177
206
  evidence,
178
207
  conflicts,
179
208
  stale,
209
+ viewportMode,
210
+ ...(viewportExtent ? { viewportExtent } : {}),
180
211
  ...(message !== undefined ? { message } : {}),
181
212
  ...(error !== undefined ? { error } : {}),
182
213
  revision,
@@ -229,14 +260,20 @@ export function createHonuaFeatureTable(options) {
229
260
  }
230
261
  // ── Query composition ───────────────────────────────────────
231
262
  function projection() {
232
- const fields = columns.filter((column) => column.visible).map((column) => column.field);
263
+ const fields = columns
264
+ .filter((column) => column.visible)
265
+ .slice(0, budgets.maxFields)
266
+ .map((column) => column.field);
233
267
  if (fields.length === 0)
234
268
  return undefined;
235
269
  return identityField && !fields.includes(identityField) ? [...fields, identityField] : fields;
236
270
  }
237
271
  function orderedFields() {
238
- const visible = columns.filter((column) => column.visible).map((column) => column.field);
239
- return visible.length > 0 ? visible : columns.map((column) => column.field);
272
+ const visible = columns
273
+ .filter((column) => column.visible)
274
+ .slice(0, budgets.maxFields)
275
+ .map((column) => column.field);
276
+ return visible.length > 0 ? visible : columns.slice(0, budgets.maxFields).map((column) => column.field);
240
277
  }
241
278
  function composeQuery(page) {
242
279
  const descriptor = options.source.descriptor;
@@ -247,15 +284,29 @@ export function createHonuaFeatureTable(options) {
247
284
  : {}),
248
285
  });
249
286
  const outFields = projection();
287
+ const extentFilter = viewportMode === "linked" && viewportExtent
288
+ ? envelope(viewportExtent.xmin, viewportExtent.ymin, viewportExtent.xmax, viewportExtent.ymax, viewportExtent.spatialReference)
289
+ : undefined;
290
+ const combined = combineFeatureTableSpatialFilters(extentFilter, projected.query.spatialFilter);
291
+ const spatialFilter = combined.filter;
250
292
  const query = {
251
293
  ...(projected.where !== undefined ? { where: projected.where } : {}),
252
- ...(projected.query.spatialFilter ? { spatialFilter: projected.query.spatialFilter } : {}),
294
+ ...(spatialFilter ? { spatialFilter } : {}),
253
295
  ...(outFields ? { outFields } : {}),
254
296
  ...(sort.length > 0 ? { orderBy: [...sort] } : {}),
255
297
  pagination: { ...(page.offset === undefined ? {} : { offset: page.offset }), limit: page.limit },
256
298
  returnGeometry,
257
299
  };
258
- return { query, degraded: projected.degraded ?? [], filterKey: projected.cacheKey };
300
+ const extentKey = extentFilter ? JSON.stringify(extentFilter.geometry) : "independent";
301
+ // The disjoint case is part of the cache identity: panning back into the
302
+ // filtered area must not be served the empty page cached while outside it.
303
+ const disjointKey = combined.disjoint ? "|disjoint" : "";
304
+ return {
305
+ query,
306
+ degraded: projected.degraded ?? [],
307
+ filterKey: `${projected.cacheKey}|viewport=${extentKey}${disjointKey}`,
308
+ empty: combined.disjoint,
309
+ };
259
310
  }
260
311
  function identity(filterKey, planFingerprint) {
261
312
  const descriptor = options.source.descriptor;
@@ -408,6 +459,19 @@ export function createHonuaFeatureTable(options) {
408
459
  evidence = buildEvidence(plan, composed.degraded, cacheKey);
409
460
  return cached;
410
461
  }
462
+ if (composed.empty) {
463
+ // No request: the intersection is empty by construction. The page is
464
+ // cached under the disjoint filter key so re-entering the filtered area
465
+ // rebuilds a real query rather than reusing this one.
466
+ sequence += 1;
467
+ const emptyPage = { offset, rows: Object.freeze([]), bytes: 0, cacheKey, sequence };
468
+ pages.set(offset, emptyPage);
469
+ exhausted = true;
470
+ totalKnown = 0;
471
+ totalEvidence = "exhausted-pages";
472
+ evidence = buildEvidence(plan, composed.degraded, cacheKey);
473
+ return emptyPage;
474
+ }
411
475
  if (ledger.requests >= budgets.maxRequests) {
412
476
  markExhausted("requests");
413
477
  return undefined;
@@ -615,7 +679,12 @@ export function createHonuaFeatureTable(options) {
615
679
  // refuse to load a brand-new question forever, so the per-identity counter
616
680
  // and its exhaustion flags reset while the lifetime totals stay intact.
617
681
  ledger = Object.freeze({ ...ledger, requests: 0, exhausted: Object.freeze([]) });
618
- budgetNotice = undefined;
682
+ budgetNotice =
683
+ columns.filter((column) => column.visible).length > budgets.maxFields
684
+ ? `Only the first ${budgets.maxFields} visible fields are projected and rendered.`
685
+ : undefined;
686
+ if (budgetNotice)
687
+ markExhausted("fields");
619
688
  conflicts = Object.freeze([]);
620
689
  if (state !== "unsupported")
621
690
  state = "idle";
@@ -722,6 +791,7 @@ export function createHonuaFeatureTable(options) {
722
791
  const nextField = fields[field];
723
792
  if (!row || nextField === undefined)
724
793
  return publish();
794
+ revealColumn(field, fields.length);
725
795
  focus = Object.freeze({ rowKey: row.key, field: nextField });
726
796
  return publish();
727
797
  }
@@ -733,6 +803,13 @@ export function createHonuaFeatureTable(options) {
733
803
  }
734
804
  return max;
735
805
  }
806
+ function revealColumn(index, totalColumns) {
807
+ if (index >= columnWindow.startIndex && index < columnWindow.endIndex)
808
+ return;
809
+ const span = Math.min(budgets.maxRenderedColumns, totalColumns);
810
+ const startIndex = index < columnWindow.startIndex ? index : index - span + 1;
811
+ columnWindow = clampColumnWindow({ startIndex, endIndex: startIndex + span }, totalColumns, span);
812
+ }
736
813
  // ── Realtime reconciliation (REQ-005) ───────────────────────
737
814
  function reconcile(diff) {
738
815
  const keyed = {
@@ -779,6 +856,18 @@ export function createHonuaFeatureTable(options) {
779
856
  const attributes = change.record?.feature.attributes;
780
857
  if (!attributes)
781
858
  continue;
859
+ if (filters.length > 0) {
860
+ stale = true;
861
+ if (state === "ready" || state === "partial")
862
+ state = "stale";
863
+ message = "A live update may have changed filtered membership; refresh the bounded page.";
864
+ nextConflicts.push({
865
+ code: "filter-membership-unknown",
866
+ message,
867
+ rowKeys: Object.freeze([change.key]),
868
+ });
869
+ continue;
870
+ }
782
871
  if (changesSortKey(existing.attributes, attributes, sortFields)) {
783
872
  nextConflicts.push({
784
873
  code: "sort-key-changed",
@@ -876,20 +965,59 @@ export function createHonuaFeatureTable(options) {
876
965
  async function setColumns(nextColumns) {
877
966
  const before = (projection() ?? []).join(",");
878
967
  columns = resolveColumns(nextColumns);
879
- if ((projection() ?? []).join(",") === before)
968
+ const fieldsExceeded = columns.filter((column) => column.visible).length > budgets.maxFields;
969
+ if ((projection() ?? []).join(",") === before) {
970
+ if (fieldsExceeded) {
971
+ markExhausted("fields");
972
+ budgetNotice = `Only the first ${budgets.maxFields} visible fields are projected and rendered.`;
973
+ }
880
974
  return publish();
975
+ }
881
976
  invalidate();
977
+ if (fieldsExceeded) {
978
+ markExhausted("fields");
979
+ budgetNotice = `Only the first ${budgets.maxFields} visible fields are projected and rendered.`;
980
+ }
882
981
  ensureWindow();
883
982
  return run(undefined);
884
983
  }
984
+ async function runAggregation(aggregation, aggregateOptions = {}) {
985
+ throwIfAborted(aggregateOptions.signal);
986
+ const composed = composeQuery({ limit: budgets.pageSize });
987
+ if (composed.empty) {
988
+ return Object.freeze({
989
+ rows: Object.freeze([]),
990
+ truncated: false,
991
+ evidence: buildEvidence(options.planner?.(composed.query), composed.degraded, composed.filterKey),
992
+ });
993
+ }
994
+ const query = {
995
+ ...composed.query,
996
+ aggregation,
997
+ returnGeometry: false,
998
+ pagination: { limit: budgets.pageSize },
999
+ ...(aggregateOptions.signal ? { signal: aggregateOptions.signal } : {}),
1000
+ };
1001
+ const plan = options.planner?.(query);
1002
+ const result = await options.source.query(query);
1003
+ throwIfAborted(aggregateOptions.signal);
1004
+ const aggregateEvidence = buildEvidence(plan, [...composed.degraded, ...(result.degraded ?? [])], composed.filterKey);
1005
+ return Object.freeze({
1006
+ rows: Object.freeze(result.features.slice(0, budgets.pageSize).map((feature) => feature.attributes)),
1007
+ truncated: result.features.length > budgets.pageSize || result.exceededTransferLimit === true,
1008
+ evidence: aggregateEvidence,
1009
+ });
1010
+ }
885
1011
  async function runExport(request) {
886
1012
  const fields = orderedFields();
887
1013
  if (currentState() === "unsupported") {
1014
+ const serialized = serializeBoundedExport(request.format, [], fields, columns, budgets.maxExportBytes);
888
1015
  return Object.freeze({
889
1016
  format: request.format,
890
- content: request.format === "csv" ? toCsv([], fields, columns) : toJson([], fields, columns),
1017
+ content: serialized.content,
891
1018
  rowCount: 0,
892
- truncated: false,
1019
+ truncated: serialized.truncated,
1020
+ ...(serialized.truncated ? { limit: "export-bytes" } : {}),
893
1021
  columns: Object.freeze([...fields]),
894
1022
  evidence,
895
1023
  });
@@ -928,16 +1056,100 @@ export function createHonuaFeatureTable(options) {
928
1056
  }
929
1057
  recomputeCount();
930
1058
  publish();
1059
+ const serialized = serializeBoundedExport(request.format, rows, fields, columns, budgets.maxExportBytes);
1060
+ if (serialized.truncated)
1061
+ limit = "export-bytes";
931
1062
  return Object.freeze({
932
1063
  format: request.format,
933
- content: request.format === "csv" ? toCsv(rows, fields, columns) : toJson(rows, fields, columns),
934
- rowCount: rows.length,
1064
+ content: serialized.content,
1065
+ rowCount: serialized.rowCount,
935
1066
  truncated: limit !== undefined,
936
1067
  ...(limit ? { limit } : {}),
937
1068
  columns: Object.freeze([...fields]),
938
1069
  evidence,
939
1070
  });
940
1071
  }
1072
+ async function runSecureExport(request) {
1073
+ throwIfAborted(request.signal);
1074
+ const fields = orderedFields();
1075
+ const maxRows = Math.max(0, Math.min(request.maxRows ?? budgets.maxExportRows, budgets.maxExportRows));
1076
+ const first = composeQuery({ offset: 0, limit: Math.min(budgets.pageSize, Math.max(1, maxRows)) });
1077
+ const plan = options.planner?.(first.query);
1078
+ const authorization = await request.adapter.authorize({
1079
+ sourceId: options.sourceId,
1080
+ format: request.format,
1081
+ selectionOnly: request.selectionOnly === true,
1082
+ maxRows,
1083
+ maxBytes: budgets.maxExportBytes,
1084
+ authorizationScope,
1085
+ ...(plan ? { planId: plan.id, planFingerprint: plan.fingerprint } : {}),
1086
+ ...(request.signal ? { signal: request.signal } : {}),
1087
+ });
1088
+ throwIfAborted(request.signal);
1089
+ if (!authorization.authorized) {
1090
+ throw new Error(authorization.reason ?? "The host denied the table export under the current authorization.");
1091
+ }
1092
+ const selected = request.selectionOnly ? new Set(selection) : undefined;
1093
+ const rows = [];
1094
+ let offset = 0;
1095
+ let requests = 0;
1096
+ let degraded = first.degraded;
1097
+ let sourceExhausted = false;
1098
+ while (rows.length < maxRows && requests < budgets.maxRequests && !sourceExhausted) {
1099
+ throwIfAborted(request.signal);
1100
+ const composed = composeQuery({ offset, limit: budgets.pageSize });
1101
+ if (composed.empty) {
1102
+ degraded = composed.degraded;
1103
+ sourceExhausted = true;
1104
+ break;
1105
+ }
1106
+ const result = await options.source.query({
1107
+ ...composed.query,
1108
+ ...(request.signal ? { signal: request.signal } : {}),
1109
+ });
1110
+ requests += 1;
1111
+ degraded = [...composed.degraded, ...(result.degraded ?? [])];
1112
+ for (const [index, feature] of result.features.entries()) {
1113
+ const row = toRow(feature, offset + index);
1114
+ if (!row)
1115
+ continue;
1116
+ if ((!selected || selected.has(row.key)) && rows.length < maxRows)
1117
+ rows.push(row);
1118
+ }
1119
+ sourceExhausted = result.features.length < budgets.pageSize && result.exceededTransferLimit !== true;
1120
+ offset += budgets.pageSize;
1121
+ }
1122
+ throwIfAborted(request.signal);
1123
+ const exportEvidence = buildEvidence(plan, degraded, first.filterKey);
1124
+ const serialized = serializeBoundedExport(request.format, rows, fields, columns, budgets.maxExportBytes);
1125
+ const truncated = serialized.truncated ||
1126
+ rows.length >= maxRows ||
1127
+ (!sourceExhausted && requests >= budgets.maxRequests) ||
1128
+ (selected !== undefined && rows.length < selected.size);
1129
+ const provenance = Object.freeze({
1130
+ sourceId: options.sourceId,
1131
+ adapterId: request.adapter.id,
1132
+ authorizationId: authorization.authorizationId,
1133
+ exportedAt: new Date().toISOString(),
1134
+ format: request.format,
1135
+ rowCount: serialized.rowCount,
1136
+ truncated,
1137
+ authorizationScope,
1138
+ ...(plan ? { planId: plan.id, planFingerprint: plan.fingerprint } : {}),
1139
+ });
1140
+ await request.adapter.record(provenance);
1141
+ throwIfAborted(request.signal);
1142
+ return Object.freeze({
1143
+ format: request.format,
1144
+ content: serialized.content,
1145
+ rowCount: serialized.rowCount,
1146
+ truncated,
1147
+ ...(serialized.truncated ? { limit: "export-bytes" } : {}),
1148
+ columns: Object.freeze([...fields]),
1149
+ evidence: exportEvidence,
1150
+ provenance,
1151
+ });
1152
+ }
941
1153
  return {
942
1154
  get snapshot() {
943
1155
  return snapshot;
@@ -960,6 +1172,17 @@ export function createHonuaFeatureTable(options) {
960
1172
  window = next;
961
1173
  return run(undefined);
962
1174
  },
1175
+ setColumnScroll(metrics) {
1176
+ const totalColumns = Math.min(budgets.maxFields, columns.filter((column) => column.visible).length);
1177
+ const next = featureTableColumnWindow(metrics, {
1178
+ totalColumns,
1179
+ maxRenderedColumns: budgets.maxRenderedColumns,
1180
+ });
1181
+ if (next.startIndex === columnWindow.startIndex && next.endIndex === columnWindow.endIndex)
1182
+ return snapshot;
1183
+ columnWindow = next;
1184
+ return publish();
1185
+ },
963
1186
  setSort,
964
1187
  async toggleSort(field, toggleOptions = {}) {
965
1188
  const existing = sort.find((entry) => entry.field === field);
@@ -980,6 +1203,23 @@ export function createHonuaFeatureTable(options) {
980
1203
  async setColumnVisibility(field, visible) {
981
1204
  return setColumns(columns.map((column) => (column.field === field ? { ...column, visible } : { ...column })));
982
1205
  },
1206
+ async setViewportMode(mode) {
1207
+ if (mode === viewportMode)
1208
+ return snapshot;
1209
+ viewportMode = mode;
1210
+ invalidate();
1211
+ ensureWindow();
1212
+ return run(undefined);
1213
+ },
1214
+ async setViewportExtent(extent) {
1215
+ viewportExtent = extent ? Object.freeze({ ...extent }) : undefined;
1216
+ if (viewportMode === "independent")
1217
+ return publish();
1218
+ invalidate();
1219
+ ensureWindow();
1220
+ return run(undefined);
1221
+ },
1222
+ aggregate: runAggregation,
983
1223
  select(keys, selectOptions = {}) {
984
1224
  setSelection(selectOptions.replace === false ? [...selection, ...keys] : keys);
985
1225
  },
@@ -1014,6 +1254,12 @@ export function createHonuaFeatureTable(options) {
1014
1254
  return Object.freeze(out);
1015
1255
  },
1016
1256
  setFocus(nextFocus) {
1257
+ if (nextFocus) {
1258
+ const fields = orderedFields();
1259
+ const index = fields.indexOf(nextFocus.field);
1260
+ if (index >= 0)
1261
+ revealColumn(index, fields.length);
1262
+ }
1017
1263
  focus = nextFocus ? Object.freeze({ ...nextFocus }) : undefined;
1018
1264
  publish();
1019
1265
  },
@@ -1031,6 +1277,7 @@ export function createHonuaFeatureTable(options) {
1031
1277
  publish();
1032
1278
  },
1033
1279
  export: runExport,
1280
+ secureExport: runSecureExport,
1034
1281
  cancel() {
1035
1282
  const controller = inFlight;
1036
1283
  inFlight = undefined;
@@ -1073,6 +1320,10 @@ export function linkFeatureTableToExploration(table, view, options = {}) {
1073
1320
  const syncFilters = options.filters ?? true;
1074
1321
  const syncSelection = options.selection ?? true;
1075
1322
  const syncPage = options.page ?? true;
1323
+ const syncExtent = options.extent ?? false;
1324
+ if (syncExtent) {
1325
+ void table.setViewportMode("linked").then(() => table.setViewportExtent(view.state.extent));
1326
+ }
1076
1327
  let applying = false;
1077
1328
  let suppressFilterPublish = 0;
1078
1329
  /** Filter snapshots requested by inbound assignments; these are suppressed. */
@@ -1096,7 +1347,7 @@ export function linkFeatureTableToExploration(table, view, options = {}) {
1096
1347
  }
1097
1348
  }
1098
1349
  }
1099
- const unsubscribeContext = view.subscribe(["selection", "sort", "visibleFields", "filters"], (event) => {
1350
+ const unsubscribeContext = view.subscribe(["selection", "sort", "visibleFields", "filters", "extent"], (event) => {
1100
1351
  if (applying)
1101
1352
  return;
1102
1353
  applying = true;
@@ -1130,6 +1381,8 @@ export function linkFeatureTableToExploration(table, view, options = {}) {
1130
1381
  }
1131
1382
  });
1132
1383
  }
1384
+ if (syncExtent && event.changedSlices.has("extent"))
1385
+ void table.setViewportExtent(event.state.extent);
1133
1386
  }
1134
1387
  finally {
1135
1388
  applying = false;
@@ -1343,10 +1596,104 @@ function toCsv(rows, fields, columns) {
1343
1596
  }
1344
1597
  return lines.join("\n");
1345
1598
  }
1599
+ /**
1600
+ * Largest row prefix that fits `maxBytes`, found by binary search.
1601
+ *
1602
+ * Trimming one row at a time re-encoded the whole export on every step, so a
1603
+ * 10,000-row export of wide attributes could run thousands of full CSV/JSON
1604
+ * encodings on the main thread -- a bounded export that freezes the page is
1605
+ * not bounded. Both encoders are prefix-monotonic (a longer prefix is never
1606
+ * fewer bytes: CSV appends whole lines, JSON appends whole elements), so the
1607
+ * fitting prefixes form a contiguous range and bisection finds its edge in
1608
+ * O(log n) encodings.
1609
+ */
1610
+ function serializeBoundedExport(format, rows, fields, columns, maxBytes) {
1611
+ const encoder = new TextEncoder();
1612
+ const encode = (kept) => format === "csv" ? toCsv(rows.slice(0, kept), fields, columns) : toJson(rows.slice(0, kept), fields, columns);
1613
+ const fits = (content) => encoder.encode(content).byteLength <= maxBytes;
1614
+ const whole = encode(rows.length);
1615
+ if (fits(whole))
1616
+ return { content: whole, rowCount: rows.length, truncated: false };
1617
+ // Invariant: `best` always fits, `high` never does. The header-only export
1618
+ // is the floor; when even that overflows the budget nothing can be emitted.
1619
+ let best;
1620
+ let low = 0;
1621
+ let high = rows.length;
1622
+ while (low < high) {
1623
+ const middle = Math.floor((low + high) / 2);
1624
+ const content = encode(middle);
1625
+ if (fits(content)) {
1626
+ best = { content, rowCount: middle };
1627
+ low = middle + 1;
1628
+ }
1629
+ else {
1630
+ high = middle;
1631
+ }
1632
+ }
1633
+ if (best)
1634
+ return { content: best.content, rowCount: best.rowCount, truncated: best.rowCount < rows.length };
1635
+ return { content: "", rowCount: 0, truncated: true };
1636
+ }
1346
1637
  function csvCell(value) {
1347
- if (!value.includes(",") && !value.includes('"') && !value.includes("\n"))
1348
- return value;
1349
- return `"${value.split('"').join('""')}"`;
1638
+ // Spreadsheet programs treat leading =, +, -, @, tab, and carriage return
1639
+ // as formula/control syntax even in a CSV cell. Prefixing an apostrophe
1640
+ // forces literal display and prevents a server-returned value from becoming
1641
+ // executable spreadsheet content when a user opens the bounded export.
1642
+ const safe = /^[\t\r]|^\s*[=+\-@]/.test(value) ? `'${value}` : value;
1643
+ if (!safe.includes(",") && !safe.includes('"') && !safe.includes("\n"))
1644
+ return safe;
1645
+ return `"${safe.split('"').join('""')}"`;
1646
+ }
1647
+ function throwIfAborted(signal) {
1648
+ if (!signal?.aborted)
1649
+ return;
1650
+ if (typeof signal.throwIfAborted === "function")
1651
+ signal.throwIfAborted();
1652
+ const error = new Error("The table operation was cancelled.");
1653
+ error.name = "AbortError";
1654
+ throw error;
1655
+ }
1656
+ function sameSpatialReference(left, right) {
1657
+ if (left === undefined || right === undefined)
1658
+ return true;
1659
+ if (left === right)
1660
+ return true;
1661
+ return JSON.stringify(left) === JSON.stringify(right);
1662
+ }
1663
+ function combineFeatureTableSpatialFilters(extent, existing) {
1664
+ if (!extent)
1665
+ return { disjoint: false, filter: existing };
1666
+ if (!existing)
1667
+ return { disjoint: false, filter: extent };
1668
+ if (extent.geometryType !== "esriGeometryEnvelope" || existing.geometryType !== "esriGeometryEnvelope") {
1669
+ throw new Error("A linked table extent cannot be combined exactly with a non-envelope spatial filter; use independent viewport mode or an envelope filter.");
1670
+ }
1671
+ const left = extent.geometry;
1672
+ const right = existing.geometry;
1673
+ const values = [left.xmin, left.ymin, left.xmax, left.ymax, right.xmin, right.ymin, right.xmax, right.ymax];
1674
+ if (!values.every((value) => typeof value === "number" && Number.isFinite(value))) {
1675
+ throw new Error("A linked table extent and spatial filter must contain finite envelope coordinates.");
1676
+ }
1677
+ const leftReference = left.spatialReference;
1678
+ const rightReference = right.spatialReference;
1679
+ if (!sameSpatialReference(leftReference, rightReference)) {
1680
+ throw new Error("A linked table extent and spatial filter must share one spatial reference; reproject the filter or use independent viewport mode.");
1681
+ }
1682
+ const xmin = Math.max(left.xmin, right.xmin);
1683
+ const ymin = Math.max(left.ymin, right.ymin);
1684
+ const xmax = Math.min(left.xmax, right.xmax);
1685
+ const ymax = Math.min(left.ymax, right.ymax);
1686
+ if (xmin > xmax || ymin > ymax)
1687
+ return { disjoint: true };
1688
+ return {
1689
+ disjoint: false,
1690
+ filter: envelope(xmin, ymin, xmax, ymax, (leftReference ?? rightReference)),
1691
+ };
1692
+ }
1693
+ function clampColumnWindow(value, totalColumns, maxRenderedColumns) {
1694
+ const span = Math.min(maxRenderedColumns, totalColumns);
1695
+ const startIndex = Math.min(Math.max(0, value.startIndex), Math.max(0, totalColumns - span));
1696
+ return Object.freeze({ startIndex, endIndex: Math.min(totalColumns, startIndex + span) });
1350
1697
  }
1351
1698
  function toJson(rows, fields, columns) {
1352
1699
  const byField = new Map(columns.map((column) => [column.field, column]));
@@ -16,7 +16,7 @@
16
16
  * @module
17
17
  */
18
18
  import type { SortSpec } from "../contract/types.js";
19
- import { type HonuaFeatureTableConflict, type HonuaFeatureTableCount, type HonuaFeatureTableFocus, type HonuaFeatureTableFocusMove, type HonuaFeatureTableResolvedColumn, type HonuaFeatureTableSnapshot, type HonuaFeatureTableState, type HonuaFeatureTableWindow } from "./feature-table-engine.js";
19
+ import { type HonuaFeatureTableConflict, type HonuaFeatureTableCount, type HonuaFeatureTableFocus, type HonuaFeatureTableFocusMove, type HonuaFeatureTableQueryEvidence, type HonuaFeatureTableResolvedColumn, type HonuaFeatureTableSnapshot, type HonuaFeatureTableState, type HonuaFeatureTableWindow } from "./feature-table-engine.js";
20
20
  import type { HonuaFeatureTableModel } from "./types.js";
21
21
  /** One rendered row. `placeholder` rows sit inside the window but are not resident. */
22
22
  export interface HonuaFeatureTableViewRow {
@@ -29,11 +29,26 @@ export interface HonuaFeatureTableViewRow {
29
29
  readonly cells: readonly string[];
30
30
  readonly placeholder: boolean;
31
31
  }
32
+ /** One visible diagnostic describing where accepted query-plan work ran. */
33
+ export interface HonuaFeatureTableWorkBadge {
34
+ readonly tier: "server" | "worker" | "client";
35
+ readonly label: string;
36
+ readonly detail: string;
37
+ }
38
+ export interface HonuaFeatureTableColumnControl {
39
+ readonly field: string;
40
+ readonly label: string;
41
+ readonly visible: boolean;
42
+ }
32
43
  /** Everything the renderer needs. Derived from an engine snapshot. */
33
44
  export interface HonuaFeatureTableViewModel {
34
45
  readonly label: string;
35
46
  readonly state: HonuaFeatureTableState;
36
47
  readonly columns: readonly HonuaFeatureTableResolvedColumn[];
48
+ readonly totalColumnCount: number;
49
+ readonly columnWindow: HonuaFeatureTableWindow;
50
+ readonly columnWidth: number;
51
+ readonly columnControls: readonly HonuaFeatureTableColumnControl[];
37
52
  readonly rows: readonly HonuaFeatureTableViewRow[];
38
53
  readonly window: HonuaFeatureTableWindow;
39
54
  readonly ariaRowCount: number;
@@ -42,9 +57,19 @@ export interface HonuaFeatureTableViewModel {
42
57
  readonly sort: readonly SortSpec[];
43
58
  readonly focus?: HonuaFeatureTableFocus;
44
59
  readonly conflicts: readonly HonuaFeatureTableConflict[];
60
+ /** Compact server/worker/client diagnostics projected from accepted-plan evidence. */
61
+ readonly workBadges: readonly HonuaFeatureTableWorkBadge[];
45
62
  /** Row height in CSS pixels; drives the virtualization spacers. */
46
63
  readonly rowHeight: number;
47
64
  readonly busy: boolean;
65
+ /**
66
+ * The user unchecked every column. Distinct from a merely empty horizontal
67
+ * window: the grid renders an explicit no-column state instead of a column
68
+ * the controls report as hidden.
69
+ */
70
+ readonly columnsHidden: boolean;
71
+ /** Announced in place of the grid when {@link columnsHidden} is set. */
72
+ readonly columnsHiddenLabel?: string;
48
73
  }
49
74
  /** Caller-supplied labels and announcements for `<honua-feature-table>`. */
50
75
  export interface HonuaFeatureTableMessages {
@@ -52,6 +77,8 @@ export interface HonuaFeatureTableMessages {
52
77
  readonly count?: (count: HonuaFeatureTableCount) => string;
53
78
  readonly status?: Partial<Readonly<Record<HonuaFeatureTableState, string>>>;
54
79
  readonly state?: (state: HonuaFeatureTableState, count: HonuaFeatureTableCount, message?: string) => string;
80
+ /** Shown in place of the grid when every column has been hidden. */
81
+ readonly columnsHidden?: string;
55
82
  }
56
83
  /** Human/screen-reader summary of the table's lifecycle state. */
57
84
  export declare function describeFeatureTableState(state: HonuaFeatureTableState, count: HonuaFeatureTableCount, message: string | undefined, messages?: HonuaFeatureTableMessages): string;
@@ -59,6 +86,7 @@ export declare function describeFeatureTableState(state: HonuaFeatureTableState,
59
86
  export declare function featureTableViewModel<T>(snapshot: HonuaFeatureTableSnapshot<T>, options?: {
60
87
  readonly label?: string;
61
88
  readonly rowHeight?: number;
89
+ readonly columnWidth?: number;
62
90
  readonly messages?: HonuaFeatureTableMessages;
63
91
  }): HonuaFeatureTableViewModel;
64
92
  /**
@@ -74,6 +102,12 @@ export declare function legacyFeatureTableViewModel<T>(model: HonuaFeatureTableM
74
102
  readonly selectedFeatureId?: string;
75
103
  readonly messages?: HonuaFeatureTableMessages;
76
104
  }): HonuaFeatureTableViewModel;
105
+ /**
106
+ * Collapse accepted-plan work into compact, accessible execution diagnostics.
107
+ * The labels never infer pushdown: a tier appears only when the engine's
108
+ * evidence names work that actually ran there.
109
+ */
110
+ export declare function featureTableWorkBadges(evidence: HonuaFeatureTableQueryEvidence): readonly HonuaFeatureTableWorkBadge[];
77
111
  /**
78
112
  * Map a `KeyboardEvent.key` (plus modifiers) onto a grid navigation move.
79
113
  * Returns `undefined` for keys the grid must not swallow.