@honua/app-platform 0.1.7-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.
- package/control-plane/admin-client.d.ts +87 -0
- package/control-plane/admin-client.d.ts.map +1 -0
- package/control-plane/admin-client.js +169 -0
- package/control-plane/client.d.ts.map +1 -1
- package/control-plane/client.js +21 -3
- package/control-plane/commands/catalog.d.ts +133 -0
- package/control-plane/commands/catalog.d.ts.map +1 -0
- package/control-plane/commands/catalog.js +353 -0
- package/control-plane/commands/errors.d.ts +96 -0
- package/control-plane/commands/errors.d.ts.map +1 -0
- package/control-plane/commands/errors.js +130 -0
- package/control-plane/commands/identity.d.ts +71 -0
- package/control-plane/commands/identity.d.ts.map +1 -0
- package/control-plane/commands/identity.js +0 -0
- package/control-plane/commands/index.d.ts +39 -0
- package/control-plane/commands/index.d.ts.map +1 -0
- package/control-plane/commands/index.js +34 -0
- package/control-plane/commands/runtime.d.ts +146 -0
- package/control-plane/commands/runtime.d.ts.map +1 -0
- package/control-plane/commands/runtime.js +318 -0
- package/control-plane/commands/types.d.ts +226 -0
- package/control-plane/commands/types.d.ts.map +1 -0
- package/control-plane/commands/types.js +30 -0
- package/control-plane/commands/validate.d.ts +27 -0
- package/control-plane/commands/validate.d.ts.map +1 -0
- package/control-plane/commands/validate.js +132 -0
- package/control-plane/generated/admin-api.d.ts +30263 -0
- package/control-plane/generated/admin-api.d.ts.map +1 -0
- package/control-plane/generated/admin-api.js +5 -0
- package/control-plane/generated/admin-operations.d.ts +4053 -0
- package/control-plane/generated/admin-operations.d.ts.map +1 -0
- package/control-plane/generated/admin-operations.js +820 -0
- package/control-plane/index.d.ts +11 -0
- package/control-plane/index.d.ts.map +1 -1
- package/control-plane/index.js +8 -0
- package/control-plane/types.d.ts +14 -0
- package/control-plane/types.d.ts.map +1 -1
- package/controls/catalog.d.ts.map +1 -1
- package/controls/catalog.js +12 -1
- package/controls/qualification.d.ts.map +1 -1
- package/controls/qualification.js +61 -2
- package/core/client.js +1 -1
- package/core/credential-redaction.d.ts +163 -0
- package/core/credential-redaction.d.ts.map +1 -0
- package/core/credential-redaction.js +389 -0
- package/core/error-classifications.d.ts +15 -0
- package/core/error-classifications.d.ts.map +1 -1
- package/core/error-classifications.js +15 -0
- package/core/error-code-registry.d.ts +15 -0
- package/core/error-code-registry.d.ts.map +1 -1
- package/core/error-code-registry.js +15 -0
- package/core/grpc-adapter.d.ts +2 -2
- package/core/grpc-adapter.d.ts.map +1 -1
- package/core/grpc-adapter.js +30 -6
- package/core/job-run-errors.d.ts +10 -0
- package/core/job-run-errors.d.ts.map +1 -0
- package/core/job-run-errors.js +14 -0
- package/core/job-run-lifecycle.d.ts +34 -0
- package/core/job-run-lifecycle.d.ts.map +1 -0
- package/core/job-run-lifecycle.js +193 -0
- package/core/ogc-processes.d.ts +38 -31
- package/core/ogc-processes.d.ts.map +1 -1
- package/core/ogc-processes.js +123 -173
- package/core/process-runner.js +15 -100
- package/core/surfaces.d.ts +2 -8
- package/core/surfaces.d.ts.map +1 -1
- package/core/surfaces.js +21 -112
- package/core/types.d.ts +7 -6
- package/core/types.d.ts.map +1 -1
- package/gen/geospatial/v1/common_pb.d.ts +514 -0
- package/gen/geospatial/v1/common_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/common_pb.js +335 -0
- package/gen/geospatial/v1/execution_types_pb.d.ts +1186 -0
- package/gen/geospatial/v1/execution_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/execution_types_pb.js +396 -0
- package/gen/geospatial/v1/feature_service_pb.d.ts +403 -0
- package/gen/geospatial/v1/feature_service_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/feature_service_pb.js +47 -0
- package/gen/geospatial/v1/index.d.ts +5 -0
- package/gen/geospatial/v1/index.d.ts.map +1 -0
- package/gen/geospatial/v1/index.js +9 -0
- package/gen/geospatial/v1/spatial_types_pb.d.ts +285 -0
- package/gen/geospatial/v1/spatial_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/spatial_types_pb.js +58 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.d.ts +379 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.d.ts.map +1 -0
- package/gen/geospatial/v1/workspace_artifact_types_pb.js +151 -0
- package/generated-app/projection.d.ts.map +1 -1
- package/generated-app/projection.js +10 -1
- package/operate/client.d.ts +11 -1
- package/operate/client.d.ts.map +1 -1
- package/operate/client.js +39 -2
- package/operate/index.d.ts +1 -1
- package/operate/index.d.ts.map +1 -1
- package/operate/types.d.ts +40 -6
- package/operate/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/query-planner/grpc.d.ts +4 -4
- package/query-planner/grpc.d.ts.map +1 -1
- package/query-planner/grpc.js +11 -11
- package/query-planner/types.d.ts +4 -4
- package/query-planner/types.d.ts.map +1 -1
- package/runtime/errors.d.ts +1 -1
- package/runtime/errors.d.ts.map +1 -1
- package/runtime/errors.js +2 -0
- package/runtime/generated/map-package-schema-meta.d.ts +11 -0
- package/runtime/generated/map-package-schema-meta.d.ts.map +1 -0
- package/runtime/generated/map-package-schema-meta.js +12 -0
- package/runtime/generated/map-package-schema-validator.d.ts +16 -0
- package/runtime/generated/map-package-schema-validator.d.ts.map +1 -0
- package/runtime/generated/map-package-schema-validator.js +1422 -0
- package/runtime/index.d.ts +5 -1
- package/runtime/index.d.ts.map +1 -1
- package/runtime/index.js +6 -0
- package/runtime/map-package-export.d.ts +151 -0
- package/runtime/map-package-export.d.ts.map +1 -0
- package/runtime/map-package-export.js +636 -0
- package/runtime/map-package-validation.d.ts +21 -1
- package/runtime/map-package-validation.d.ts.map +1 -1
- package/runtime/map-package-validation.js +132 -95
- package/runtime/map-package.d.ts +80 -0
- package/runtime/map-package.d.ts.map +1 -1
- package/scene-workspace/cesium-adapter.d.ts +8 -8
- package/scene-workspace/cesium-adapter.d.ts.map +1 -1
- package/scene-workspace/cesium-adapter.js +31 -18
- package/scene-workspace/primitives.d.ts.map +1 -1
- package/scene-workspace/primitives.js +9 -6
- package/scene-workspace/source-to-cesium.js +1 -1
- package/studio/chart-spec.d.ts +6 -6
- package/studio/chart-spec.d.ts.map +1 -1
- package/studio/chart-spec.js +6 -6
- package/studio/index.d.ts +17 -7
- package/studio/index.d.ts.map +1 -1
- package/studio/index.js +15 -5
- package/studio/lifecycle-client.d.ts +262 -22
- package/studio/lifecycle-client.d.ts.map +1 -1
- package/studio/lifecycle-client.js +524 -24
- package/studio/lifecycle-errors.d.ts +8 -2
- package/studio/lifecycle-errors.d.ts.map +1 -1
- package/studio/lifecycle-errors.js +6 -1
- package/studio/lifecycle-types.d.ts +228 -8
- package/studio/lifecycle-types.d.ts.map +1 -1
- package/studio/publication-status.d.ts +55 -0
- package/studio/publication-status.d.ts.map +1 -0
- package/studio/publication-status.js +103 -0
- package/web-components/application-context.d.ts +186 -0
- package/web-components/application-context.d.ts.map +1 -0
- package/web-components/application-context.js +429 -0
- package/web-components/elements.d.ts +10 -1
- package/web-components/elements.d.ts.map +1 -1
- package/web-components/elements.js +187 -7
- package/web-components/export-redaction.d.ts +11 -102
- package/web-components/export-redaction.d.ts.map +1 -1
- package/web-components/export-redaction.js +7 -260
- package/web-components/feature-editor-offline.d.ts +32 -0
- package/web-components/feature-editor-offline.d.ts.map +1 -0
- package/web-components/feature-editor-offline.js +115 -0
- package/web-components/feature-editor-workflow.d.ts +85 -22
- package/web-components/feature-editor-workflow.d.ts.map +1 -1
- package/web-components/feature-editor-workflow.js +240 -0
- package/web-components/feature-editor.d.ts +20 -6
- package/web-components/feature-editor.d.ts.map +1 -1
- package/web-components/feature-editor.js +159 -7
- package/web-components/feature-inspection.d.ts +287 -0
- package/web-components/feature-inspection.d.ts.map +1 -0
- package/web-components/feature-inspection.js +1464 -0
- package/web-components/feature-mutation.d.ts +77 -0
- package/web-components/feature-mutation.d.ts.map +1 -0
- package/web-components/feature-mutation.js +137 -0
- package/web-components/feature-table-engine.d.ts +94 -4
- package/web-components/feature-table-engine.d.ts.map +1 -1
- package/web-components/feature-table-engine.js +365 -18
- package/web-components/feature-table-view.d.ts +35 -1
- package/web-components/feature-table-view.d.ts.map +1 -1
- package/web-components/feature-table-view.js +128 -12
- package/web-components/index.d.ts +14 -6
- package/web-components/index.d.ts.map +1 -1
- package/web-components/index.js +8 -2
- package/gen/honua/v1/feature_service_pb.d.ts +0 -918
- package/gen/honua/v1/feature_service_pb.d.ts.map +0 -1
- 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(
|
|
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
|
|
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
|
|
239
|
-
|
|
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
|
-
...(
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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:
|
|
1017
|
+
content: serialized.content,
|
|
891
1018
|
rowCount: 0,
|
|
892
|
-
truncated:
|
|
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:
|
|
934
|
-
rowCount:
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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.
|