@honua/app-platform 0.1.12 → 0.1.14
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/columnar/apache-arrow.d.ts +17 -0
- package/columnar/apache-arrow.d.ts.map +1 -0
- package/columnar/apache-arrow.js +1036 -0
- package/columnar/batch-cache-conformance.d.ts +66 -0
- package/columnar/batch-cache-conformance.d.ts.map +1 -0
- package/columnar/batch-cache-conformance.js +317 -0
- package/columnar/batch-cache-indexeddb.d.ts +31 -0
- package/columnar/batch-cache-indexeddb.d.ts.map +1 -0
- package/columnar/batch-cache-indexeddb.js +148 -0
- package/columnar/batch-cache.d.ts +298 -0
- package/columnar/batch-cache.d.ts.map +1 -0
- package/columnar/batch-cache.js +757 -0
- package/columnar/geoarrow-aggregate.d.ts +178 -0
- package/columnar/geoarrow-aggregate.d.ts.map +1 -0
- package/columnar/geoarrow-aggregate.js +976 -0
- package/columnar/geoarrow-patch.d.ts +382 -0
- package/columnar/geoarrow-patch.d.ts.map +1 -0
- package/columnar/geoarrow-patch.js +1656 -0
- package/columnar/geoarrow-reproject.d.ts +22 -0
- package/columnar/geoarrow-reproject.d.ts.map +1 -0
- package/columnar/geoarrow-reproject.js +119 -0
- package/columnar/geoarrow-serialization.d.ts +68 -0
- package/columnar/geoarrow-serialization.d.ts.map +1 -0
- package/columnar/geoarrow-serialization.js +331 -0
- package/columnar/geoarrow-types.d.ts +271 -0
- package/columnar/geoarrow-types.d.ts.map +1 -0
- package/columnar/geoarrow-types.js +15 -0
- package/columnar/geoarrow.d.ts +52 -0
- package/columnar/geoarrow.d.ts.map +1 -0
- package/columnar/geoarrow.js +1469 -0
- package/columnar/index.d.ts +43 -0
- package/columnar/index.d.ts.map +1 -0
- package/columnar/index.js +29 -0
- package/columnar/operators.d.ts +70 -0
- package/columnar/operators.d.ts.map +1 -0
- package/columnar/operators.js +307 -0
- package/columnar/result.d.ts +369 -0
- package/columnar/result.d.ts.map +1 -0
- package/columnar/result.js +815 -0
- package/columnar/telemetry.d.ts +152 -0
- package/columnar/telemetry.d.ts.map +1 -0
- package/columnar/telemetry.js +265 -0
- package/columnar/transfer.d.ts +28 -0
- package/columnar/transfer.d.ts.map +1 -0
- package/columnar/transfer.js +669 -0
- package/columnar/types.d.ts +175 -0
- package/columnar/types.d.ts.map +1 -0
- package/columnar/types.js +22 -0
- package/columnar/worker.d.ts +157 -0
- package/columnar/worker.d.ts.map +1 -0
- package/columnar/worker.js +1059 -0
- package/contract/pmtiles.d.ts +6 -5
- package/contract/pmtiles.d.ts.map +1 -1
- package/contract/pmtiles.js +30 -5
- package/contract/source.d.ts.map +1 -1
- package/contract/source.js +11 -8
- package/controls/catalog.d.ts.map +1 -1
- package/controls/catalog.js +110 -0
- package/controls/index.d.ts +1 -1
- package/controls/index.js +1 -1
- package/controls/types.d.ts +1 -1
- package/controls/types.js +1 -1
- package/core/client.d.ts.map +1 -1
- package/core/client.js +87 -19
- package/core/deprecation-notice.d.ts +28 -0
- package/core/deprecation-notice.d.ts.map +1 -0
- package/core/deprecation-notice.js +22 -0
- package/core/esri-geojson.d.ts.map +1 -1
- package/core/esri-geojson.js +14 -8
- package/core/optional-grpc-peer.d.ts +15 -0
- package/core/optional-grpc-peer.d.ts.map +1 -0
- package/core/optional-grpc-peer.js +60 -0
- package/esri-compat/basemap-gallery.d.ts +2 -0
- package/esri-compat/basemap-gallery.d.ts.map +1 -1
- package/esri-compat/basemap-gallery.js +12 -0
- package/esri-compat/basemap.d.ts +4 -0
- package/esri-compat/basemap.d.ts.map +1 -1
- package/esri-compat/basemap.js +70 -0
- package/esri-compat/bookmarks.d.ts +2 -0
- package/esri-compat/bookmarks.d.ts.map +1 -1
- package/esri-compat/bookmarks.js +12 -0
- package/esri-compat/controls.d.ts +18 -0
- package/esri-compat/controls.d.ts.map +1 -1
- package/esri-compat/controls.js +120 -57
- package/esri-compat/directions.d.ts +4 -0
- package/esri-compat/directions.d.ts.map +1 -1
- package/esri-compat/directions.js +30 -20
- package/esri-compat/editor.d.ts +4 -0
- package/esri-compat/editor.d.ts.map +1 -1
- package/esri-compat/editor.js +16 -0
- package/esri-compat/esri-config.d.ts +2 -1
- package/esri-compat/esri-config.d.ts.map +1 -1
- package/esri-compat/feature-effect.d.ts +23 -0
- package/esri-compat/feature-effect.d.ts.map +1 -0
- package/esri-compat/feature-effect.js +12 -0
- package/esri-compat/feature-form.d.ts +6 -0
- package/esri-compat/feature-form.d.ts.map +1 -1
- package/esri-compat/feature-form.js +63 -0
- package/esri-compat/feature-layer.d.ts +21 -2
- package/esri-compat/feature-layer.d.ts.map +1 -1
- package/esri-compat/feature-layer.js +172 -1
- package/esri-compat/feature-table.d.ts +5 -0
- package/esri-compat/feature-table.d.ts.map +1 -1
- package/esri-compat/feature-table.js +23 -0
- package/esri-compat/identity-manager.d.ts +1 -1
- package/esri-compat/identity-manager.js +1 -1
- package/esri-compat/legend.d.ts +2 -0
- package/esri-compat/legend.d.ts.map +1 -1
- package/esri-compat/legend.js +98 -0
- package/esri-compat/locator.d.ts +1 -1
- package/esri-compat/locator.js +1 -1
- package/esri-compat/map-view-mount.d.ts +1 -0
- package/esri-compat/map-view-mount.d.ts.map +1 -1
- package/esri-compat/map-view-mount.js +46 -25
- package/esri-compat/map-view.d.ts +13 -0
- package/esri-compat/map-view.d.ts.map +1 -1
- package/esri-compat/map-view.js +94 -8
- package/esri-compat/map.d.ts +5 -1
- package/esri-compat/map.d.ts.map +1 -1
- package/esri-compat/map.js +10 -0
- package/esri-compat/measurement.d.ts +2 -0
- package/esri-compat/measurement.d.ts.map +1 -1
- package/esri-compat/measurement.js +14 -0
- package/esri-compat/point.d.ts +7 -0
- package/esri-compat/point.d.ts.map +1 -1
- package/esri-compat/point.js +33 -3
- package/esri-compat/popup.d.ts +2 -0
- package/esri-compat/popup.d.ts.map +1 -1
- package/esri-compat/popup.js +12 -0
- package/esri-compat/portal.d.ts +1 -1
- package/esri-compat/portal.js +1 -1
- package/esri-compat/print.d.ts +3 -0
- package/esri-compat/print.d.ts.map +1 -1
- package/esri-compat/print.js +15 -0
- package/esri-compat/search.d.ts +4 -0
- package/esri-compat/search.d.ts.map +1 -1
- package/esri-compat/search.js +25 -28
- package/esri-compat/sketch.d.ts +2 -0
- package/esri-compat/sketch.d.ts.map +1 -1
- package/esri-compat/sketch.js +12 -0
- package/esri-compat/widget-host.d.ts +46 -17
- package/esri-compat/widget-host.d.ts.map +1 -1
- package/esri-compat/widget-host.js +84 -20
- package/esri-compat/widget-shell-hosts.d.ts +61 -0
- package/esri-compat/widget-shell-hosts.d.ts.map +1 -0
- package/esri-compat/widget-shell-hosts.js +76 -0
- package/kernel/renderer.d.ts +82 -0
- package/kernel/renderer.js +1 -0
- package/offline/conflict-resolution.d.ts +183 -0
- package/offline/conflict-resolution.d.ts.map +1 -0
- package/offline/conflict-resolution.js +286 -0
- package/offline/credential-screen.d.ts +34 -0
- package/offline/credential-screen.d.ts.map +1 -0
- package/offline/credential-screen.js +78 -0
- package/offline/digest.d.ts +15 -0
- package/offline/digest.d.ts.map +1 -0
- package/offline/digest.js +58 -0
- package/offline/edit-queue.d.ts +403 -0
- package/offline/edit-queue.d.ts.map +1 -0
- package/offline/edit-queue.js +1807 -0
- package/offline/edit-replay.d.ts +116 -0
- package/offline/edit-replay.d.ts.map +1 -0
- package/offline/edit-replay.js +392 -0
- package/offline/fetch-handler.d.ts +12 -0
- package/offline/fetch-handler.d.ts.map +1 -0
- package/offline/fetch-handler.js +65 -0
- package/offline/index.d.ts +52 -0
- package/offline/index.d.ts.map +1 -0
- package/offline/index.js +34 -0
- package/offline/indexeddb.d.ts +135 -0
- package/offline/indexeddb.d.ts.map +1 -0
- package/offline/indexeddb.js +888 -0
- package/offline/memory-store.d.ts +17 -0
- package/offline/memory-store.d.ts.map +1 -0
- package/offline/memory-store.js +183 -0
- package/offline/quota.d.ts +161 -0
- package/offline/quota.d.ts.map +1 -0
- package/offline/quota.js +370 -0
- package/offline/read-gate.d.ts +175 -0
- package/offline/read-gate.d.ts.map +1 -0
- package/offline/read-gate.js +252 -0
- package/offline/read-resource.d.ts +99 -0
- package/offline/read-resource.d.ts.map +1 -0
- package/offline/read-resource.js +156 -0
- package/offline/read.d.ts +50 -0
- package/offline/read.d.ts.map +1 -0
- package/offline/read.js +65 -0
- package/offline/region.d.ts +29 -0
- package/offline/region.d.ts.map +1 -0
- package/offline/region.js +1007 -0
- package/offline/replay-conflict.d.ts +183 -0
- package/offline/replay-conflict.d.ts.map +1 -0
- package/offline/replay-conflict.js +482 -0
- package/offline/selection.d.ts +140 -0
- package/offline/selection.d.ts.map +1 -0
- package/offline/selection.js +391 -0
- package/offline/snapshot.d.ts +132 -0
- package/offline/snapshot.d.ts.map +1 -0
- package/offline/snapshot.js +312 -0
- package/offline/status.d.ts +136 -0
- package/offline/status.d.ts.map +1 -0
- package/offline/status.js +479 -0
- package/offline/store-conformance.d.ts +48 -0
- package/offline/store-conformance.d.ts.map +1 -0
- package/offline/store-conformance.js +233 -0
- package/offline/tile-protocol.d.ts +81 -0
- package/offline/tile-protocol.d.ts.map +1 -0
- package/offline/tile-protocol.js +119 -0
- package/offline/types.d.ts +349 -0
- package/offline/types.d.ts.map +1 -0
- package/offline/types.js +75 -0
- package/package.json +14 -2
- package/realtime/odata-delta.d.ts +1 -1
- package/realtime/odata-delta.d.ts.map +1 -1
- package/realtime/odata-delta.js +38 -10
- package/scene-workspace/index.d.ts +2 -2
- package/scene-workspace/index.d.ts.map +1 -1
- package/scene-workspace/index.js +1 -1
- package/scene-workspace/scene-discovery.d.ts +111 -91
- package/scene-workspace/scene-discovery.d.ts.map +1 -1
- package/scene-workspace/scene-discovery.js +340 -92
- package/scene-workspace/scene-view.d.ts +6 -6
- package/scene-workspace/scene-view.d.ts.map +1 -1
- package/scene-workspace/scene-view.js +32 -6
- package/web-components/elements.d.ts +34 -0
- package/web-components/elements.d.ts.map +1 -1
- package/web-components/elements.js +82 -8
- package/web-components/index.d.ts +2 -0
- package/web-components/index.d.ts.map +1 -1
- package/web-components/index.js +3 -1
- package/web-components/map-chrome.d.ts +103 -0
- package/web-components/map-chrome.d.ts.map +1 -0
- package/web-components/map-chrome.js +325 -0
- package/web-components/widget-shell.d.ts +68 -0
- package/web-components/widget-shell.d.ts.map +1 -0
- package/web-components/widget-shell.js +224 -0
- package/widget-capabilities.d.ts +67 -0
- package/widget-capabilities.js +152 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { GeoArrowConversionLimits, GeoArrowPosition } from "./geoarrow-types.js";
|
|
2
|
+
import type { ColumnarBatchIdentityV1 } from "./types.js";
|
|
3
|
+
import type { ColumnarWorkerOperation } from "./worker.js";
|
|
4
|
+
export interface CreateGeoArrowReprojectOperationOptions {
|
|
5
|
+
/** The CRS written to the output GeoArrow geometry metadata. */
|
|
6
|
+
readonly targetCrs: string | Readonly<Record<string, unknown>>;
|
|
7
|
+
/** The caller supplies the complete identity for the reprojected result. */
|
|
8
|
+
readonly identity: ColumnarBatchIdentityV1;
|
|
9
|
+
/** The schema identity must change when the CRS contract changes. */
|
|
10
|
+
readonly schemaId: string;
|
|
11
|
+
/** A host-owned CRS transform; it must return the same coordinate dimensionality. */
|
|
12
|
+
readonly project: (position: GeoArrowPosition) => GeoArrowPosition;
|
|
13
|
+
/** Additional conversion ceilings applied to both decode and encode. */
|
|
14
|
+
readonly limits?: GeoArrowConversionLimits;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Create a bounded worker operation for CRS reprojection of a GeoArrow batch.
|
|
18
|
+
* The SDK traverses and validates the normative geometry; CRS math remains
|
|
19
|
+
* host-owned so this path has no projection-library or network dependency.
|
|
20
|
+
*/
|
|
21
|
+
export declare function createGeoArrowReprojectOperation(options: CreateGeoArrowReprojectOperationOptions): ColumnarWorkerOperation;
|
|
22
|
+
//# sourceMappingURL=geoarrow-reproject.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geoarrow-reproject.d.ts","sourceRoot":"","sources":["../../../src/columnar/geoarrow-reproject.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAEV,wBAAwB,EAKxB,gBAAgB,EACjB,MAAM,qBAAqB,CAAC;AAE7B,OAAO,KAAK,EAAE,uBAAuB,EAAmB,MAAM,YAAY,CAAC;AAC3E,OAAO,KAAK,EAAE,uBAAuB,EAAkC,MAAM,aAAa,CAAC;AAE3F,MAAM,WAAW,uCAAuC;IACtD,gEAAgE;IAChE,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAC/D,4EAA4E;IAC5E,QAAQ,CAAC,QAAQ,EAAE,uBAAuB,CAAC;IAC3C,qEAAqE;IACrE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,qFAAqF;IACrF,QAAQ,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,gBAAgB,KAAK,gBAAgB,CAAC;IACnE,wEAAwE;IACxE,QAAQ,CAAC,MAAM,CAAC,EAAE,wBAAwB,CAAC;CAC5C;AAiCD;;;;GAIG;AACH,wBAAgB,gCAAgC,CAC9C,OAAO,EAAE,uCAAuC,GAC/C,uBAAuB,CA4FzB"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createGeoArrowBatch, decodeGeoArrowBatch, inspectGeoArrowBatch } from "./geoarrow.js";
|
|
2
|
+
function reprojectPosition(position, project) {
|
|
3
|
+
// GeoArrow represents an empty Point as an all-NaN position. It has no
|
|
4
|
+
// coordinates to transform, and passing it to a host projection would either
|
|
5
|
+
// reject it or turn the sentinel into a non-empty geometry.
|
|
6
|
+
if (position.every(Number.isNaN))
|
|
7
|
+
return position;
|
|
8
|
+
const result = project(position);
|
|
9
|
+
if (!Array.isArray(result) || result.length !== position.length) {
|
|
10
|
+
throw new TypeError("GeoArrow reprojection must preserve coordinate dimensionality.");
|
|
11
|
+
}
|
|
12
|
+
if (result.some((coordinate) => typeof coordinate !== "number" || !Number.isFinite(coordinate))) {
|
|
13
|
+
throw new TypeError("GeoArrow reprojection must return finite numeric coordinates.");
|
|
14
|
+
}
|
|
15
|
+
return result;
|
|
16
|
+
}
|
|
17
|
+
function reprojectGeometry(geometry, kind, project) {
|
|
18
|
+
if (geometry === null)
|
|
19
|
+
return null;
|
|
20
|
+
if (kind === "point")
|
|
21
|
+
return reprojectPosition(geometry, project);
|
|
22
|
+
if (kind === "linestring") {
|
|
23
|
+
return geometry.map((position) => reprojectPosition(position, project));
|
|
24
|
+
}
|
|
25
|
+
return geometry.map((ring) => ring.map((position) => reprojectPosition(position, project)));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Create a bounded worker operation for CRS reprojection of a GeoArrow batch.
|
|
29
|
+
* The SDK traverses and validates the normative geometry; CRS math remains
|
|
30
|
+
* host-owned so this path has no projection-library or network dependency.
|
|
31
|
+
*/
|
|
32
|
+
export function createGeoArrowReprojectOperation(options) {
|
|
33
|
+
if (typeof options.project !== "function")
|
|
34
|
+
throw new TypeError("project must be a function.");
|
|
35
|
+
const limits = options.limits ?? {};
|
|
36
|
+
return (batch, context) => {
|
|
37
|
+
context.signal.throwIfAborted();
|
|
38
|
+
context.reportProgress(0, "decode");
|
|
39
|
+
const inspection = inspectGeoArrowBatch(batch, limits);
|
|
40
|
+
const decoded = decodeGeoArrowBatch(batch, limits);
|
|
41
|
+
context.signal.throwIfAborted();
|
|
42
|
+
context.reportProgress(0.5, "reproject");
|
|
43
|
+
const values = decoded.rows.map((row) => reprojectGeometry(row.geometry, inspection.geometry.kind, options.project));
|
|
44
|
+
const geometry = inspection.geometry.kind === "point"
|
|
45
|
+
? {
|
|
46
|
+
kind: "point",
|
|
47
|
+
field: inspection.geometry.field,
|
|
48
|
+
dimensions: inspection.geometry.dimensions,
|
|
49
|
+
coordinateLayout: inspection.geometry.coordinateLayout,
|
|
50
|
+
crs: options.targetCrs,
|
|
51
|
+
edges: inspection.geometry.edges,
|
|
52
|
+
values: values,
|
|
53
|
+
}
|
|
54
|
+
: inspection.geometry.kind === "linestring"
|
|
55
|
+
? {
|
|
56
|
+
kind: "linestring",
|
|
57
|
+
field: inspection.geometry.field,
|
|
58
|
+
dimensions: inspection.geometry.dimensions,
|
|
59
|
+
coordinateLayout: inspection.geometry.coordinateLayout,
|
|
60
|
+
crs: options.targetCrs,
|
|
61
|
+
edges: inspection.geometry.edges,
|
|
62
|
+
values: values,
|
|
63
|
+
}
|
|
64
|
+
: {
|
|
65
|
+
kind: "polygon",
|
|
66
|
+
field: inspection.geometry.field,
|
|
67
|
+
dimensions: inspection.geometry.dimensions,
|
|
68
|
+
coordinateLayout: inspection.geometry.coordinateLayout,
|
|
69
|
+
crs: options.targetCrs,
|
|
70
|
+
edges: inspection.geometry.edges,
|
|
71
|
+
values: values,
|
|
72
|
+
};
|
|
73
|
+
const input = {
|
|
74
|
+
id: batch.id,
|
|
75
|
+
sequence: batch.sequence,
|
|
76
|
+
...(batch.rowOffset === undefined ? {} : { rowOffset: batch.rowOffset }),
|
|
77
|
+
schemaId: options.schemaId,
|
|
78
|
+
identity: options.identity,
|
|
79
|
+
geometry,
|
|
80
|
+
...(inspection.temporal
|
|
81
|
+
? {
|
|
82
|
+
temporal: {
|
|
83
|
+
field: inspection.temporal.field,
|
|
84
|
+
unit: inspection.temporal.unit,
|
|
85
|
+
...(inspection.temporal.timezone === undefined ? {} : { timezone: inspection.temporal.timezone }),
|
|
86
|
+
values: decoded.rows.map((row) => row.timestamp ?? null),
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
: {}),
|
|
90
|
+
...(inspection.dictionary
|
|
91
|
+
? {
|
|
92
|
+
dictionary: {
|
|
93
|
+
field: inspection.dictionary.field,
|
|
94
|
+
ordered: inspection.dictionary.ordered,
|
|
95
|
+
values: decoded.rows.map((row) => row.dictionaryValue ?? null),
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
: {}),
|
|
99
|
+
...(inspection.featureIds
|
|
100
|
+
? {
|
|
101
|
+
featureIds: {
|
|
102
|
+
field: inspection.featureIds.field,
|
|
103
|
+
values: decoded.rows.map((row) => row.featureId),
|
|
104
|
+
},
|
|
105
|
+
}
|
|
106
|
+
: {}),
|
|
107
|
+
};
|
|
108
|
+
const result = createGeoArrowBatch(input, limits);
|
|
109
|
+
const output = inspection.dictionary?.ordered === true
|
|
110
|
+
? {
|
|
111
|
+
...result.batch,
|
|
112
|
+
buffers: result.batch.buffers.map((buffer) => batch.buffers.find((original) => original.id === buffer.id) ?? buffer),
|
|
113
|
+
}
|
|
114
|
+
: result.batch;
|
|
115
|
+
context.signal.throwIfAborted();
|
|
116
|
+
context.reportProgress(1, "complete");
|
|
117
|
+
return output;
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded, versioned persistence envelope for the normative GeoArrow mapping,
|
|
3
|
+
* plus the migration ladder that keeps a persisted cache readable across an
|
|
4
|
+
* envelope layout change (issue #940).
|
|
5
|
+
*
|
|
6
|
+
* Epic #394 requires that "persistent caches require versioned serialization
|
|
7
|
+
* and migration". Serialization landed first and pinned a single envelope
|
|
8
|
+
* version, so the moment the layout changed every persisted entry became
|
|
9
|
+
* permanently unreadable. The ladder here closes that: a stored envelope is
|
|
10
|
+
* migrated forward through an ordered registry of `fromVersion` → `toVersion`
|
|
11
|
+
* steps before it is validated, and the applied chain is reported in the
|
|
12
|
+
* deserialization metrics.
|
|
13
|
+
*
|
|
14
|
+
* Two properties are deliberate.
|
|
15
|
+
*
|
|
16
|
+
* 1. **Fail closed, before any payload is decoded.** Version resolution runs on
|
|
17
|
+
* the parsed envelope header. An unknown, future, or unreachable version
|
|
18
|
+
* throws `unsupported-serialization` with a stable message and no backing
|
|
19
|
+
* buffer is ever base64-decoded.
|
|
20
|
+
* 2. **A migration cannot widen a caller's bounds.** A step receives only the
|
|
21
|
+
* parsed envelope — never the caller's {@link GeoArrowSerializationOptions} —
|
|
22
|
+
* and every ceiling is enforced against the migrated envelope, so migrating
|
|
23
|
+
* an old entry can never admit more bytes than a fresh read would.
|
|
24
|
+
*/
|
|
25
|
+
import type { GeoArrowBatchSerializationResult, GeoArrowEnvelopeMigrationPlanV1, GeoArrowEnvelopeMigrationV1, GeoArrowSerializationOptions } from "./geoarrow-types.js";
|
|
26
|
+
import type { ColumnarBatchV1 } from "./types.js";
|
|
27
|
+
/** Stable discriminator for a persisted GeoArrow batch envelope. */
|
|
28
|
+
export declare const HONUA_GEOARROW_ENVELOPE_KIND: "honua.geoarrow.batch";
|
|
29
|
+
/**
|
|
30
|
+
* Envelope layout version written today.
|
|
31
|
+
*
|
|
32
|
+
* `1.1` adds the decoded `byteLength` of every backing and stamps the GeoArrow
|
|
33
|
+
* layout version, so backing ceilings are enforced before base64 is decoded
|
|
34
|
+
* rather than after a caller-sized allocation already happened.
|
|
35
|
+
*/
|
|
36
|
+
export declare const HONUA_GEOARROW_ENVELOPE_VERSION: "1.1";
|
|
37
|
+
/**
|
|
38
|
+
* Ordered migration ladder applied on read, oldest step first.
|
|
39
|
+
*
|
|
40
|
+
* Every step is a pure structural rewrite of one parsed envelope. `1.0 → 1.1`
|
|
41
|
+
* derives each backing's decoded length from its base64 length — arithmetic on
|
|
42
|
+
* the encoded string, never a decode — and stamps the layout version that
|
|
43
|
+
* envelope 1.0 left implicit.
|
|
44
|
+
*/
|
|
45
|
+
export declare const GEOARROW_ENVELOPE_MIGRATIONS: readonly GeoArrowEnvelopeMigrationV1[];
|
|
46
|
+
/**
|
|
47
|
+
* Resolve the ordered steps that carry `version` forward to `targetVersion`.
|
|
48
|
+
*
|
|
49
|
+
* Exported because a host that persists envelopes needs to answer "can this
|
|
50
|
+
* entry still be read?" without decoding it, and because the unreachable case —
|
|
51
|
+
* a ladder whose chain dead-ends before the target — has to be provable rather
|
|
52
|
+
* than assumed. The walk is bounded and cycle-safe: at most
|
|
53
|
+
* {@link MAX_MIGRATION_STEPS} steps, and no version is visited twice.
|
|
54
|
+
*/
|
|
55
|
+
export declare function planGeoArrowEnvelopeMigration(version: unknown, migrations?: readonly GeoArrowEnvelopeMigrationV1[], targetVersion?: string): GeoArrowEnvelopeMigrationPlanV1;
|
|
56
|
+
/** Serialize one validated GeoArrow batch into a bounded, versioned persistence envelope. */
|
|
57
|
+
export declare function serializeGeoArrowBatch(batch: ColumnarBatchV1, options?: GeoArrowSerializationOptions): Uint8Array;
|
|
58
|
+
/**
|
|
59
|
+
* Report which envelope versions this build can still read.
|
|
60
|
+
*
|
|
61
|
+
* A store uses it to decide whether a persisted entry is worth keeping without
|
|
62
|
+
* decoding it, and a diagnostic uses it to explain why an entry was discarded.
|
|
63
|
+
*/
|
|
64
|
+
export declare function readableGeoArrowEnvelopeVersions(migrations?: readonly GeoArrowEnvelopeMigrationV1[]): readonly string[];
|
|
65
|
+
/** Deserialize and revalidate a persisted GeoArrow envelope with explicit size ceilings. */
|
|
66
|
+
export declare function deserializeGeoArrowBatch(input: Uint8Array | ArrayBuffer, options?: GeoArrowSerializationOptions): GeoArrowBatchSerializationResult;
|
|
67
|
+
export type { GeoArrowBatchSerializationResult, GeoArrowSerializationMetrics } from "./geoarrow-types.js";
|
|
68
|
+
//# sourceMappingURL=geoarrow-serialization.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"geoarrow-serialization.d.ts","sourceRoot":"","sources":["../../../src/columnar/geoarrow-serialization.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,OAAO,KAAK,EACV,gCAAgC,EAChC,+BAA+B,EAC/B,2BAA2B,EAE3B,4BAA4B,EAC7B,MAAM,qBAAqB,CAAC;AAG7B,OAAO,KAAK,EAAE,eAAe,EAAsB,MAAM,YAAY,CAAC;AAEtE,oEAAoE;AACpE,eAAO,MAAM,4BAA4B,EAAG,sBAA+B,CAAC;AAE5E;;;;;;GAMG;AACH,eAAO,MAAM,+BAA+B,EAAG,KAAc,CAAC;AAiC9D;;;;;;;GAOG;AACH,eAAO,MAAM,4BAA4B,EAAE,SAAS,2BAA2B,EAiBnC,CAAC;AAmD7C;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,OAAO,EAChB,UAAU,GAAE,SAAS,2BAA2B,EAAiC,EACjF,aAAa,GAAE,MAA8B,GAC5C,+BAA+B,CAqCjC;AAgHD,6FAA6F;AAC7F,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,eAAe,EAAE,OAAO,GAAE,4BAAiC,GAAG,UAAU,CA6CrH;AAED;;;;;GAKG;AACH,wBAAgB,gCAAgC,CAC9C,UAAU,GAAE,SAAS,2BAA2B,EAAiC,GAChF,SAAS,MAAM,EAAE,CAMnB;AAED,4FAA4F;AAC5F,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,UAAU,GAAG,WAAW,EAC/B,OAAO,GAAE,4BAAiC,GACzC,gCAAgC,CAmDlC;AAED,YAAY,EAAE,gCAAgC,EAAE,4BAA4B,EAAE,MAAM,qBAAqB,CAAC"}
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { HonuaGeoArrowError as GeoArrowError, HONUA_GEOARROW_LAYOUT_VERSION } from "./geoarrow-types.js";
|
|
2
|
+
import { inspectGeoArrowBatch } from "./geoarrow.js";
|
|
3
|
+
/** Stable discriminator for a persisted GeoArrow batch envelope. */
|
|
4
|
+
export const HONUA_GEOARROW_ENVELOPE_KIND = "honua.geoarrow.batch";
|
|
5
|
+
/**
|
|
6
|
+
* Envelope layout version written today.
|
|
7
|
+
*
|
|
8
|
+
* `1.1` adds the decoded `byteLength` of every backing and stamps the GeoArrow
|
|
9
|
+
* layout version, so backing ceilings are enforced before base64 is decoded
|
|
10
|
+
* rather than after a caller-sized allocation already happened.
|
|
11
|
+
*/
|
|
12
|
+
export const HONUA_GEOARROW_ENVELOPE_VERSION = "1.1";
|
|
13
|
+
const SERIALIZATION_KIND = HONUA_GEOARROW_ENVELOPE_KIND;
|
|
14
|
+
const SERIALIZATION_VERSION = HONUA_GEOARROW_ENVELOPE_VERSION;
|
|
15
|
+
const DEFAULT_MAX_SERIALIZED_BYTES = 128 * 1024 * 1024;
|
|
16
|
+
const BASE64_CHUNK = 0x8000;
|
|
17
|
+
/** Bound on ladder walks so a malformed or cyclic registry cannot spin. */
|
|
18
|
+
const MAX_MIGRATION_STEPS = 16;
|
|
19
|
+
/**
|
|
20
|
+
* Ordered migration ladder applied on read, oldest step first.
|
|
21
|
+
*
|
|
22
|
+
* Every step is a pure structural rewrite of one parsed envelope. `1.0 → 1.1`
|
|
23
|
+
* derives each backing's decoded length from its base64 length — arithmetic on
|
|
24
|
+
* the encoded string, never a decode — and stamps the layout version that
|
|
25
|
+
* envelope 1.0 left implicit.
|
|
26
|
+
*/
|
|
27
|
+
export const GEOARROW_ENVELOPE_MIGRATIONS = Object.freeze([
|
|
28
|
+
Object.freeze({
|
|
29
|
+
fromVersion: "1.0",
|
|
30
|
+
toVersion: "1.1",
|
|
31
|
+
migrate: (envelope) => {
|
|
32
|
+
const backings = Array.isArray(envelope.backings) ? envelope.backings : [];
|
|
33
|
+
return {
|
|
34
|
+
...envelope,
|
|
35
|
+
version: "1.1",
|
|
36
|
+
layout: HONUA_GEOARROW_LAYOUT_VERSION,
|
|
37
|
+
backings: backings.map((backing) => {
|
|
38
|
+
if (!isRecord(backing))
|
|
39
|
+
return backing;
|
|
40
|
+
return { ...backing, byteLength: base64DecodedLength(backing.data) };
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
]);
|
|
46
|
+
function fail(message, code) {
|
|
47
|
+
throw new GeoArrowError(code, message);
|
|
48
|
+
}
|
|
49
|
+
function isRecord(value) {
|
|
50
|
+
return typeof value === "object" && value !== null;
|
|
51
|
+
}
|
|
52
|
+
function toBase64(bytes) {
|
|
53
|
+
let binary = "";
|
|
54
|
+
for (let offset = 0; offset < bytes.length; offset += BASE64_CHUNK) {
|
|
55
|
+
binary += String.fromCharCode(...bytes.subarray(offset, Math.min(offset + BASE64_CHUNK, bytes.length)));
|
|
56
|
+
}
|
|
57
|
+
return btoa(binary);
|
|
58
|
+
}
|
|
59
|
+
function fromBase64(value, label) {
|
|
60
|
+
if (typeof value !== "string" || value.length % 4 !== 0 || !/^[A-Za-z0-9+/]*={0,2}$/.test(value)) {
|
|
61
|
+
fail(`${label} must be valid base64.`, "invalid-batch");
|
|
62
|
+
}
|
|
63
|
+
let binary;
|
|
64
|
+
try {
|
|
65
|
+
binary = atob(value);
|
|
66
|
+
}
|
|
67
|
+
catch (cause) {
|
|
68
|
+
throw new GeoArrowError("invalid-batch", `${label} must be valid base64.`, undefined, { cause });
|
|
69
|
+
}
|
|
70
|
+
const result = new Uint8Array(binary.length);
|
|
71
|
+
for (let index = 0; index < binary.length; index += 1)
|
|
72
|
+
result[index] = binary.charCodeAt(index);
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
function serializedLimit(options) {
|
|
76
|
+
const limit = options.maxSerializedBytes ?? DEFAULT_MAX_SERIALIZED_BYTES;
|
|
77
|
+
if (!Number.isSafeInteger(limit) || limit <= 0)
|
|
78
|
+
fail("maxSerializedBytes must be a positive safe integer.", "invalid-batch");
|
|
79
|
+
return limit;
|
|
80
|
+
}
|
|
81
|
+
function encodeEnvelope(envelope, limit) {
|
|
82
|
+
const bytes = new TextEncoder().encode(JSON.stringify(envelope));
|
|
83
|
+
if (bytes.byteLength > limit) {
|
|
84
|
+
fail(`GeoArrow envelope is ${bytes.byteLength} bytes; the limit is ${limit}.`, "serialization-limit-exceeded");
|
|
85
|
+
}
|
|
86
|
+
return bytes;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolve the ordered steps that carry `version` forward to `targetVersion`.
|
|
90
|
+
*
|
|
91
|
+
* Exported because a host that persists envelopes needs to answer "can this
|
|
92
|
+
* entry still be read?" without decoding it, and because the unreachable case —
|
|
93
|
+
* a ladder whose chain dead-ends before the target — has to be provable rather
|
|
94
|
+
* than assumed. The walk is bounded and cycle-safe: at most
|
|
95
|
+
* {@link MAX_MIGRATION_STEPS} steps, and no version is visited twice.
|
|
96
|
+
*/
|
|
97
|
+
export function planGeoArrowEnvelopeMigration(version, migrations = GEOARROW_ENVELOPE_MIGRATIONS, targetVersion = SERIALIZATION_VERSION) {
|
|
98
|
+
if (typeof version !== "string" || !isLayoutVersion(version)) {
|
|
99
|
+
return { applicable: false, reason: "unknown-version", detail: `"${String(version)}" is not a layout version.` };
|
|
100
|
+
}
|
|
101
|
+
if (version === targetVersion)
|
|
102
|
+
return { applicable: true, steps: Object.freeze([]), migrations: Object.freeze([]) };
|
|
103
|
+
const steps = [];
|
|
104
|
+
const applied = [];
|
|
105
|
+
const visited = new Set([version]);
|
|
106
|
+
let cursor = version;
|
|
107
|
+
while (steps.length < MAX_MIGRATION_STEPS) {
|
|
108
|
+
const step = migrations.find((candidate) => candidate.fromVersion === cursor);
|
|
109
|
+
if (!step)
|
|
110
|
+
break;
|
|
111
|
+
if (visited.has(step.toVersion))
|
|
112
|
+
break;
|
|
113
|
+
visited.add(step.toVersion);
|
|
114
|
+
steps.push(`${step.fromVersion}->${step.toVersion}`);
|
|
115
|
+
applied.push(step);
|
|
116
|
+
cursor = step.toVersion;
|
|
117
|
+
if (cursor === targetVersion) {
|
|
118
|
+
return { applicable: true, steps: Object.freeze(steps), migrations: Object.freeze(applied) };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// The walk stalled. A version the ladder has never heard of is unknown (or
|
|
122
|
+
// ahead of this build); one the ladder knows but cannot carry home is an
|
|
123
|
+
// unreachable path, and both refuse rather than guess at a layout.
|
|
124
|
+
const known = migrations.some((step) => step.fromVersion === version || step.toVersion === version);
|
|
125
|
+
if (steps.length > 0 || known) {
|
|
126
|
+
return {
|
|
127
|
+
applicable: false,
|
|
128
|
+
reason: "unreachable-version",
|
|
129
|
+
detail: `No migration path carries envelope version "${version}" to "${targetVersion}".`,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
applicable: false,
|
|
134
|
+
reason: compareLayoutVersions(version, targetVersion) > 0 ? "future-version" : "unknown-version",
|
|
135
|
+
detail: `Envelope version "${version}" has no migration to "${targetVersion}".`,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function readEnvelope(input, limit, migrations) {
|
|
139
|
+
const bytes = input instanceof Uint8Array ? input : new Uint8Array(input);
|
|
140
|
+
if (bytes.byteLength > limit) {
|
|
141
|
+
fail(`GeoArrow envelope is ${bytes.byteLength} bytes; the limit is ${limit}.`, "serialization-limit-exceeded");
|
|
142
|
+
}
|
|
143
|
+
let value;
|
|
144
|
+
try {
|
|
145
|
+
value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
146
|
+
}
|
|
147
|
+
catch (cause) {
|
|
148
|
+
throw new GeoArrowError("invalid-batch", "GeoArrow envelope must be valid UTF-8 JSON.", undefined, { cause });
|
|
149
|
+
}
|
|
150
|
+
if (!isRecord(value) || value.kind !== SERIALIZATION_KIND) {
|
|
151
|
+
fail(`Unsupported GeoArrow serialization kind "${String(isRecord(value) ? value.kind : undefined)}".`, "unsupported-serialization");
|
|
152
|
+
}
|
|
153
|
+
// Version resolution precedes every payload read: an envelope this build
|
|
154
|
+
// cannot carry forward is refused before a single backing is decoded.
|
|
155
|
+
const storedVersion = typeof value.version === "string" ? value.version : String(value.version);
|
|
156
|
+
const plan = planGeoArrowEnvelopeMigration(value.version, migrations);
|
|
157
|
+
if (!plan.applicable) {
|
|
158
|
+
fail(`Unsupported GeoArrow serialization version "${storedVersion}". ${plan.detail}`, "unsupported-serialization");
|
|
159
|
+
}
|
|
160
|
+
let migrated = value;
|
|
161
|
+
for (const step of plan.migrations) {
|
|
162
|
+
// A step sees only the envelope, never the caller's options, so no
|
|
163
|
+
// migration can widen a bound the caller set.
|
|
164
|
+
const next = step.migrate(Object.freeze({ ...migrated }));
|
|
165
|
+
if (!isRecord(next) || next.version !== step.toVersion) {
|
|
166
|
+
fail(`GeoArrow migration ${step.fromVersion}->${step.toVersion} did not produce a "${step.toVersion}" envelope.`, "unsupported-serialization");
|
|
167
|
+
}
|
|
168
|
+
migrated = next;
|
|
169
|
+
}
|
|
170
|
+
if (migrated.version !== SERIALIZATION_VERSION) {
|
|
171
|
+
// Defensive: the plan only resolves when its chain ends at the target and
|
|
172
|
+
// each step's output was verified above, so reaching here means a step
|
|
173
|
+
// rewrote the version underneath the ladder.
|
|
174
|
+
fail(`Unsupported GeoArrow serialization version "${storedVersion}"; migration did not reach "${SERIALIZATION_VERSION}".`, "unsupported-serialization");
|
|
175
|
+
}
|
|
176
|
+
if (migrated.layout !== HONUA_GEOARROW_LAYOUT_VERSION) {
|
|
177
|
+
// Envelope 1.0 carried no layout stamp, so its migration supplies this
|
|
178
|
+
// build's; a stamp that still disagrees came from a foreign layout and the
|
|
179
|
+
// buffers underneath it cannot be interpreted here.
|
|
180
|
+
fail(`Unsupported GeoArrow layout version "${String(migrated.layout)}"; this build reads "${HONUA_GEOARROW_LAYOUT_VERSION}".`, "unsupported-serialization");
|
|
181
|
+
}
|
|
182
|
+
if (!isRecord(migrated.batch) || !Array.isArray(migrated.batch.buffers) || !Array.isArray(migrated.backings)) {
|
|
183
|
+
fail("GeoArrow envelope is missing its batch or backing buffers.", "invalid-batch");
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
envelope: migrated,
|
|
187
|
+
byteLength: bytes.byteLength,
|
|
188
|
+
applied: plan.steps,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
function metrics(serializedBytes, batch, envelopeVersion, migrations) {
|
|
192
|
+
const seen = new Set();
|
|
193
|
+
for (const buffer of batch.buffers)
|
|
194
|
+
seen.add(buffer.data);
|
|
195
|
+
return Object.freeze({
|
|
196
|
+
serializedBytes,
|
|
197
|
+
backingBytes: [...seen].reduce((total, data) => total + data.byteLength, 0),
|
|
198
|
+
backingBuffers: seen.size,
|
|
199
|
+
envelopeVersion,
|
|
200
|
+
migrations: Object.freeze([...migrations]),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
/** Layout versions are `<major>.<minor>` digit pairs; no regex, no backtracking. */
|
|
204
|
+
function isLayoutVersion(value) {
|
|
205
|
+
const parts = value.split(".");
|
|
206
|
+
if (parts.length !== 2)
|
|
207
|
+
return false;
|
|
208
|
+
return parts.every((part) => part.length > 0 && part.length <= 6 && [...part].every((char) => char >= "0" && char <= "9"));
|
|
209
|
+
}
|
|
210
|
+
function compareLayoutVersions(left, right) {
|
|
211
|
+
const [leftMajor = 0, leftMinor = 0] = left.split(".").map(Number);
|
|
212
|
+
const [rightMajor = 0, rightMinor = 0] = right.split(".").map(Number);
|
|
213
|
+
if (leftMajor !== rightMajor)
|
|
214
|
+
return leftMajor < rightMajor ? -1 : 1;
|
|
215
|
+
if (leftMinor !== rightMinor)
|
|
216
|
+
return leftMinor < rightMinor ? -1 : 1;
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
/** Decoded length of a base64 string, computed from its length and padding. */
|
|
220
|
+
function base64DecodedLength(value) {
|
|
221
|
+
if (typeof value !== "string" || value.length % 4 !== 0)
|
|
222
|
+
return -1;
|
|
223
|
+
const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
|
|
224
|
+
return (value.length / 4) * 3 - padding;
|
|
225
|
+
}
|
|
226
|
+
/** Serialize one validated GeoArrow batch into a bounded, versioned persistence envelope. */
|
|
227
|
+
export function serializeGeoArrowBatch(batch, options = {}) {
|
|
228
|
+
const limit = serializedLimit(options);
|
|
229
|
+
const inspection = inspectGeoArrowBatch(batch, options);
|
|
230
|
+
const backings = [];
|
|
231
|
+
const backingViews = [];
|
|
232
|
+
const backingIds = new Map();
|
|
233
|
+
const buffers = [];
|
|
234
|
+
for (const descriptor of inspection.batch.buffers) {
|
|
235
|
+
let backingId = backingIds.get(descriptor.data);
|
|
236
|
+
if (!backingId) {
|
|
237
|
+
backingId = `backing-${backings.length}`;
|
|
238
|
+
backingIds.set(descriptor.data, backingId);
|
|
239
|
+
backings.push({ id: backingId, byteLength: descriptor.data.byteLength, data: "" });
|
|
240
|
+
backingViews.push(new Uint8Array(descriptor.data));
|
|
241
|
+
}
|
|
242
|
+
buffers.push({
|
|
243
|
+
id: descriptor.id,
|
|
244
|
+
role: descriptor.role,
|
|
245
|
+
...(descriptor.field === undefined ? {} : { field: descriptor.field }),
|
|
246
|
+
backingId,
|
|
247
|
+
byteOffset: descriptor.byteOffset,
|
|
248
|
+
byteLength: descriptor.byteLength,
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
const envelope = {
|
|
252
|
+
kind: SERIALIZATION_KIND,
|
|
253
|
+
version: SERIALIZATION_VERSION,
|
|
254
|
+
layout: HONUA_GEOARROW_LAYOUT_VERSION,
|
|
255
|
+
batch: { ...inspection.batch, buffers },
|
|
256
|
+
backings,
|
|
257
|
+
};
|
|
258
|
+
const base64Bytes = backingViews.reduce((total, bytes) => total + 4 * Math.ceil(bytes.byteLength / 3), 0);
|
|
259
|
+
const envelopeOverhead = new TextEncoder().encode(JSON.stringify({ ...envelope, backings: backings.map(({ id, byteLength }) => ({ id, byteLength, data: "" })) })).byteLength;
|
|
260
|
+
if (envelopeOverhead + base64Bytes > limit) {
|
|
261
|
+
fail(`GeoArrow envelope requires at least ${envelopeOverhead + base64Bytes} bytes; the limit is ${limit}.`, "serialization-limit-exceeded");
|
|
262
|
+
}
|
|
263
|
+
backings.forEach((backing, index) => {
|
|
264
|
+
backing.data = toBase64(backingViews[index]);
|
|
265
|
+
});
|
|
266
|
+
return encodeEnvelope(envelope, limit);
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Report which envelope versions this build can still read.
|
|
270
|
+
*
|
|
271
|
+
* A store uses it to decide whether a persisted entry is worth keeping without
|
|
272
|
+
* decoding it, and a diagnostic uses it to explain why an entry was discarded.
|
|
273
|
+
*/
|
|
274
|
+
export function readableGeoArrowEnvelopeVersions(migrations = GEOARROW_ENVELOPE_MIGRATIONS) {
|
|
275
|
+
const versions = new Set([SERIALIZATION_VERSION]);
|
|
276
|
+
for (const step of migrations) {
|
|
277
|
+
if (planGeoArrowEnvelopeMigration(step.fromVersion, migrations).applicable)
|
|
278
|
+
versions.add(step.fromVersion);
|
|
279
|
+
}
|
|
280
|
+
return Object.freeze([...versions].sort());
|
|
281
|
+
}
|
|
282
|
+
/** Deserialize and revalidate a persisted GeoArrow envelope with explicit size ceilings. */
|
|
283
|
+
export function deserializeGeoArrowBatch(input, options = {}) {
|
|
284
|
+
const limit = serializedLimit(options);
|
|
285
|
+
const { envelope, byteLength, applied } = readEnvelope(input, limit, options.migrations ?? GEOARROW_ENVELOPE_MIGRATIONS);
|
|
286
|
+
const backingMap = new Map();
|
|
287
|
+
let backingBytes = 0;
|
|
288
|
+
for (const backing of envelope.backings) {
|
|
289
|
+
if (!isRecord(backing) || typeof backing.id !== "string" || backingMap.has(backing.id)) {
|
|
290
|
+
fail("GeoArrow envelope contains an invalid or duplicate backing id.", "invalid-batch");
|
|
291
|
+
}
|
|
292
|
+
// Declared since envelope 1.1 and derived by the 1.0 migration, so the
|
|
293
|
+
// ceiling is enforced against the migrated envelope before any allocation.
|
|
294
|
+
const declared = backing.byteLength;
|
|
295
|
+
if (!Number.isSafeInteger(declared) || declared < 0) {
|
|
296
|
+
fail(`GeoArrow backing ${backing.id} does not declare a decoded byte length.`, "invalid-batch");
|
|
297
|
+
}
|
|
298
|
+
if (options.maxBackingBytes !== undefined && backingBytes + declared > options.maxBackingBytes) {
|
|
299
|
+
fail(`GeoArrow backings exceed the ${options.maxBackingBytes}-byte limit.`, "serialization-limit-exceeded");
|
|
300
|
+
}
|
|
301
|
+
const bytes = fromBase64(backing.data, `backing ${backing.id}`);
|
|
302
|
+
if (bytes.byteLength !== declared) {
|
|
303
|
+
fail(`GeoArrow backing ${backing.id} decoded to ${bytes.byteLength} bytes, not ${declared}.`, "invalid-batch");
|
|
304
|
+
}
|
|
305
|
+
backingBytes += bytes.byteLength;
|
|
306
|
+
backingMap.set(backing.id, bytes.buffer);
|
|
307
|
+
}
|
|
308
|
+
const serializedBatch = envelope.batch;
|
|
309
|
+
const buffers = serializedBatch.buffers.map((descriptor) => {
|
|
310
|
+
if (!isRecord(descriptor) || typeof descriptor.id !== "string" || typeof descriptor.backingId !== "string") {
|
|
311
|
+
fail("GeoArrow envelope contains an invalid buffer descriptor.", "invalid-batch");
|
|
312
|
+
}
|
|
313
|
+
const data = backingMap.get(descriptor.backingId);
|
|
314
|
+
if (!data)
|
|
315
|
+
fail(`GeoArrow buffer references unknown backing "${descriptor.backingId}".`, "invalid-batch");
|
|
316
|
+
return {
|
|
317
|
+
id: descriptor.id,
|
|
318
|
+
role: descriptor.role,
|
|
319
|
+
...(descriptor.field === undefined ? {} : { field: descriptor.field }),
|
|
320
|
+
data,
|
|
321
|
+
byteOffset: descriptor.byteOffset,
|
|
322
|
+
byteLength: descriptor.byteLength,
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
const batch = { ...serializedBatch, buffers };
|
|
326
|
+
inspectGeoArrowBatch(batch, options);
|
|
327
|
+
return Object.freeze({
|
|
328
|
+
batch,
|
|
329
|
+
metrics: metrics(byteLength, batch, SERIALIZATION_VERSION, applied),
|
|
330
|
+
});
|
|
331
|
+
}
|