@honua/react 0.1.11-beta.0 → 0.1.13
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/connect-url-safety.d.ts +53 -0
- package/connect-url-safety.js +189 -0
- package/contract/pmtiles.d.ts +6 -5
- package/contract/pmtiles.d.ts.map +1 -1
- package/contract/pmtiles.js +30 -5
- package/control-plane/generated/admin-operations.d.ts +3 -3
- package/control-plane/generated/admin-operations.d.ts.map +1 -1
- package/control-plane/generated/admin-operations.js +3 -3
- 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 +10 -6
- 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/geocoding/index.d.ts +114 -0
- package/geocoding/index.d.ts.map +1 -0
- package/geocoding/index.js +293 -0
- package/geocoding/provider-http.d.ts +37 -0
- package/geocoding/provider-http.d.ts.map +1 -0
- package/geocoding/provider-http.js +93 -0
- package/geocoding/provider.d.ts +145 -0
- package/geocoding/provider.d.ts.map +1 -0
- package/geocoding/provider.js +41 -0
- package/geocoding/providers/honua.d.ts +28 -0
- package/geocoding/providers/honua.d.ts.map +1 -0
- package/geocoding/providers/honua.js +68 -0
- package/geocoding/providers/nominatim.d.ts +47 -0
- package/geocoding/providers/nominatim.d.ts.map +1 -0
- package/geocoding/providers/nominatim.js +105 -0
- package/geocoding/providers/pelias.d.ts +40 -0
- package/geocoding/providers/pelias.d.ts.map +1 -0
- package/geocoding/providers/pelias.js +115 -0
- package/geocoding/providers/photon.d.ts +39 -0
- package/geocoding/providers/photon.d.ts.map +1 -0
- package/geocoding/providers/photon.js +107 -0
- package/geometry/convert.d.ts +13 -0
- package/geometry/convert.d.ts.map +1 -0
- package/geometry/convert.js +12 -0
- package/geometry/index.d.ts +17 -0
- package/geometry/index.d.ts.map +1 -0
- package/geometry/index.js +15 -0
- package/geometry/internal.d.ts +25 -0
- package/geometry/internal.d.ts.map +1 -0
- package/geometry/internal.js +40 -0
- package/geometry/ops.d.ts +71 -0
- package/geometry/ops.d.ts.map +1 -0
- package/geometry/ops.js +147 -0
- package/geometry/project.d.ts +40 -0
- package/geometry/project.d.ts.map +1 -0
- package/geometry/project.js +88 -0
- package/geometry/types.d.ts +36 -0
- package/geometry/types.d.ts.map +1 -0
- package/geometry/types.js +12 -0
- package/kernel/renderer.d.ts +82 -0
- package/kernel/renderer.js +1 -0
- package/offline/digest.d.ts +15 -0
- package/offline/digest.js +58 -0
- package/offline/quota.d.ts +161 -0
- package/offline/quota.js +370 -0
- package/offline/types.d.ts +349 -0
- package/offline/types.js +75 -0
- package/package.json +14 -2
- package/widget-capabilities.d.ts +67 -0
- package/widget-capabilities.js +152 -0
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import { beginColumnarSpan } from "./telemetry.js";
|
|
2
|
+
import { COLUMNAR_BATCH_KIND, COLUMNAR_BATCH_VERSION, COLUMNAR_TRANSFER_KIND, DEFAULT_COLUMNAR_BATCH_MAX_BACKING_BYTES, DEFAULT_COLUMNAR_BATCH_MAX_BUFFER_VIEWS, DEFAULT_COLUMNAR_BATCH_MAX_METADATA_ENTRIES, DEFAULT_COLUMNAR_BATCH_MAX_ROWS, DEFAULT_COLUMNAR_BATCH_MAX_SCHEMA_NODES, DEFAULT_COLUMNAR_BATCH_MAX_STRING_BYTES, HonuaColumnarTransferError, } from "./types.js";
|
|
3
|
+
const BUFFER_ROLES = new Set(["validity", "offsets", "type-ids", "values", "dictionary", "geometry", "custom"]);
|
|
4
|
+
const trustedBatches = new WeakSet();
|
|
5
|
+
const trustedBatchUsage = new WeakMap();
|
|
6
|
+
const leasedBuffers = new WeakMap();
|
|
7
|
+
function invalid(message) {
|
|
8
|
+
throw new HonuaColumnarTransferError("invalid-batch", message);
|
|
9
|
+
}
|
|
10
|
+
function invalidBoundary(operation) {
|
|
11
|
+
try {
|
|
12
|
+
return operation();
|
|
13
|
+
}
|
|
14
|
+
catch (cause) {
|
|
15
|
+
if (cause instanceof HonuaColumnarTransferError)
|
|
16
|
+
throw cause;
|
|
17
|
+
throw new HonuaColumnarTransferError("invalid-batch", "Columnar batch validation failed", { cause });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function resolveLimits(limits) {
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
maxRows: normalizeLimit(limits.maxRows, DEFAULT_COLUMNAR_BATCH_MAX_ROWS, "maxRows"),
|
|
23
|
+
maxBackingBytes: normalizeLimit(limits.maxBackingBytes, DEFAULT_COLUMNAR_BATCH_MAX_BACKING_BYTES, "maxBackingBytes"),
|
|
24
|
+
maxSchemaNodes: normalizeLimit(limits.maxSchemaNodes, DEFAULT_COLUMNAR_BATCH_MAX_SCHEMA_NODES, "maxSchemaNodes"),
|
|
25
|
+
maxMetadataEntries: normalizeLimit(limits.maxMetadataEntries, DEFAULT_COLUMNAR_BATCH_MAX_METADATA_ENTRIES, "maxMetadataEntries"),
|
|
26
|
+
maxBufferViews: normalizeLimit(limits.maxBufferViews, DEFAULT_COLUMNAR_BATCH_MAX_BUFFER_VIEWS, "maxBufferViews"),
|
|
27
|
+
maxStringBytes: normalizeLimit(limits.maxStringBytes, DEFAULT_COLUMNAR_BATCH_MAX_STRING_BYTES, "maxStringBytes"),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function createUsage() {
|
|
31
|
+
return { schemaNodes: 0, metadataEntries: 0, bufferViews: 0, stringBytes: 0 };
|
|
32
|
+
}
|
|
33
|
+
function consumeCount(usage, key, count, limit, code, label) {
|
|
34
|
+
if (count > limit - usage[key]) {
|
|
35
|
+
throw new HonuaColumnarTransferError(code, `${label} exceeds the configured ${limit}-entry limit`);
|
|
36
|
+
}
|
|
37
|
+
usage[key] += count;
|
|
38
|
+
}
|
|
39
|
+
function consumeString(value, label, usage, limits) {
|
|
40
|
+
if (typeof value !== "string")
|
|
41
|
+
invalid(`${label} must be a string`);
|
|
42
|
+
const remaining = limits.maxStringBytes - usage.stringBytes;
|
|
43
|
+
let bytes = 0;
|
|
44
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
45
|
+
const code = value.charCodeAt(index);
|
|
46
|
+
if (code <= 0x7f)
|
|
47
|
+
bytes += 1;
|
|
48
|
+
else if (code <= 0x7ff)
|
|
49
|
+
bytes += 2;
|
|
50
|
+
else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
|
|
51
|
+
const next = value.charCodeAt(index + 1);
|
|
52
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
53
|
+
bytes += 4;
|
|
54
|
+
index += 1;
|
|
55
|
+
}
|
|
56
|
+
else
|
|
57
|
+
bytes += 3;
|
|
58
|
+
}
|
|
59
|
+
else
|
|
60
|
+
bytes += 3;
|
|
61
|
+
if (bytes > remaining) {
|
|
62
|
+
throw new HonuaColumnarTransferError("string-limit-exceeded", `${label} exceeds the configured ${limits.maxStringBytes}-byte descriptor string limit`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
usage.stringBytes += bytes;
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function boundedMetadataKeys(value, label, usage, limits) {
|
|
69
|
+
const remaining = limits.maxMetadataEntries - usage.metadataEntries;
|
|
70
|
+
const keys = [];
|
|
71
|
+
for (const key in value) {
|
|
72
|
+
if (!Object.hasOwn(value, key))
|
|
73
|
+
continue;
|
|
74
|
+
if (keys.length >= remaining) {
|
|
75
|
+
throw new HonuaColumnarTransferError("metadata-limit-exceeded", `${label} exceeds the configured ${limits.maxMetadataEntries}-entry limit`);
|
|
76
|
+
}
|
|
77
|
+
keys.push(key);
|
|
78
|
+
}
|
|
79
|
+
consumeCount(usage, "metadataEntries", keys.length, limits.maxMetadataEntries, "metadata-limit-exceeded", label);
|
|
80
|
+
return keys.sort();
|
|
81
|
+
}
|
|
82
|
+
function requireIdentifier(value, label, usage, limits) {
|
|
83
|
+
const normalized = consumeString(value, label, usage, limits);
|
|
84
|
+
if (normalized.trim().length === 0)
|
|
85
|
+
invalid(`${label} must be a non-empty string`);
|
|
86
|
+
if (normalized !== normalized.trim())
|
|
87
|
+
invalid(`${label} must not have leading or trailing whitespace`);
|
|
88
|
+
return normalized;
|
|
89
|
+
}
|
|
90
|
+
function requireSafeNonNegativeInteger(value, label) {
|
|
91
|
+
if (!Number.isSafeInteger(value) || value < 0)
|
|
92
|
+
invalid(`${label} must be a non-negative safe integer`);
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
function normalizeLimit(value, fallback, label) {
|
|
96
|
+
const resolved = value ?? fallback;
|
|
97
|
+
if (!Number.isSafeInteger(resolved) || resolved <= 0) {
|
|
98
|
+
throw new HonuaColumnarTransferError("invalid-batch", `${label} must be a positive safe integer`);
|
|
99
|
+
}
|
|
100
|
+
return resolved;
|
|
101
|
+
}
|
|
102
|
+
function normalizeStringRecord(value, label, usage, limits) {
|
|
103
|
+
if (value === undefined)
|
|
104
|
+
return undefined;
|
|
105
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
106
|
+
invalid(`${label} must be an object`);
|
|
107
|
+
const keys = boundedMetadataKeys(value, label, usage, limits);
|
|
108
|
+
const normalized = Object.create(null);
|
|
109
|
+
for (const key of keys) {
|
|
110
|
+
const normalizedKey = requireIdentifier(key, `${label} key`, usage, limits);
|
|
111
|
+
normalized[normalizedKey] = consumeString(value[key], `${label}.${key}`, usage, limits);
|
|
112
|
+
}
|
|
113
|
+
return Object.freeze(normalized);
|
|
114
|
+
}
|
|
115
|
+
function normalizeParameters(value, label, usage, limits) {
|
|
116
|
+
if (value === undefined)
|
|
117
|
+
return undefined;
|
|
118
|
+
if (typeof value !== "object" || value === null || Array.isArray(value))
|
|
119
|
+
invalid(`${label} must be an object`);
|
|
120
|
+
const keys = boundedMetadataKeys(value, label, usage, limits);
|
|
121
|
+
const normalized = Object.create(null);
|
|
122
|
+
for (const key of keys) {
|
|
123
|
+
const normalizedKey = requireIdentifier(key, `${label} key`, usage, limits);
|
|
124
|
+
const item = value[key];
|
|
125
|
+
if (typeof item !== "string" && typeof item !== "number" && typeof item !== "boolean") {
|
|
126
|
+
invalid(`${label}.${key} must be a string, number, or boolean`);
|
|
127
|
+
}
|
|
128
|
+
if (typeof item === "number" && !Number.isFinite(item))
|
|
129
|
+
invalid(`${label}.${key} must be finite`);
|
|
130
|
+
normalized[normalizedKey] = typeof item === "string" ? consumeString(item, `${label}.${key}`, usage, limits) : item;
|
|
131
|
+
}
|
|
132
|
+
return Object.freeze(normalized);
|
|
133
|
+
}
|
|
134
|
+
function normalizeType(type, label, usage, limits) {
|
|
135
|
+
if (typeof type !== "object" || type === null)
|
|
136
|
+
invalid(`${label} must be an object`);
|
|
137
|
+
const name = type.name;
|
|
138
|
+
const rawParameters = type.parameters;
|
|
139
|
+
const parameters = normalizeParameters(rawParameters, `${label}.parameters`, usage, limits);
|
|
140
|
+
return Object.freeze({
|
|
141
|
+
name: requireIdentifier(name, `${label}.name`, usage, limits),
|
|
142
|
+
...(parameters === undefined ? {} : { parameters }),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function normalizeField(field, path, depth, usage, limits) {
|
|
146
|
+
if (depth > 64)
|
|
147
|
+
invalid(`${path} exceeds the maximum schema nesting depth`);
|
|
148
|
+
if (typeof field !== "object" || field === null)
|
|
149
|
+
invalid(`${path} must be an object`);
|
|
150
|
+
consumeCount(usage, "schemaNodes", 1, limits.maxSchemaNodes, "schema-limit-exceeded", "schema fields");
|
|
151
|
+
const name = field.name;
|
|
152
|
+
const type = field.type;
|
|
153
|
+
const nullable = field.nullable;
|
|
154
|
+
const rawChildren = field.children;
|
|
155
|
+
const rawMetadata = field.metadata;
|
|
156
|
+
if (typeof nullable !== "boolean")
|
|
157
|
+
invalid(`${path}.nullable must be a boolean`);
|
|
158
|
+
if (rawChildren !== undefined && !Array.isArray(rawChildren))
|
|
159
|
+
invalid(`${path}.children must be an array`);
|
|
160
|
+
const childCount = rawChildren?.length;
|
|
161
|
+
if (childCount !== undefined && childCount > limits.maxSchemaNodes - usage.schemaNodes) {
|
|
162
|
+
throw new HonuaColumnarTransferError("schema-limit-exceeded", `schema fields exceeds the configured ${limits.maxSchemaNodes}-entry limit`);
|
|
163
|
+
}
|
|
164
|
+
let children;
|
|
165
|
+
if (rawChildren && childCount !== undefined) {
|
|
166
|
+
children = [];
|
|
167
|
+
for (let index = 0; index < childCount; index += 1) {
|
|
168
|
+
children.push(normalizeField(rawChildren[index], `${path}.children[${index}]`, depth + 1, usage, limits));
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (children) {
|
|
172
|
+
const names = new Set();
|
|
173
|
+
for (const child of children) {
|
|
174
|
+
if (names.has(child.name))
|
|
175
|
+
invalid(`${path} has duplicate child field ${child.name}`);
|
|
176
|
+
names.add(child.name);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
const metadata = normalizeStringRecord(rawMetadata, `${path}.metadata`, usage, limits);
|
|
180
|
+
return Object.freeze({
|
|
181
|
+
name: requireIdentifier(name, `${path}.name`, usage, limits),
|
|
182
|
+
type: normalizeType(type, `${path}.type`, usage, limits),
|
|
183
|
+
nullable,
|
|
184
|
+
...(children === undefined ? {} : { children: Object.freeze(children) }),
|
|
185
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
function normalizeSchema(schema, usage, limits) {
|
|
189
|
+
if (typeof schema !== "object" || schema === null)
|
|
190
|
+
invalid("schema must be an object");
|
|
191
|
+
const id = schema.id;
|
|
192
|
+
const rawFields = schema.fields;
|
|
193
|
+
const rawMetadata = schema.metadata;
|
|
194
|
+
if (!Array.isArray(rawFields))
|
|
195
|
+
invalid("schema.fields must be an array");
|
|
196
|
+
const fieldCount = rawFields.length;
|
|
197
|
+
if (fieldCount > limits.maxSchemaNodes - usage.schemaNodes) {
|
|
198
|
+
throw new HonuaColumnarTransferError("schema-limit-exceeded", `schema fields exceeds the configured ${limits.maxSchemaNodes}-entry limit`);
|
|
199
|
+
}
|
|
200
|
+
const fieldNames = new Set();
|
|
201
|
+
const fields = [];
|
|
202
|
+
for (let index = 0; index < fieldCount; index += 1) {
|
|
203
|
+
const normalized = normalizeField(rawFields[index], `schema.fields[${index}]`, 0, usage, limits);
|
|
204
|
+
if (fieldNames.has(normalized.name))
|
|
205
|
+
invalid(`schema has duplicate top-level field ${normalized.name}`);
|
|
206
|
+
fieldNames.add(normalized.name);
|
|
207
|
+
fields.push(normalized);
|
|
208
|
+
}
|
|
209
|
+
const metadata = normalizeStringRecord(rawMetadata, "schema.metadata", usage, limits);
|
|
210
|
+
return Object.freeze({
|
|
211
|
+
id: requireIdentifier(id, "schema.id", usage, limits),
|
|
212
|
+
fields: Object.freeze(fields),
|
|
213
|
+
...(metadata === undefined ? {} : { metadata }),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
function schemaFieldPaths(schema) {
|
|
217
|
+
const paths = new Set();
|
|
218
|
+
const visit = (fields, parent) => {
|
|
219
|
+
for (const field of fields) {
|
|
220
|
+
const path = parent ? `${parent}.${field.name}` : field.name;
|
|
221
|
+
paths.add(path);
|
|
222
|
+
if (field.children)
|
|
223
|
+
visit(field.children, path);
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
visit(schema.fields);
|
|
227
|
+
return paths;
|
|
228
|
+
}
|
|
229
|
+
function requireRfc3339(value, label, usage, limits) {
|
|
230
|
+
const normalized = consumeString(value, label, usage, limits);
|
|
231
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:Z|([+-])(\d{2}):(\d{2}))$/.exec(normalized);
|
|
232
|
+
if (!match || !Number.isFinite(Date.parse(normalized))) {
|
|
233
|
+
invalid(`${label} must be an RFC 3339 instant`);
|
|
234
|
+
}
|
|
235
|
+
// Date.parse silently normalizes out-of-range calendar values (e.g. 2026-02-31
|
|
236
|
+
// -> 2026-03-03, 24:00:00 -> next day), so validate the components explicitly.
|
|
237
|
+
const [, y, mo, d, h, mi, s, , offH, offM] = match;
|
|
238
|
+
const year = Number(y);
|
|
239
|
+
const month = Number(mo);
|
|
240
|
+
const day = Number(d);
|
|
241
|
+
const daysInMonth = [31, isLeapYear(year) ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
|
242
|
+
if (month < 1 ||
|
|
243
|
+
month > 12 ||
|
|
244
|
+
day < 1 ||
|
|
245
|
+
day > daysInMonth[month - 1] ||
|
|
246
|
+
Number(h) > 23 ||
|
|
247
|
+
Number(mi) > 59 ||
|
|
248
|
+
Number(s) > 59 ||
|
|
249
|
+
(offH !== undefined && (Number(offH) > 23 || Number(offM) > 59))) {
|
|
250
|
+
invalid(`${label} must be an RFC 3339 instant`);
|
|
251
|
+
}
|
|
252
|
+
return normalized;
|
|
253
|
+
}
|
|
254
|
+
function isLeapYear(year) {
|
|
255
|
+
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
256
|
+
}
|
|
257
|
+
function normalizeBatchIdentity(identity, schema, usage, limits) {
|
|
258
|
+
if (identity === undefined)
|
|
259
|
+
return undefined;
|
|
260
|
+
if (typeof identity !== "object" || identity === null)
|
|
261
|
+
invalid("identity must be an object");
|
|
262
|
+
consumeCount(usage, "metadataEntries", 7, limits.maxMetadataEntries, "metadata-limit-exceeded", "identity");
|
|
263
|
+
const sourceId = requireIdentifier(identity.sourceId, "identity.sourceId", usage, limits);
|
|
264
|
+
const sourceVersion = requireIdentifier(identity.sourceVersion, "identity.sourceVersion", usage, limits);
|
|
265
|
+
const schemaVersion = requireIdentifier(identity.schemaVersion, "identity.schemaVersion", usage, limits);
|
|
266
|
+
if (schemaVersion !== schema.id)
|
|
267
|
+
invalid("identity.schemaVersion must equal schema.id");
|
|
268
|
+
const planId = requireIdentifier(identity.planId, "identity.planId", usage, limits);
|
|
269
|
+
const authorizationScope = requireIdentifier(identity.authorizationScope, "identity.authorizationScope", usage, limits);
|
|
270
|
+
const ordering = identity.ordering;
|
|
271
|
+
if (typeof ordering !== "object" || ordering === null)
|
|
272
|
+
invalid("identity.ordering must be an object");
|
|
273
|
+
if (typeof ordering.stable !== "boolean")
|
|
274
|
+
invalid("identity.ordering.stable must be a boolean");
|
|
275
|
+
if (!Array.isArray(ordering.keys))
|
|
276
|
+
invalid("identity.ordering.keys must be an array");
|
|
277
|
+
const keyCount = ordering.keys.length;
|
|
278
|
+
consumeCount(usage, "metadataEntries", keyCount * 3, limits.maxMetadataEntries, "metadata-limit-exceeded", "identity ordering keys");
|
|
279
|
+
if (ordering.stable && keyCount === 0)
|
|
280
|
+
invalid("a stable identity ordering requires at least one key");
|
|
281
|
+
const fieldPaths = schemaFieldPaths(schema);
|
|
282
|
+
const orderingKeys = [];
|
|
283
|
+
for (let index = 0; index < keyCount; index += 1) {
|
|
284
|
+
const key = ordering.keys[index];
|
|
285
|
+
if (typeof key !== "object" || key === null)
|
|
286
|
+
invalid(`identity.ordering.keys[${index}] must be an object`);
|
|
287
|
+
const field = requireIdentifier(key.field, `identity.ordering.keys[${index}].field`, usage, limits);
|
|
288
|
+
if (!fieldPaths.has(field))
|
|
289
|
+
invalid(`identity.ordering.keys[${index}].field does not exist in schema: ${field}`);
|
|
290
|
+
if (key.direction !== "ascending" && key.direction !== "descending") {
|
|
291
|
+
invalid(`identity.ordering.keys[${index}].direction is not supported`);
|
|
292
|
+
}
|
|
293
|
+
if (key.nulls !== "first" && key.nulls !== "last") {
|
|
294
|
+
invalid(`identity.ordering.keys[${index}].nulls is not supported`);
|
|
295
|
+
}
|
|
296
|
+
orderingKeys.push(Object.freeze({ field, direction: key.direction, nulls: key.nulls }));
|
|
297
|
+
}
|
|
298
|
+
const freshness = identity.freshness;
|
|
299
|
+
if (typeof freshness !== "object" || freshness === null)
|
|
300
|
+
invalid("identity.freshness must be an object");
|
|
301
|
+
const observedAt = requireRfc3339(identity.freshness.observedAt, "identity.freshness.observedAt", usage, limits);
|
|
302
|
+
const staleAfter = freshness.staleAfter === undefined
|
|
303
|
+
? undefined
|
|
304
|
+
: requireRfc3339(freshness.staleAfter, "identity.freshness.staleAfter", usage, limits);
|
|
305
|
+
if (staleAfter !== undefined && Date.parse(staleAfter) < Date.parse(observedAt)) {
|
|
306
|
+
invalid("identity.freshness.staleAfter must not precede observedAt");
|
|
307
|
+
}
|
|
308
|
+
const validator = freshness.validator === undefined
|
|
309
|
+
? undefined
|
|
310
|
+
: requireIdentifier(freshness.validator, "identity.freshness.validator", usage, limits);
|
|
311
|
+
const generation = freshness.generation === undefined
|
|
312
|
+
? undefined
|
|
313
|
+
: requireIdentifier(freshness.generation, "identity.freshness.generation", usage, limits);
|
|
314
|
+
return Object.freeze({
|
|
315
|
+
sourceId,
|
|
316
|
+
sourceVersion,
|
|
317
|
+
schemaVersion,
|
|
318
|
+
planId,
|
|
319
|
+
authorizationScope,
|
|
320
|
+
ordering: Object.freeze({ stable: ordering.stable, keys: Object.freeze(orderingKeys) }),
|
|
321
|
+
freshness: Object.freeze({
|
|
322
|
+
observedAt,
|
|
323
|
+
...(staleAfter === undefined ? {} : { staleAfter }),
|
|
324
|
+
...(validator === undefined ? {} : { validator }),
|
|
325
|
+
...(generation === undefined ? {} : { generation }),
|
|
326
|
+
}),
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
function assertBufferFields(buffers, schema) {
|
|
330
|
+
const paths = schemaFieldPaths(schema);
|
|
331
|
+
for (const [index, buffer] of buffers.entries()) {
|
|
332
|
+
if (buffer.field !== undefined && !paths.has(buffer.field)) {
|
|
333
|
+
invalid(`buffers[${index}].field does not exist in schema: ${buffer.field}`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
function assertRowRange(rowOffset, rowCount) {
|
|
338
|
+
if (rowOffset !== undefined && !Number.isSafeInteger(rowOffset + rowCount)) {
|
|
339
|
+
invalid("rowOffset + rowCount exceeds safe integer precision");
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
function normalizeBuffer(buffer, index, usage, limits) {
|
|
343
|
+
const label = `buffers[${index}]`;
|
|
344
|
+
if (typeof buffer !== "object" || buffer === null)
|
|
345
|
+
invalid(`${label} must be an object`);
|
|
346
|
+
const id = buffer.id;
|
|
347
|
+
const role = buffer.role;
|
|
348
|
+
const rawField = buffer.field;
|
|
349
|
+
const data = buffer.data;
|
|
350
|
+
const rawByteOffset = buffer.byteOffset;
|
|
351
|
+
const rawByteLength = buffer.byteLength;
|
|
352
|
+
if (!(data instanceof ArrayBuffer))
|
|
353
|
+
invalid(`${label}.data must be a transferable ArrayBuffer`);
|
|
354
|
+
assertAttached(data, `${label}.data`);
|
|
355
|
+
if (!BUFFER_ROLES.has(role))
|
|
356
|
+
invalid(`${label}.role is not supported`);
|
|
357
|
+
const byteOffset = requireSafeNonNegativeInteger(rawByteOffset, `${label}.byteOffset`);
|
|
358
|
+
const byteLength = requireSafeNonNegativeInteger(rawByteLength, `${label}.byteLength`);
|
|
359
|
+
if (byteOffset + byteLength > data.byteLength)
|
|
360
|
+
invalid(`${label} view exceeds its backing buffer`);
|
|
361
|
+
const field = rawField === undefined ? undefined : requireIdentifier(rawField, `${label}.field`, usage, limits);
|
|
362
|
+
return Object.freeze({
|
|
363
|
+
id: requireIdentifier(id, `${label}.id`, usage, limits),
|
|
364
|
+
role,
|
|
365
|
+
...(field === undefined ? {} : { field }),
|
|
366
|
+
data,
|
|
367
|
+
byteOffset,
|
|
368
|
+
byteLength,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
function assertAttached(buffer, label) {
|
|
372
|
+
try {
|
|
373
|
+
// Constructing a zero-length view does not copy or touch payload bytes. It
|
|
374
|
+
// succeeds for a legitimate empty buffer and throws for a detached one.
|
|
375
|
+
new Uint8Array(buffer, 0, 0);
|
|
376
|
+
}
|
|
377
|
+
catch {
|
|
378
|
+
invalid(`${label} is detached`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function normalizeBatchInput(input, limits) {
|
|
382
|
+
if (typeof input !== "object" || input === null)
|
|
383
|
+
invalid("batch input must be an object");
|
|
384
|
+
const id = input.id;
|
|
385
|
+
const rawSchema = input.schema;
|
|
386
|
+
const rawRowCount = input.rowCount;
|
|
387
|
+
const rawSequence = input.sequence;
|
|
388
|
+
const rawRowOffset = input.rowOffset;
|
|
389
|
+
const rawIdentity = input.identity;
|
|
390
|
+
const rawBuffers = input.buffers;
|
|
391
|
+
if (!Array.isArray(rawBuffers))
|
|
392
|
+
invalid("buffers must be an array");
|
|
393
|
+
const usage = createUsage();
|
|
394
|
+
const bufferCount = rawBuffers.length;
|
|
395
|
+
if (bufferCount > limits.maxBufferViews) {
|
|
396
|
+
throw new HonuaColumnarTransferError("buffer-view-limit-exceeded", `buffers exceeds the configured ${limits.maxBufferViews}-entry limit`);
|
|
397
|
+
}
|
|
398
|
+
consumeCount(usage, "bufferViews", bufferCount, limits.maxBufferViews, "buffer-view-limit-exceeded", "buffers");
|
|
399
|
+
const schema = normalizeSchema(rawSchema, usage, limits);
|
|
400
|
+
const rowCount = requireSafeNonNegativeInteger(rawRowCount, "rowCount");
|
|
401
|
+
const sequence = requireSafeNonNegativeInteger(rawSequence, "sequence");
|
|
402
|
+
const rowOffset = rawRowOffset === undefined ? undefined : requireSafeNonNegativeInteger(rawRowOffset, "rowOffset");
|
|
403
|
+
assertRowRange(rowOffset, rowCount);
|
|
404
|
+
const identity = normalizeBatchIdentity(rawIdentity, schema, usage, limits);
|
|
405
|
+
const ids = new Set();
|
|
406
|
+
const buffers = [];
|
|
407
|
+
for (let index = 0; index < bufferCount; index += 1) {
|
|
408
|
+
const normalized = normalizeBuffer(rawBuffers[index], index, usage, limits);
|
|
409
|
+
if (ids.has(normalized.id))
|
|
410
|
+
invalid(`buffers has duplicate id ${normalized.id}`);
|
|
411
|
+
ids.add(normalized.id);
|
|
412
|
+
buffers.push(normalized);
|
|
413
|
+
}
|
|
414
|
+
assertBufferFields(buffers, schema);
|
|
415
|
+
const batch = Object.freeze({
|
|
416
|
+
kind: COLUMNAR_BATCH_KIND,
|
|
417
|
+
version: COLUMNAR_BATCH_VERSION,
|
|
418
|
+
id: requireIdentifier(id, "id", usage, limits),
|
|
419
|
+
schema,
|
|
420
|
+
rowCount,
|
|
421
|
+
sequence,
|
|
422
|
+
...(rowOffset === undefined ? {} : { rowOffset }),
|
|
423
|
+
...(identity === undefined ? {} : { identity }),
|
|
424
|
+
buffers: Object.freeze(buffers),
|
|
425
|
+
});
|
|
426
|
+
trustedBatches.add(batch);
|
|
427
|
+
trustedBatchUsage.set(batch, Object.freeze({ ...usage }));
|
|
428
|
+
return batch;
|
|
429
|
+
}
|
|
430
|
+
function assertNotAborted(signal) {
|
|
431
|
+
if (signal?.aborted)
|
|
432
|
+
throw new HonuaColumnarTransferError("aborted", "Columnar transfer was aborted");
|
|
433
|
+
}
|
|
434
|
+
function inspectTrusted(batch, limits) {
|
|
435
|
+
const usage = trustedBatchUsage.get(batch);
|
|
436
|
+
if (!usage)
|
|
437
|
+
invalid("trusted batch normalization accounting is unavailable");
|
|
438
|
+
if (usage.schemaNodes > limits.maxSchemaNodes) {
|
|
439
|
+
throw new HonuaColumnarTransferError("schema-limit-exceeded", `Columnar batch has ${usage.schemaNodes} schema fields; the limit is ${limits.maxSchemaNodes}`);
|
|
440
|
+
}
|
|
441
|
+
if (usage.metadataEntries > limits.maxMetadataEntries) {
|
|
442
|
+
throw new HonuaColumnarTransferError("metadata-limit-exceeded", `Columnar batch has ${usage.metadataEntries} metadata entries; the limit is ${limits.maxMetadataEntries}`);
|
|
443
|
+
}
|
|
444
|
+
if (usage.bufferViews > limits.maxBufferViews) {
|
|
445
|
+
throw new HonuaColumnarTransferError("buffer-view-limit-exceeded", `Columnar batch has ${usage.bufferViews} buffer views; the limit is ${limits.maxBufferViews}`);
|
|
446
|
+
}
|
|
447
|
+
if (usage.stringBytes > limits.maxStringBytes) {
|
|
448
|
+
throw new HonuaColumnarTransferError("string-limit-exceeded", `Columnar batch has ${usage.stringBytes} descriptor string bytes; the limit is ${limits.maxStringBytes}`);
|
|
449
|
+
}
|
|
450
|
+
if (batch.rowCount > limits.maxRows) {
|
|
451
|
+
throw new HonuaColumnarTransferError("row-limit-exceeded", `Columnar batch has ${batch.rowCount} rows; the limit is ${limits.maxRows}`);
|
|
452
|
+
}
|
|
453
|
+
const transfer = [];
|
|
454
|
+
const seen = new Set();
|
|
455
|
+
let logicalBytes = 0;
|
|
456
|
+
let backingBytes = 0;
|
|
457
|
+
for (const [index, buffer] of batch.buffers.entries()) {
|
|
458
|
+
assertAttached(buffer.data, `buffers[${index}].data`);
|
|
459
|
+
if (buffer.byteOffset + buffer.byteLength > buffer.data.byteLength) {
|
|
460
|
+
invalid(`buffers[${index}] is detached or exceeds its backing buffer`);
|
|
461
|
+
}
|
|
462
|
+
logicalBytes += buffer.byteLength;
|
|
463
|
+
if (!Number.isSafeInteger(logicalBytes))
|
|
464
|
+
invalid("logical byte length exceeds safe integer precision");
|
|
465
|
+
if (!seen.has(buffer.data)) {
|
|
466
|
+
seen.add(buffer.data);
|
|
467
|
+
transfer.push(buffer.data);
|
|
468
|
+
backingBytes += buffer.data.byteLength;
|
|
469
|
+
if (!Number.isSafeInteger(backingBytes))
|
|
470
|
+
invalid("backing byte length exceeds safe integer precision");
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
if (backingBytes > limits.maxBackingBytes) {
|
|
474
|
+
throw new HonuaColumnarTransferError("memory-limit-exceeded", `Columnar batch owns ${backingBytes} backing bytes; the limit is ${limits.maxBackingBytes}`);
|
|
475
|
+
}
|
|
476
|
+
const metrics = Object.freeze({
|
|
477
|
+
rows: batch.rowCount,
|
|
478
|
+
logicalBytes,
|
|
479
|
+
backingBytes,
|
|
480
|
+
transferBytes: backingBytes,
|
|
481
|
+
copiedBytes: 0,
|
|
482
|
+
bufferViews: batch.buffers.length,
|
|
483
|
+
backingBuffers: transfer.length,
|
|
484
|
+
});
|
|
485
|
+
return { metrics, transfer: Object.freeze(transfer) };
|
|
486
|
+
}
|
|
487
|
+
function inspect(batch, limits = {}) {
|
|
488
|
+
return invalidBoundary(() => {
|
|
489
|
+
const resolvedLimits = resolveLimits(limits);
|
|
490
|
+
if (typeof batch !== "object" || batch === null)
|
|
491
|
+
invalid("batch must be an object");
|
|
492
|
+
const kind = batch.kind;
|
|
493
|
+
const version = batch.version;
|
|
494
|
+
if (kind !== COLUMNAR_BATCH_KIND || version !== COLUMNAR_BATCH_VERSION) {
|
|
495
|
+
invalid(`batch must be ${COLUMNAR_BATCH_KIND}@${COLUMNAR_BATCH_VERSION}`);
|
|
496
|
+
}
|
|
497
|
+
const snapshot = trustedBatches.has(batch) ? batch : normalizeBatchInput(batch, resolvedLimits);
|
|
498
|
+
return inspectTrusted(snapshot, resolvedLimits);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
/**
|
|
502
|
+
* Validate and normalize one batch without copying its payload buffers.
|
|
503
|
+
* Schema metadata is cloned/frozen so later caller mutation cannot change the
|
|
504
|
+
* transport contract; every `data` reference remains exactly caller-owned.
|
|
505
|
+
*/
|
|
506
|
+
export function createColumnarBatch(input, limits = {}) {
|
|
507
|
+
return invalidBoundary(() => {
|
|
508
|
+
const resolvedLimits = resolveLimits(limits);
|
|
509
|
+
const batch = normalizeBatchInput(input, resolvedLimits);
|
|
510
|
+
inspectTrusted(batch, resolvedLimits);
|
|
511
|
+
return batch;
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
/** Inspect an owned batch and return exact backing-allocation/copy accounting. */
|
|
515
|
+
export function inspectColumnarBatch(batch, limits = {}) {
|
|
516
|
+
return inspect(batch, limits).metrics;
|
|
517
|
+
}
|
|
518
|
+
/**
|
|
519
|
+
* Owns the right to transfer a batch once. The SDK first uses structured clone
|
|
520
|
+
* to detach the lease's buffers, then invokes the consumer with the owned clone
|
|
521
|
+
* and its exact deduplicated transfer list. The optional consumer promise is an
|
|
522
|
+
* acknowledgement/backpressure boundary; rejection never restores ownership.
|
|
523
|
+
*/
|
|
524
|
+
export class ColumnarBatchLease {
|
|
525
|
+
#batch;
|
|
526
|
+
#state = "owned";
|
|
527
|
+
#limits;
|
|
528
|
+
#reservedBuffers;
|
|
529
|
+
constructor(batch, limits = {}) {
|
|
530
|
+
const resolvedLimits = resolveLimits(limits);
|
|
531
|
+
let snapshot;
|
|
532
|
+
if (trustedBatches.has(batch))
|
|
533
|
+
snapshot = batch;
|
|
534
|
+
else {
|
|
535
|
+
snapshot = invalidBoundary(() => {
|
|
536
|
+
if (typeof batch !== "object" || batch === null)
|
|
537
|
+
invalid("batch must be an object");
|
|
538
|
+
const kind = batch.kind;
|
|
539
|
+
const version = batch.version;
|
|
540
|
+
if (kind !== COLUMNAR_BATCH_KIND || version !== COLUMNAR_BATCH_VERSION) {
|
|
541
|
+
invalid(`batch must be ${COLUMNAR_BATCH_KIND}@${COLUMNAR_BATCH_VERSION}`);
|
|
542
|
+
}
|
|
543
|
+
return normalizeBatchInput(batch, resolvedLimits);
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
const inspection = inspectTrusted(snapshot, resolvedLimits);
|
|
547
|
+
for (const buffer of inspection.transfer) {
|
|
548
|
+
if (leasedBuffers.has(buffer)) {
|
|
549
|
+
throw new HonuaColumnarTransferError("already-leased", "A backing buffer is already owned by another columnar batch lease");
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
this.#batch = snapshot;
|
|
553
|
+
this.#limits = resolvedLimits;
|
|
554
|
+
this.#reservedBuffers = inspection.transfer;
|
|
555
|
+
for (const buffer of this.#reservedBuffers)
|
|
556
|
+
leasedBuffers.set(buffer, this);
|
|
557
|
+
}
|
|
558
|
+
get state() {
|
|
559
|
+
return this.#state;
|
|
560
|
+
}
|
|
561
|
+
get batch() {
|
|
562
|
+
if (this.#state === "disposed")
|
|
563
|
+
throw new HonuaColumnarTransferError("disposed", "Columnar batch lease is disposed");
|
|
564
|
+
if (this.#state === "transferred" || this.#state === "transferring") {
|
|
565
|
+
throw new HonuaColumnarTransferError("already-transferred", "Columnar batch ownership has been transferred");
|
|
566
|
+
}
|
|
567
|
+
return this.#batch;
|
|
568
|
+
}
|
|
569
|
+
async transfer(target, options = {}) {
|
|
570
|
+
const batch = this.batch;
|
|
571
|
+
// Observation is woven through this method rather than wrapped around it:
|
|
572
|
+
// an extra promise hop would change when the consumer's acknowledgement
|
|
573
|
+
// settles relative to the worker host's own request bookkeeping.
|
|
574
|
+
const telemetry = options.telemetry;
|
|
575
|
+
const span = telemetry
|
|
576
|
+
? beginColumnarSpan(telemetry, "columnar-transfer", batch.identity, { batchId: batch.id })
|
|
577
|
+
: undefined;
|
|
578
|
+
try {
|
|
579
|
+
assertNotAborted(options.signal);
|
|
580
|
+
const effectiveLimits = {
|
|
581
|
+
maxRows: options.maxRows ?? this.#limits.maxRows,
|
|
582
|
+
maxBackingBytes: options.maxBackingBytes ?? this.#limits.maxBackingBytes,
|
|
583
|
+
maxSchemaNodes: options.maxSchemaNodes ?? this.#limits.maxSchemaNodes,
|
|
584
|
+
maxMetadataEntries: options.maxMetadataEntries ?? this.#limits.maxMetadataEntries,
|
|
585
|
+
maxBufferViews: options.maxBufferViews ?? this.#limits.maxBufferViews,
|
|
586
|
+
maxStringBytes: options.maxStringBytes ?? this.#limits.maxStringBytes,
|
|
587
|
+
};
|
|
588
|
+
const { metrics, transfer } = inspect(batch, effectiveLimits);
|
|
589
|
+
const message = Object.freeze({
|
|
590
|
+
kind: COLUMNAR_TRANSFER_KIND,
|
|
591
|
+
version: COLUMNAR_BATCH_VERSION,
|
|
592
|
+
batch,
|
|
593
|
+
metrics,
|
|
594
|
+
});
|
|
595
|
+
this.#state = "transferring";
|
|
596
|
+
let ownedMessage;
|
|
597
|
+
try {
|
|
598
|
+
ownedMessage = structuredClone(message, { transfer: [...transfer] });
|
|
599
|
+
}
|
|
600
|
+
catch (cause) {
|
|
601
|
+
this.#state = "owned";
|
|
602
|
+
throw new HonuaColumnarTransferError("transport-failed", "Columnar ownership transfer failed", {
|
|
603
|
+
cause,
|
|
604
|
+
});
|
|
605
|
+
}
|
|
606
|
+
this.#state = "transferred";
|
|
607
|
+
this.#batch = undefined;
|
|
608
|
+
this.releaseReservations();
|
|
609
|
+
const ownedTransfer = inspect(ownedMessage.batch, effectiveLimits).transfer;
|
|
610
|
+
let acknowledgement;
|
|
611
|
+
try {
|
|
612
|
+
acknowledgement = target(ownedMessage, ownedTransfer);
|
|
613
|
+
}
|
|
614
|
+
catch (cause) {
|
|
615
|
+
throw new HonuaColumnarTransferError("transport-failed", "Columnar consumer rejected the transferred batch", {
|
|
616
|
+
cause,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
try {
|
|
620
|
+
await acknowledgement;
|
|
621
|
+
}
|
|
622
|
+
catch (cause) {
|
|
623
|
+
throw new HonuaColumnarTransferError("transport-failed", "Columnar transfer was handed off but the consumer acknowledgement failed", { cause });
|
|
624
|
+
}
|
|
625
|
+
span?.finish(transferSpanDetail(metrics));
|
|
626
|
+
return Object.freeze({ batchId: batch.id, metrics, acknowledged: true });
|
|
627
|
+
}
|
|
628
|
+
catch (error) {
|
|
629
|
+
span?.fail(error, error instanceof HonuaColumnarTransferError ? { code: error.code } : undefined);
|
|
630
|
+
throw error;
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
/** Release this lease's owned references. Idempotent after disposal. */
|
|
634
|
+
dispose() {
|
|
635
|
+
if (this.#state === "disposed")
|
|
636
|
+
return;
|
|
637
|
+
if (this.#state === "transferring") {
|
|
638
|
+
throw new HonuaColumnarTransferError("already-transferred", "Cannot dispose a columnar batch during transfer");
|
|
639
|
+
}
|
|
640
|
+
this.#batch = undefined;
|
|
641
|
+
this.releaseReservations();
|
|
642
|
+
this.#state = "disposed";
|
|
643
|
+
}
|
|
644
|
+
releaseReservations() {
|
|
645
|
+
for (const buffer of this.#reservedBuffers) {
|
|
646
|
+
if (leasedBuffers.get(buffer) === this)
|
|
647
|
+
leasedBuffers.delete(buffer);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
/** Validate and wrap a batch in a one-owner transfer lease. */
|
|
652
|
+
export function leaseColumnarBatch(batch, limits = {}) {
|
|
653
|
+
return new ColumnarBatchLease(batch, limits);
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* The transfer span reports the accounting the transfer already produced —
|
|
657
|
+
* nothing here measures anything the receipt did not already carry.
|
|
658
|
+
*/
|
|
659
|
+
function transferSpanDetail(metrics) {
|
|
660
|
+
return {
|
|
661
|
+
rows: metrics.rows,
|
|
662
|
+
logicalBytes: metrics.logicalBytes,
|
|
663
|
+
backingBytes: metrics.backingBytes,
|
|
664
|
+
transferBytes: metrics.transferBytes,
|
|
665
|
+
copiedBytes: metrics.copiedBytes,
|
|
666
|
+
bufferViews: metrics.bufferViews,
|
|
667
|
+
backingBuffers: metrics.backingBuffers,
|
|
668
|
+
};
|
|
669
|
+
}
|