@honua/react 0.1.12 → 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.
Files changed (190) hide show
  1. package/columnar/apache-arrow.d.ts +17 -0
  2. package/columnar/apache-arrow.d.ts.map +1 -0
  3. package/columnar/apache-arrow.js +1036 -0
  4. package/columnar/batch-cache-conformance.d.ts +66 -0
  5. package/columnar/batch-cache-conformance.d.ts.map +1 -0
  6. package/columnar/batch-cache-conformance.js +317 -0
  7. package/columnar/batch-cache-indexeddb.d.ts +31 -0
  8. package/columnar/batch-cache-indexeddb.d.ts.map +1 -0
  9. package/columnar/batch-cache-indexeddb.js +148 -0
  10. package/columnar/batch-cache.d.ts +298 -0
  11. package/columnar/batch-cache.d.ts.map +1 -0
  12. package/columnar/batch-cache.js +757 -0
  13. package/columnar/geoarrow-aggregate.d.ts +178 -0
  14. package/columnar/geoarrow-aggregate.d.ts.map +1 -0
  15. package/columnar/geoarrow-aggregate.js +976 -0
  16. package/columnar/geoarrow-patch.d.ts +382 -0
  17. package/columnar/geoarrow-patch.d.ts.map +1 -0
  18. package/columnar/geoarrow-patch.js +1656 -0
  19. package/columnar/geoarrow-reproject.d.ts +22 -0
  20. package/columnar/geoarrow-reproject.d.ts.map +1 -0
  21. package/columnar/geoarrow-reproject.js +119 -0
  22. package/columnar/geoarrow-serialization.d.ts +68 -0
  23. package/columnar/geoarrow-serialization.d.ts.map +1 -0
  24. package/columnar/geoarrow-serialization.js +331 -0
  25. package/columnar/geoarrow-types.d.ts +271 -0
  26. package/columnar/geoarrow-types.d.ts.map +1 -0
  27. package/columnar/geoarrow-types.js +15 -0
  28. package/columnar/geoarrow.d.ts +52 -0
  29. package/columnar/geoarrow.d.ts.map +1 -0
  30. package/columnar/geoarrow.js +1469 -0
  31. package/columnar/index.d.ts +43 -0
  32. package/columnar/index.d.ts.map +1 -0
  33. package/columnar/index.js +29 -0
  34. package/columnar/operators.d.ts +70 -0
  35. package/columnar/operators.d.ts.map +1 -0
  36. package/columnar/operators.js +307 -0
  37. package/columnar/result.d.ts +369 -0
  38. package/columnar/result.d.ts.map +1 -0
  39. package/columnar/result.js +815 -0
  40. package/columnar/telemetry.d.ts +152 -0
  41. package/columnar/telemetry.d.ts.map +1 -0
  42. package/columnar/telemetry.js +265 -0
  43. package/columnar/transfer.d.ts +28 -0
  44. package/columnar/transfer.d.ts.map +1 -0
  45. package/columnar/transfer.js +669 -0
  46. package/columnar/types.d.ts +175 -0
  47. package/columnar/types.d.ts.map +1 -0
  48. package/columnar/types.js +22 -0
  49. package/columnar/worker.d.ts +157 -0
  50. package/columnar/worker.d.ts.map +1 -0
  51. package/columnar/worker.js +1059 -0
  52. package/connect-url-safety.d.ts +53 -0
  53. package/connect-url-safety.js +189 -0
  54. package/contract/pmtiles.d.ts +6 -5
  55. package/contract/pmtiles.d.ts.map +1 -1
  56. package/contract/pmtiles.js +30 -5
  57. package/core/client.d.ts.map +1 -1
  58. package/core/client.js +87 -19
  59. package/core/deprecation-notice.d.ts +28 -0
  60. package/core/deprecation-notice.d.ts.map +1 -0
  61. package/core/deprecation-notice.js +22 -0
  62. package/core/esri-geojson.d.ts.map +1 -1
  63. package/core/esri-geojson.js +10 -6
  64. package/core/optional-grpc-peer.d.ts +15 -0
  65. package/core/optional-grpc-peer.d.ts.map +1 -0
  66. package/core/optional-grpc-peer.js +60 -0
  67. package/esri-compat/basemap-gallery.d.ts +2 -0
  68. package/esri-compat/basemap-gallery.d.ts.map +1 -1
  69. package/esri-compat/basemap-gallery.js +12 -0
  70. package/esri-compat/basemap.d.ts +4 -0
  71. package/esri-compat/basemap.d.ts.map +1 -1
  72. package/esri-compat/basemap.js +70 -0
  73. package/esri-compat/bookmarks.d.ts +2 -0
  74. package/esri-compat/bookmarks.d.ts.map +1 -1
  75. package/esri-compat/bookmarks.js +12 -0
  76. package/esri-compat/controls.d.ts +18 -0
  77. package/esri-compat/controls.d.ts.map +1 -1
  78. package/esri-compat/controls.js +120 -57
  79. package/esri-compat/directions.d.ts +4 -0
  80. package/esri-compat/directions.d.ts.map +1 -1
  81. package/esri-compat/directions.js +30 -20
  82. package/esri-compat/editor.d.ts +4 -0
  83. package/esri-compat/editor.d.ts.map +1 -1
  84. package/esri-compat/editor.js +16 -0
  85. package/esri-compat/esri-config.d.ts +2 -1
  86. package/esri-compat/esri-config.d.ts.map +1 -1
  87. package/esri-compat/feature-effect.d.ts +23 -0
  88. package/esri-compat/feature-effect.d.ts.map +1 -0
  89. package/esri-compat/feature-effect.js +12 -0
  90. package/esri-compat/feature-form.d.ts +6 -0
  91. package/esri-compat/feature-form.d.ts.map +1 -1
  92. package/esri-compat/feature-form.js +63 -0
  93. package/esri-compat/feature-layer.d.ts +21 -2
  94. package/esri-compat/feature-layer.d.ts.map +1 -1
  95. package/esri-compat/feature-layer.js +172 -1
  96. package/esri-compat/feature-table.d.ts +5 -0
  97. package/esri-compat/feature-table.d.ts.map +1 -1
  98. package/esri-compat/feature-table.js +23 -0
  99. package/esri-compat/identity-manager.d.ts +1 -1
  100. package/esri-compat/identity-manager.js +1 -1
  101. package/esri-compat/legend.d.ts +2 -0
  102. package/esri-compat/legend.d.ts.map +1 -1
  103. package/esri-compat/legend.js +98 -0
  104. package/esri-compat/locator.d.ts +1 -1
  105. package/esri-compat/locator.js +1 -1
  106. package/esri-compat/map-view-mount.d.ts +1 -0
  107. package/esri-compat/map-view-mount.d.ts.map +1 -1
  108. package/esri-compat/map-view-mount.js +46 -25
  109. package/esri-compat/map-view.d.ts +13 -0
  110. package/esri-compat/map-view.d.ts.map +1 -1
  111. package/esri-compat/map-view.js +94 -8
  112. package/esri-compat/map.d.ts +5 -1
  113. package/esri-compat/map.d.ts.map +1 -1
  114. package/esri-compat/map.js +10 -0
  115. package/esri-compat/measurement.d.ts +2 -0
  116. package/esri-compat/measurement.d.ts.map +1 -1
  117. package/esri-compat/measurement.js +14 -0
  118. package/esri-compat/point.d.ts +7 -0
  119. package/esri-compat/point.d.ts.map +1 -1
  120. package/esri-compat/point.js +33 -3
  121. package/esri-compat/popup.d.ts +2 -0
  122. package/esri-compat/popup.d.ts.map +1 -1
  123. package/esri-compat/popup.js +12 -0
  124. package/esri-compat/portal.d.ts +1 -1
  125. package/esri-compat/portal.js +1 -1
  126. package/esri-compat/print.d.ts +3 -0
  127. package/esri-compat/print.d.ts.map +1 -1
  128. package/esri-compat/print.js +15 -0
  129. package/esri-compat/search.d.ts +4 -0
  130. package/esri-compat/search.d.ts.map +1 -1
  131. package/esri-compat/search.js +25 -28
  132. package/esri-compat/sketch.d.ts +2 -0
  133. package/esri-compat/sketch.d.ts.map +1 -1
  134. package/esri-compat/sketch.js +12 -0
  135. package/esri-compat/widget-host.d.ts +46 -17
  136. package/esri-compat/widget-host.d.ts.map +1 -1
  137. package/esri-compat/widget-host.js +84 -20
  138. package/esri-compat/widget-shell-hosts.d.ts +61 -0
  139. package/esri-compat/widget-shell-hosts.d.ts.map +1 -0
  140. package/esri-compat/widget-shell-hosts.js +76 -0
  141. package/geocoding/index.d.ts +114 -0
  142. package/geocoding/index.d.ts.map +1 -0
  143. package/geocoding/index.js +293 -0
  144. package/geocoding/provider-http.d.ts +37 -0
  145. package/geocoding/provider-http.d.ts.map +1 -0
  146. package/geocoding/provider-http.js +93 -0
  147. package/geocoding/provider.d.ts +145 -0
  148. package/geocoding/provider.d.ts.map +1 -0
  149. package/geocoding/provider.js +41 -0
  150. package/geocoding/providers/honua.d.ts +28 -0
  151. package/geocoding/providers/honua.d.ts.map +1 -0
  152. package/geocoding/providers/honua.js +68 -0
  153. package/geocoding/providers/nominatim.d.ts +47 -0
  154. package/geocoding/providers/nominatim.d.ts.map +1 -0
  155. package/geocoding/providers/nominatim.js +105 -0
  156. package/geocoding/providers/pelias.d.ts +40 -0
  157. package/geocoding/providers/pelias.d.ts.map +1 -0
  158. package/geocoding/providers/pelias.js +115 -0
  159. package/geocoding/providers/photon.d.ts +39 -0
  160. package/geocoding/providers/photon.d.ts.map +1 -0
  161. package/geocoding/providers/photon.js +107 -0
  162. package/geometry/convert.d.ts +13 -0
  163. package/geometry/convert.d.ts.map +1 -0
  164. package/geometry/convert.js +12 -0
  165. package/geometry/index.d.ts +17 -0
  166. package/geometry/index.d.ts.map +1 -0
  167. package/geometry/index.js +15 -0
  168. package/geometry/internal.d.ts +25 -0
  169. package/geometry/internal.d.ts.map +1 -0
  170. package/geometry/internal.js +40 -0
  171. package/geometry/ops.d.ts +71 -0
  172. package/geometry/ops.d.ts.map +1 -0
  173. package/geometry/ops.js +147 -0
  174. package/geometry/project.d.ts +40 -0
  175. package/geometry/project.d.ts.map +1 -0
  176. package/geometry/project.js +88 -0
  177. package/geometry/types.d.ts +36 -0
  178. package/geometry/types.d.ts.map +1 -0
  179. package/geometry/types.js +12 -0
  180. package/kernel/renderer.d.ts +82 -0
  181. package/kernel/renderer.js +1 -0
  182. package/offline/digest.d.ts +15 -0
  183. package/offline/digest.js +58 -0
  184. package/offline/quota.d.ts +161 -0
  185. package/offline/quota.js +370 -0
  186. package/offline/types.d.ts +349 -0
  187. package/offline/types.js +75 -0
  188. package/package.json +14 -2
  189. package/widget-capabilities.d.ts +67 -0
  190. package/widget-capabilities.js +152 -0
@@ -0,0 +1,1656 @@
1
+ import { createGeoArrowBatch, decodeGeoArrowBatch, inspectGeoArrowBatch } from "./geoarrow.js";
2
+ import { beginColumnarSpan } from "./telemetry.js";
3
+ import { createColumnarBatch } from "./transfer.js";
4
+ /** Honua's realtime columnar patch layout, recorded in schema metadata. */
5
+ export const HONUA_COLUMNAR_PATCH_LAYOUT_VERSION = "1.0";
6
+ /** Stable discriminator for a versioned columnar patch. */
7
+ export const COLUMNAR_PATCH_KIND = "honua.columnar-patch";
8
+ /** Version discriminator for the first columnar patch contract. */
9
+ export const COLUMNAR_PATCH_VERSION = "1.0";
10
+ /** Tombstoned rows as a fraction of `rowCount` before a rebuild is forced. */
11
+ export const DEFAULT_COLUMNAR_PATCH_MAX_TOMBSTONE_RATIO = 0.25;
12
+ /** UTF-8 ceiling for the encoded tombstone overlay before a rebuild is forced. */
13
+ export const DEFAULT_COLUMNAR_PATCH_MAX_TOMBSTONE_OVERLAY_BYTES = 4_096;
14
+ /** Fraction of declared reserved capacity that may be consumed before a rebuild. */
15
+ export const DEFAULT_COLUMNAR_PATCH_MAX_CAPACITY_UTILIZATION = 0.9;
16
+ /** Vertex count relative to the last rebuild before a rebuild is forced. */
17
+ export const DEFAULT_COLUMNAR_PATCH_MAX_VERTEX_GROWTH_RATIO = 1.5;
18
+ /** Operations accepted in one patch. There is no unbounded mode. */
19
+ export const DEFAULT_COLUMNAR_PATCH_MAX_OPERATIONS = 65_536;
20
+ /** Rows copied between cooperative abort checks during a rebuild. */
21
+ export const COLUMNAR_PATCH_ABORT_CHECK_ROWS = 8_192;
22
+ const META = Object.freeze({
23
+ version: "honua.columnar-patch.version",
24
+ sequence: "honua.columnar-patch.sequence",
25
+ cursor: "honua.columnar-patch.cursor",
26
+ observedAt: "honua.columnar-patch.observed-at",
27
+ generation: "honua.columnar-patch.generation",
28
+ tombstones: "honua.columnar-patch.tombstones",
29
+ baseRows: "honua.columnar-patch.base-rows",
30
+ baseVertices: "honua.columnar-patch.base-vertices",
31
+ reserveRows: "honua.columnar-patch.reserve-rows",
32
+ reserveVertices: "honua.columnar-patch.reserve-vertices",
33
+ reserveRings: "honua.columnar-patch.reserve-rings",
34
+ });
35
+ /** Typed failure for patch validation and patch-state parsing. */
36
+ export class HonuaColumnarPatchError extends Error {
37
+ code;
38
+ detail;
39
+ constructor(code, message, detail, options) {
40
+ super(message, options);
41
+ this.code = code;
42
+ this.detail = detail;
43
+ this.name = "HonuaColumnarPatchError";
44
+ }
45
+ }
46
+ function reject(code, message, batch, detail) {
47
+ return Object.freeze({
48
+ outcome: "rejected",
49
+ code,
50
+ message,
51
+ ...(detail === undefined ? {} : { detail: Object.freeze({ ...detail }) }),
52
+ batch,
53
+ });
54
+ }
55
+ function requireSafeCount(value, label) {
56
+ if (!Number.isSafeInteger(value) || value < 0) {
57
+ throw new HonuaColumnarPatchError("invalid-patch-state", `${label} must be a non-negative safe integer.`);
58
+ }
59
+ return value;
60
+ }
61
+ function requireCleanString(value, label) {
62
+ if (typeof value !== "string" || value.length === 0 || value.trim() !== value) {
63
+ throw new HonuaColumnarPatchError("invalid-patch-state", `${label} must be a non-empty trimmed string.`);
64
+ }
65
+ return value;
66
+ }
67
+ function requireUint32(value, label) {
68
+ if (!Number.isSafeInteger(value) || value < 0 || value > 0xffff_ffff) {
69
+ throw new HonuaColumnarPatchError("invalid-patch-state", `${label} must be a uint32 feature id.`);
70
+ }
71
+ return value;
72
+ }
73
+ function utf8Length(value) {
74
+ let bytes = 0;
75
+ for (let index = 0; index < value.length; index += 1) {
76
+ const code = value.charCodeAt(index);
77
+ if (code < 0x80)
78
+ bytes += 1;
79
+ else if (code < 0x800)
80
+ bytes += 2;
81
+ else if (code >= 0xd800 && code <= 0xdbff) {
82
+ bytes += 4;
83
+ index += 1;
84
+ }
85
+ else
86
+ bytes += 3;
87
+ }
88
+ return bytes;
89
+ }
90
+ /**
91
+ * Validate one realtime patch. Structural problems throw here; problems that
92
+ * only exist relative to a batch become a `rejected` outcome in
93
+ * {@link applyColumnarPatch}.
94
+ */
95
+ export function createColumnarPatch(input) {
96
+ if (typeof input !== "object" || input === null) {
97
+ throw new HonuaColumnarPatchError("invalid-patch-state", "Columnar patch input must be an object.");
98
+ }
99
+ const schemaId = requireCleanString(input.schemaId, "patch.schemaId");
100
+ const geometryKind = input.geometryKind;
101
+ if (geometryKind !== "point" && geometryKind !== "linestring" && geometryKind !== "polygon") {
102
+ throw new HonuaColumnarPatchError("geometry-kind-drift", `Unsupported patch geometry kind "${String(geometryKind)}".`);
103
+ }
104
+ const rawCursor = input.cursor;
105
+ if (typeof rawCursor !== "object" || rawCursor === null) {
106
+ throw new HonuaColumnarPatchError("invalid-patch-state", "patch.cursor must be an object.");
107
+ }
108
+ const cursor = Object.freeze({
109
+ cursor: requireCleanString(rawCursor.cursor, "patch.cursor.cursor"),
110
+ sequence: requireSafeCount(rawCursor.sequence, "patch.cursor.sequence"),
111
+ observedAt: requireCleanString(rawCursor.observedAt, "patch.cursor.observedAt"),
112
+ });
113
+ if (Number.isNaN(Date.parse(cursor.observedAt))) {
114
+ throw new HonuaColumnarPatchError("invalid-patch-state", "patch.cursor.observedAt must be an RFC 3339 instant.");
115
+ }
116
+ if (!Array.isArray(input.operations)) {
117
+ throw new HonuaColumnarPatchError("invalid-patch-state", "patch.operations must be an array.");
118
+ }
119
+ const maxOperations = input.maxOperations === undefined
120
+ ? DEFAULT_COLUMNAR_PATCH_MAX_OPERATIONS
121
+ : requireSafeCount(input.maxOperations, "patch.maxOperations");
122
+ if (input.operations.length > maxOperations) {
123
+ throw new HonuaColumnarPatchError("patch-limit-exceeded", `Patch carries ${input.operations.length} operations; the limit is ${maxOperations}.`, { operations: input.operations.length, limit: maxOperations });
124
+ }
125
+ const operations = input.operations.map((operation, index) => normalizeOperation(operation, index));
126
+ return Object.freeze({
127
+ kind: COLUMNAR_PATCH_KIND,
128
+ version: COLUMNAR_PATCH_VERSION,
129
+ schemaId,
130
+ geometryKind,
131
+ cursor,
132
+ operations: Object.freeze(operations),
133
+ });
134
+ }
135
+ function normalizeOperation(operation, index) {
136
+ if (typeof operation !== "object" || operation === null) {
137
+ throw new HonuaColumnarPatchError("invalid-patch-state", `patch.operations[${index}] must be an object.`);
138
+ }
139
+ const featureId = requireUint32(operation.featureId, `patch.operations[${index}].featureId`);
140
+ if (operation.op === "delete")
141
+ return Object.freeze({ op: "delete", featureId });
142
+ if (operation.op === "append") {
143
+ if (!("geometry" in operation)) {
144
+ throw new HonuaColumnarPatchError("invalid-patch-state", `patch.operations[${index}] must declare a geometry to append.`);
145
+ }
146
+ return Object.freeze({
147
+ op: "append",
148
+ featureId,
149
+ geometry: operation.geometry,
150
+ ...("timestamp" in operation ? { timestamp: assertTimestamp(operation.timestamp, index) } : {}),
151
+ ...("dictionaryValue" in operation
152
+ ? { dictionaryValue: assertDictionaryValue(operation.dictionaryValue, index) }
153
+ : {}),
154
+ });
155
+ }
156
+ if (operation.op === "update") {
157
+ if (!("geometry" in operation) && !("timestamp" in operation) && !("dictionaryValue" in operation)) {
158
+ throw new HonuaColumnarPatchError("invalid-patch-state", `patch.operations[${index}] must declare at least one column to update.`);
159
+ }
160
+ return Object.freeze({
161
+ op: "update",
162
+ featureId,
163
+ ...("geometry" in operation ? { geometry: operation.geometry } : {}),
164
+ ...("timestamp" in operation ? { timestamp: assertTimestamp(operation.timestamp, index) } : {}),
165
+ ...("dictionaryValue" in operation
166
+ ? { dictionaryValue: assertDictionaryValue(operation.dictionaryValue, index) }
167
+ : {}),
168
+ });
169
+ }
170
+ throw new HonuaColumnarPatchError("invalid-patch-state", `patch.operations[${index}].op must be "append", "update", or "delete".`);
171
+ }
172
+ function assertTimestamp(value, index) {
173
+ if (value === null || typeof value === "bigint")
174
+ return value;
175
+ throw new HonuaColumnarPatchError("invalid-patch-state", `patch.operations[${index}].timestamp must be a bigint or null.`);
176
+ }
177
+ function assertDictionaryValue(value, index) {
178
+ if (value === null || typeof value === "string")
179
+ return value;
180
+ throw new HonuaColumnarPatchError("invalid-patch-state", `patch.operations[${index}].dictionaryValue must be a string or null.`);
181
+ }
182
+ /** Inclusive ascending disjoint tombstone ranges with bounded mutation. */
183
+ class TombstoneSet {
184
+ #ranges;
185
+ #count;
186
+ constructor(ranges = []) {
187
+ this.#ranges = ranges.map(([start, end]) => [start, end]);
188
+ this.#count = this.#ranges.reduce((total, [start, end]) => total + (end - start + 1), 0);
189
+ }
190
+ get count() {
191
+ return this.#count;
192
+ }
193
+ get ranges() {
194
+ return this.#ranges.map(([start, end]) => Object.freeze([start, end]));
195
+ }
196
+ has(row) {
197
+ let low = 0;
198
+ let high = this.#ranges.length - 1;
199
+ while (low <= high) {
200
+ const middle = (low + high) >> 1;
201
+ const [start, end] = this.#ranges[middle];
202
+ if (row < start)
203
+ high = middle - 1;
204
+ else if (row > end)
205
+ low = middle + 1;
206
+ else
207
+ return true;
208
+ }
209
+ return false;
210
+ }
211
+ add(row) {
212
+ if (this.has(row))
213
+ return;
214
+ let insert = 0;
215
+ while (insert < this.#ranges.length && this.#ranges[insert][0] < row)
216
+ insert += 1;
217
+ this.#ranges.splice(insert, 0, [row, row]);
218
+ this.#count += 1;
219
+ this.#merge(insert);
220
+ }
221
+ #merge(index) {
222
+ if (index > 0 && this.#ranges[index - 1][1] + 1 === this.#ranges[index][0]) {
223
+ this.#ranges[index - 1][1] = this.#ranges[index][1];
224
+ this.#ranges.splice(index, 1);
225
+ index -= 1;
226
+ }
227
+ if (index + 1 < this.#ranges.length && this.#ranges[index][1] + 1 === this.#ranges[index + 1][0]) {
228
+ this.#ranges[index][1] = this.#ranges[index + 1][1];
229
+ this.#ranges.splice(index + 1, 1);
230
+ }
231
+ }
232
+ encode() {
233
+ return this.#ranges.map(([start, end]) => (start === end ? `${start}` : `${start}-${end}`)).join(",");
234
+ }
235
+ }
236
+ function parseTombstones(encoded, rowCount) {
237
+ if (encoded === undefined || encoded === "")
238
+ return new TombstoneSet();
239
+ const ranges = [];
240
+ let previousEnd = -2;
241
+ for (const token of encoded.split(",")) {
242
+ const separator = token.indexOf("-");
243
+ const startText = separator === -1 ? token : token.slice(0, separator);
244
+ const endText = separator === -1 ? token : token.slice(separator + 1);
245
+ const start = Number(startText);
246
+ const end = Number(endText);
247
+ if (!/^\d+$/.test(startText) ||
248
+ !/^\d+$/.test(endText) ||
249
+ !Number.isSafeInteger(start) ||
250
+ !Number.isSafeInteger(end) ||
251
+ start > end ||
252
+ end >= rowCount ||
253
+ start <= previousEnd + 1) {
254
+ throw new HonuaColumnarPatchError("invalid-patch-state", "Tombstone overlay must be ascending, disjoint, non-adjacent, and inside the batch.", { encoded });
255
+ }
256
+ previousEnd = end;
257
+ ranges.push([start, end]);
258
+ }
259
+ return new TombstoneSet(ranges);
260
+ }
261
+ function resolveThresholds(thresholds = {}) {
262
+ const ratio = (value, fallback, name) => {
263
+ if (value === undefined)
264
+ return fallback;
265
+ if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
266
+ throw new HonuaColumnarPatchError("invalid-patch-state", `thresholds.${name} must be a non-negative number.`);
267
+ }
268
+ return value;
269
+ };
270
+ return Object.freeze({
271
+ maxTombstoneRatio: ratio(thresholds.maxTombstoneRatio, DEFAULT_COLUMNAR_PATCH_MAX_TOMBSTONE_RATIO, "maxTombstoneRatio"),
272
+ maxTombstoneOverlayBytes: ratio(thresholds.maxTombstoneOverlayBytes, DEFAULT_COLUMNAR_PATCH_MAX_TOMBSTONE_OVERLAY_BYTES, "maxTombstoneOverlayBytes"),
273
+ maxCapacityUtilization: ratio(thresholds.maxCapacityUtilization, DEFAULT_COLUMNAR_PATCH_MAX_CAPACITY_UTILIZATION, "maxCapacityUtilization"),
274
+ maxVertexGrowthRatio: ratio(thresholds.maxVertexGrowthRatio, DEFAULT_COLUMNAR_PATCH_MAX_VERTEX_GROWTH_RATIO, "maxVertexGrowthRatio"),
275
+ });
276
+ }
277
+ function dimensionNames(dimensions) {
278
+ switch (dimensions) {
279
+ case "xy":
280
+ return ["x", "y"];
281
+ case "xyz":
282
+ return ["x", "y", "z"];
283
+ case "xym":
284
+ return ["x", "y", "m"];
285
+ default:
286
+ return ["x", "y", "z", "m"];
287
+ }
288
+ }
289
+ function descriptorMap(batch) {
290
+ return new Map(batch.buffers.map((buffer) => [buffer.id, buffer]));
291
+ }
292
+ /** Spare bytes between a descriptor's declared slice and the end of its backing. */
293
+ function spareBytes(buffer) {
294
+ if (!buffer)
295
+ return Number.POSITIVE_INFINITY;
296
+ return buffer.data.byteLength - (buffer.byteOffset + buffer.byteLength);
297
+ }
298
+ function readLayout(batch, limits) {
299
+ const inspection = inspectGeoArrowBatch(batch, limits);
300
+ return {
301
+ inspection,
302
+ kind: inspection.geometry.kind,
303
+ names: dimensionNames(inspection.geometry.dimensions),
304
+ coordinateLayout: inspection.geometry.coordinateLayout,
305
+ geometryField: inspection.geometry.field,
306
+ ...(inspection.temporal === undefined ? {} : { temporalField: inspection.temporal.field }),
307
+ ...(inspection.dictionary === undefined ? {} : { dictionaryField: inspection.dictionary.field }),
308
+ ...(inspection.featureIds === undefined ? {} : { featureIdField: inspection.featureIds.field }),
309
+ rows: inspection.batch.rowCount,
310
+ vertices: inspection.metrics.vertices,
311
+ rings: inspection.metrics.rings,
312
+ descriptors: descriptorMap(inspection.batch),
313
+ };
314
+ }
315
+ /** Remaining append capacity, derived from the batch's own allocations. */
316
+ function remainingCapacity(context) {
317
+ const geometryField = context.geometryField;
318
+ const rowLimits = [];
319
+ const validityRows = (id) => {
320
+ const descriptor = context.descriptors.get(id);
321
+ if (descriptor)
322
+ rowLimits.push((descriptor.data.byteLength - descriptor.byteOffset) * 8 - context.rows);
323
+ };
324
+ validityRows(`${geometryField}.validity`);
325
+ if (context.kind !== "point") {
326
+ rowLimits.push(Math.floor(spareBytes(context.descriptors.get(`${geometryField}.offsets`)) / 4));
327
+ }
328
+ if (context.temporalField !== undefined) {
329
+ rowLimits.push(Math.floor(spareBytes(context.descriptors.get(`${context.temporalField}.values`)) / 8));
330
+ validityRows(`${context.temporalField}.validity`);
331
+ }
332
+ if (context.dictionaryField !== undefined) {
333
+ rowLimits.push(Math.floor(spareBytes(context.descriptors.get(`${context.dictionaryField}.indices`)) / 4));
334
+ validityRows(`${context.dictionaryField}.validity`);
335
+ }
336
+ if (context.featureIdField !== undefined) {
337
+ rowLimits.push(Math.floor(spareBytes(context.descriptors.get(`${context.featureIdField}.values`)) / 4));
338
+ }
339
+ const coordinateSpare = context.coordinateLayout === "interleaved"
340
+ ? Math.floor(spareBytes(context.descriptors.get(`${geometryField}.coordinates`)) / (8 * context.names.length))
341
+ : Math.min(...context.names.map((name) => Math.floor(spareBytes(context.descriptors.get(`${geometryField}.${name}`)) / 8)));
342
+ const rings = context.kind === "polygon"
343
+ ? Math.floor(spareBytes(context.descriptors.get(`${geometryField}.ring-offsets`)) / 4)
344
+ : 0;
345
+ if (context.kind === "point")
346
+ rowLimits.push(coordinateSpare);
347
+ const rows = rowLimits.length === 0 ? coordinateSpare : Math.max(0, Math.min(...rowLimits));
348
+ return Object.freeze({
349
+ rows: Number.isFinite(rows) ? rows : 0,
350
+ vertices: context.kind === "point" ? rows : Math.max(0, coordinateSpare),
351
+ rings: Math.max(0, rings),
352
+ });
353
+ }
354
+ function buildState(metadata, context, vertices, rings) {
355
+ const declared = metadata[META.version];
356
+ if (declared !== undefined && declared !== HONUA_COLUMNAR_PATCH_LAYOUT_VERSION) {
357
+ throw new HonuaColumnarPatchError("invalid-patch-state", `Batch declares columnar patch layout "${declared}"; this build implements ${HONUA_COLUMNAR_PATCH_LAYOUT_VERSION}.`);
358
+ }
359
+ const tombstones = parseTombstones(metadata[META.tombstones], context.rows);
360
+ const number = (key, fallback) => {
361
+ const value = metadata[key];
362
+ if (value === undefined)
363
+ return fallback;
364
+ const parsed = Number(value);
365
+ if (!/^\d+$/.test(value) || !Number.isSafeInteger(parsed)) {
366
+ throw new HonuaColumnarPatchError("invalid-patch-state", `Patch metadata "${key}" must be a non-negative integer.`);
367
+ }
368
+ return parsed;
369
+ };
370
+ const capacity = remainingCapacity(context);
371
+ const sequence = metadata[META.sequence] === undefined ? undefined : number(META.sequence, 0);
372
+ const encoded = tombstones.encode();
373
+ return Object.freeze({
374
+ layoutVersion: HONUA_COLUMNAR_PATCH_LAYOUT_VERSION,
375
+ rowCount: context.rows,
376
+ liveRowCount: context.rows - tombstones.count,
377
+ tombstoneCount: tombstones.count,
378
+ tombstoneRatio: context.rows === 0 ? 0 : tombstones.count / context.rows,
379
+ tombstoneRanges: Object.freeze(tombstones.ranges),
380
+ tombstoneOverlayBytes: utf8Length(encoded),
381
+ ...(sequence === undefined ? {} : { sequence }),
382
+ ...(metadata[META.cursor] === undefined ? {} : { cursor: metadata[META.cursor] }),
383
+ ...(metadata[META.observedAt] === undefined ? {} : { observedAt: metadata[META.observedAt] }),
384
+ generation: number(META.generation, 0),
385
+ vertices,
386
+ rings,
387
+ baseRows: number(META.baseRows, context.rows),
388
+ baseVertices: number(META.baseVertices, vertices),
389
+ reserve: Object.freeze({
390
+ rows: number(META.reserveRows, capacity.rows),
391
+ vertices: number(META.reserveVertices, capacity.vertices),
392
+ rings: number(META.reserveRings, capacity.rings),
393
+ }),
394
+ capacity,
395
+ });
396
+ }
397
+ function readState(batch, layout) {
398
+ return buildState(batch.schema.metadata ?? {}, layout, layout.vertices, layout.rings);
399
+ }
400
+ /**
401
+ * Read the patch overlay a batch carries. A batch that has never been patched
402
+ * reports an empty overlay and whatever capacity its allocations already hold.
403
+ */
404
+ export function inspectColumnarPatchState(batch, limits = {}) {
405
+ return readState(batch, readLayout(batch, limits));
406
+ }
407
+ /**
408
+ * A per-row liveness mask for renderers: `1` for a live row, `0` for a
409
+ * tombstoned one. Bind it as a filter attribute so a deleted feature stops
410
+ * drawing without waiting for the rebuild that physically removes it.
411
+ */
412
+ export function columnarPatchLiveMask(state) {
413
+ const mask = new Uint8Array(state.rowCount).fill(1);
414
+ for (const [start, end] of state.tombstoneRanges)
415
+ mask.fill(0, start, end + 1);
416
+ return mask;
417
+ }
418
+ /**
419
+ * Materialize the live rows of a patched batch in row order. A tombstoned row
420
+ * is never returned, and an updated value is the value that is returned,
421
+ * because the update was written over the row it belongs to.
422
+ */
423
+ export function decodePatchedGeoArrowBatch(batch, limits = {}) {
424
+ const layout = readLayout(batch, limits);
425
+ const state = readState(batch, layout);
426
+ const decoded = decodeGeoArrowBatch(batch, limits);
427
+ const mask = columnarPatchLiveMask(state);
428
+ const rows = [];
429
+ for (let row = 0; row < decoded.rows.length; row += 1)
430
+ if (mask[row] === 1)
431
+ rows.push(decoded.rows[row]);
432
+ return Object.freeze({
433
+ rows: Object.freeze(rows),
434
+ state,
435
+ metrics: Object.freeze({
436
+ rows: layout.rows,
437
+ tombstonedRows: state.tombstoneCount,
438
+ materializedRows: rows.length,
439
+ }),
440
+ });
441
+ }
442
+ function resolveReserve(reserve, kind) {
443
+ if (typeof reserve !== "object" || reserve === null) {
444
+ throw new HonuaColumnarPatchError("invalid-patch-state", "reserve must be an object.");
445
+ }
446
+ const rows = requireSafeCount(reserve.rows, "reserve.rows");
447
+ const vertices = kind === "point"
448
+ ? rows
449
+ : reserve.vertices === undefined
450
+ ? 0
451
+ : requireSafeCount(reserve.vertices, "reserve.vertices");
452
+ const rings = kind === "polygon" ? (reserve.rings === undefined ? 0 : requireSafeCount(reserve.rings, "reserve.rings")) : 0;
453
+ return { rows, vertices, rings };
454
+ }
455
+ /** Extra backing bytes each normative buffer needs to hold the declared reserve. */
456
+ function reserveBytesFor(buffer, layout, reserve) {
457
+ const geometry = layout.inspection.geometry;
458
+ const suffix = buffer.id.slice(buffer.id.lastIndexOf(".") + 1);
459
+ if (buffer.field === geometry.field) {
460
+ if (suffix === "validity") {
461
+ return Math.ceil((layout.rows + reserve.rows) / 8) - Math.ceil(layout.rows / 8);
462
+ }
463
+ if (buffer.id.endsWith(".ring-offsets"))
464
+ return reserve.rings * 4;
465
+ if (suffix === "offsets")
466
+ return reserve.rows * 4;
467
+ if (suffix === "coordinates")
468
+ return reserve.vertices * layout.names.length * 8;
469
+ return reserve.vertices * 8;
470
+ }
471
+ if (suffix === "validity")
472
+ return Math.ceil((layout.rows + reserve.rows) / 8) - Math.ceil(layout.rows / 8);
473
+ if (layout.inspection.temporal && buffer.field === layout.inspection.temporal.field)
474
+ return reserve.rows * 8;
475
+ if (layout.inspection.dictionary && buffer.field === layout.inspection.dictionary.field) {
476
+ return suffix === "indices" ? reserve.rows * 4 : 0;
477
+ }
478
+ return reserve.rows * 4;
479
+ }
480
+ /** Re-home every payload buffer into a backing that carries the declared reserve. */
481
+ function reserveCapacity(batch, reserve, patchMetadata, limits) {
482
+ const layout = readLayout(batch, limits);
483
+ let reservedBytes = 0;
484
+ const buffers = batch.buffers.map((buffer) => {
485
+ const extra = reserveBytesFor(buffer, layout, reserve);
486
+ reservedBytes += extra;
487
+ const backing = new ArrayBuffer(buffer.byteLength + extra);
488
+ new Uint8Array(backing).set(new Uint8Array(buffer.data, buffer.byteOffset, buffer.byteLength));
489
+ return {
490
+ id: buffer.id,
491
+ role: buffer.role,
492
+ ...(buffer.field === undefined ? {} : { field: buffer.field }),
493
+ data: backing,
494
+ byteOffset: 0,
495
+ byteLength: buffer.byteLength,
496
+ };
497
+ });
498
+ return {
499
+ batch: createColumnarBatch({
500
+ id: batch.id,
501
+ schema: { ...batch.schema, metadata: withPatchMetadata(batch.schema.metadata, patchMetadata) },
502
+ rowCount: batch.rowCount,
503
+ sequence: batch.sequence,
504
+ ...(batch.rowOffset === undefined ? {} : { rowOffset: batch.rowOffset }),
505
+ ...(batch.identity === undefined ? {} : { identity: batch.identity }),
506
+ buffers,
507
+ }, limits),
508
+ reservedBytes,
509
+ };
510
+ }
511
+ /**
512
+ * Replace the patch overlay wholesale rather than merging it. A compacted
513
+ * rebuild carries no tombstones, and a surviving stale range would silently
514
+ * hide a live row.
515
+ */
516
+ function withPatchMetadata(metadata, patchMetadata) {
517
+ const merged = { ...metadata };
518
+ for (const key of Object.values(META))
519
+ delete merged[key];
520
+ return { ...merged, ...patchMetadata };
521
+ }
522
+ function patchMetadataFor(options) {
523
+ return {
524
+ [META.version]: HONUA_COLUMNAR_PATCH_LAYOUT_VERSION,
525
+ [META.generation]: String(options.generation),
526
+ [META.baseRows]: String(options.baseRows),
527
+ [META.baseVertices]: String(options.baseVertices),
528
+ [META.reserveRows]: String(options.reserve.rows),
529
+ [META.reserveVertices]: String(options.reserve.vertices),
530
+ [META.reserveRings]: String(options.reserve.rings),
531
+ ...(options.tombstones === "" ? {} : { [META.tombstones]: options.tombstones }),
532
+ ...(options.cursor === undefined
533
+ ? {}
534
+ : {
535
+ [META.sequence]: String(options.cursor.sequence),
536
+ [META.cursor]: options.cursor.cursor,
537
+ [META.observedAt]: options.cursor.observedAt,
538
+ }),
539
+ };
540
+ }
541
+ /**
542
+ * Convert semantic GeoArrow input into a batch that carries explicitly declared
543
+ * spare capacity, so later appends can be written without reallocating a
544
+ * backing buffer. The reserve is never implicit: a batch created through
545
+ * `createGeoArrowBatch` has none and rebuilds on its first append.
546
+ */
547
+ export function createPatchableGeoArrowBatch(input, options) {
548
+ if (typeof options !== "object" || options === null) {
549
+ throw new HonuaColumnarPatchError("invalid-patch-state", "Patchable batch options must be an object.");
550
+ }
551
+ const limits = { ...options };
552
+ delete limits.reserve;
553
+ const created = createGeoArrowBatch(input, limits);
554
+ const layout = readLayout(created.batch, limits);
555
+ const reserve = resolveReserve(options.reserve, layout.kind);
556
+ const metadata = patchMetadataFor({
557
+ baseRows: layout.rows,
558
+ baseVertices: layout.vertices,
559
+ reserve,
560
+ generation: 0,
561
+ tombstones: "",
562
+ });
563
+ const reserved = reserveCapacity(created.batch, reserve, metadata, limits);
564
+ const reservedLayout = readLayout(reserved.batch, limits);
565
+ return Object.freeze({
566
+ batch: reserved.batch,
567
+ state: readState(reserved.batch, reservedLayout),
568
+ metrics: Object.freeze({
569
+ rows: created.metrics.rows,
570
+ vertices: created.metrics.vertices,
571
+ rings: created.metrics.rings,
572
+ copiedBytes: created.metrics.copiedBytes,
573
+ backingBytes: created.metrics.backingBytes + reserved.reservedBytes,
574
+ reservedBytes: reserved.reservedBytes,
575
+ }),
576
+ });
577
+ }
578
+ const featureIdIndexCache = new WeakMap();
579
+ /**
580
+ * Feature-id to row index, cached per feature-id backing buffer. Feature ids
581
+ * are immutable row identity, so the cache is extended for appended rows rather
582
+ * than rebuilt, which keeps patch cost proportional to the patch.
583
+ */
584
+ function featureIdIndex(values, backing) {
585
+ let entry = featureIdIndexCache.get(backing);
586
+ if (!entry || entry.rows > values.length) {
587
+ entry = { rows: 0, index: new Map() };
588
+ featureIdIndexCache.set(backing, entry);
589
+ }
590
+ for (let row = entry.rows; row < values.length; row += 1)
591
+ entry.index.set(values[row], row);
592
+ entry.rows = values.length;
593
+ return entry.index;
594
+ }
595
+ const NULL_POINT_GEOMETRY = (names) => ({
596
+ isNull: true,
597
+ positions: [Object.freeze(new Array(names).fill(0))],
598
+ ringSizes: [],
599
+ vertices: 1,
600
+ rings: 0,
601
+ });
602
+ function planGeometry(value, kind, names, label) {
603
+ if (value === null) {
604
+ return kind === "point"
605
+ ? NULL_POINT_GEOMETRY(names.length)
606
+ : { isNull: true, positions: [], ringSizes: [], vertices: 0, rings: 0 };
607
+ }
608
+ if (!Array.isArray(value)) {
609
+ throw new HonuaColumnarPatchError("invalid-geometry", `${label} must be an array or null.`);
610
+ }
611
+ if (kind === "point") {
612
+ return { isNull: false, positions: [planPosition(value, names, label)], ringSizes: [], vertices: 1, rings: 0 };
613
+ }
614
+ if (kind === "linestring") {
615
+ const positions = value.map((position, index) => planPosition(position, names, `${label}[${index}]`));
616
+ return { isNull: false, positions, ringSizes: [], vertices: positions.length, rings: 0 };
617
+ }
618
+ const positions = [];
619
+ const ringSizes = [];
620
+ value.forEach((ring, ringIndex) => {
621
+ if (!Array.isArray(ring)) {
622
+ throw new HonuaColumnarPatchError("invalid-geometry", `${label}[${ringIndex}] must be an array.`);
623
+ }
624
+ if (ring.length !== 0 && ring.length < 4) {
625
+ throw new HonuaColumnarPatchError("invalid-geometry", `${label}[${ringIndex}] must be empty or contain at least four positions.`);
626
+ }
627
+ const ringPositions = ring.map((position, index) => planPosition(position, names, `${label}[${ringIndex}][${index}]`));
628
+ if (ringPositions.length > 0 &&
629
+ !ringPositions[0].every((coordinate, index) => coordinate === ringPositions[ringPositions.length - 1][index])) {
630
+ throw new HonuaColumnarPatchError("invalid-geometry", `${label}[${ringIndex}] must be closed.`);
631
+ }
632
+ ringSizes.push(ringPositions.length);
633
+ positions.push(...ringPositions);
634
+ });
635
+ return { isNull: false, positions, ringSizes, vertices: positions.length, rings: ringSizes.length };
636
+ }
637
+ function planPosition(value, names, label) {
638
+ if (!Array.isArray(value) || value.length !== names.length) {
639
+ throw new HonuaColumnarPatchError("invalid-geometry", `${label} must contain ${names.length} coordinates (${names.join(", ")}).`);
640
+ }
641
+ for (const coordinate of value) {
642
+ if (typeof coordinate !== "number" || !Number.isFinite(coordinate)) {
643
+ throw new HonuaColumnarPatchError("invalid-geometry", `${label} must contain finite numbers.`);
644
+ }
645
+ }
646
+ return Object.freeze([...value]);
647
+ }
648
+ function decodeDictionaryValues(inspection) {
649
+ const dictionary = inspection.dictionary;
650
+ const byValue = new Map();
651
+ if (!dictionary)
652
+ return byValue;
653
+ const decoder = new TextDecoder("utf-8", { fatal: true });
654
+ for (let index = 0; index < dictionary.offsets.length - 1; index += 1) {
655
+ const value = decoder.decode(dictionary.values.subarray(dictionary.offsets[index], dictionary.offsets[index + 1]));
656
+ if (!byValue.has(value))
657
+ byValue.set(value, index);
658
+ }
659
+ return byValue;
660
+ }
661
+ function bitSet(validity, row) {
662
+ return validity === undefined || (validity[row >> 3] & (1 << (row & 7))) !== 0;
663
+ }
664
+ function rowVertexSpan(inspection, row) {
665
+ const geometry = inspection.geometry;
666
+ if (geometry.kind === "point")
667
+ return { vertices: 1, rings: 0 };
668
+ const start = geometry.offsets[row];
669
+ const end = geometry.offsets[row + 1];
670
+ if (geometry.kind === "linestring")
671
+ return { vertices: end - start, rings: 0 };
672
+ return { vertices: geometry.ringOffsets[end] - geometry.ringOffsets[start], rings: end - start };
673
+ }
674
+ /**
675
+ * Validate the whole patch against the batch before a byte moves. Every
676
+ * rejection returns here; nothing below this point can fail partway through.
677
+ */
678
+ function planPatch(batch, patch, layout, state) {
679
+ const inspection = layout.inspection;
680
+ const featureIds = inspection.featureIds;
681
+ if (!featureIds) {
682
+ return reject("missing-feature-id-column", "A patchable batch must declare a feature-id column; row position is not stable identity.", batch);
683
+ }
684
+ const index = featureIdIndex(featureIds.values, layout.descriptors.get(`${featureIds.field}.values`).data);
685
+ const tombstones = new TombstoneSet(state.tombstoneRanges);
686
+ const orderingKeys = new Set((batch.identity?.ordering.keys ?? []).map((key) => key.field));
687
+ const dictionaryByValue = inspection.dictionary === undefined ? new Map() : decodeDictionaryValues(inspection);
688
+ const appends = [];
689
+ const updates = [];
690
+ const updatesByRow = new Map();
691
+ const deletes = [];
692
+ const touched = new Set();
693
+ const newDictionaryValues = [];
694
+ let appendedVertices = 0;
695
+ let appendedRings = 0;
696
+ let layoutBlocked = false;
697
+ let layoutBlockedReason;
698
+ const block = (reason) => {
699
+ if (!layoutBlocked) {
700
+ layoutBlocked = true;
701
+ layoutBlockedReason = reason;
702
+ }
703
+ };
704
+ for (const operation of patch.operations) {
705
+ if (touched.has(operation.featureId)) {
706
+ return reject("duplicate-feature-id", `Feature ${operation.featureId} carries more than one operation in one patch.`, batch, { featureId: operation.featureId });
707
+ }
708
+ touched.add(operation.featureId);
709
+ if (operation.op === "append") {
710
+ if (orderingKeys.size > 0) {
711
+ return reject("ordering-conflict", "Appending to a batch whose identity declares sort keys would break the declared order.", batch, { keys: [...orderingKeys] });
712
+ }
713
+ const existingRow = index.get(operation.featureId);
714
+ if (existingRow !== undefined && existingRow < layout.rows) {
715
+ if (!tombstones.has(existingRow)) {
716
+ return reject("duplicate-feature-id", `Feature ${operation.featureId} already exists in the batch.`, batch, {
717
+ featureId: operation.featureId,
718
+ row: existingRow,
719
+ });
720
+ }
721
+ // Re-creating a deleted id is legal, but two rows may not carry one id:
722
+ // the compacting rebuild is what frees the tombstoned row's identity.
723
+ block("re-creating a tombstoned feature id needs a compacting rebuild");
724
+ }
725
+ if (inspection.temporal && operation.timestamp === undefined) {
726
+ return reject("incomplete-append", `Append of feature ${operation.featureId} omits the batch's temporal column.`, batch, { featureId: operation.featureId, column: inspection.temporal.field });
727
+ }
728
+ if (inspection.dictionary && operation.dictionaryValue === undefined) {
729
+ return reject("incomplete-append", `Append of feature ${operation.featureId} omits the batch's dictionary column.`, batch, { featureId: operation.featureId, column: inspection.dictionary.field });
730
+ }
731
+ let geometry;
732
+ try {
733
+ geometry = planGeometry(operation.geometry, layout.kind, layout.names, `append(${operation.featureId})`);
734
+ }
735
+ catch (cause) {
736
+ if (cause instanceof HonuaColumnarPatchError)
737
+ return reject(cause.code, cause.message, batch, cause.detail);
738
+ throw cause;
739
+ }
740
+ if (geometry.isNull && inspection.geometry.validity === undefined) {
741
+ block("appending a null geometry needs a validity buffer the batch does not declare");
742
+ }
743
+ const timestamp = operation.timestamp ?? null;
744
+ if (timestamp === null && inspection.temporal && inspection.temporal.validity === undefined) {
745
+ block("appending a null timestamp needs a validity buffer the batch does not declare");
746
+ }
747
+ const dictionaryValue = operation.dictionaryValue ?? null;
748
+ let dictionaryIndex = null;
749
+ if (inspection.dictionary) {
750
+ if (dictionaryValue === null) {
751
+ if (inspection.dictionary.validity === undefined) {
752
+ block("appending a null dictionary value needs a validity buffer the batch does not declare");
753
+ }
754
+ }
755
+ else {
756
+ const existing = dictionaryByValue.get(dictionaryValue);
757
+ if (existing === undefined) {
758
+ block("appending an unseen dictionary value needs a larger dictionary");
759
+ if (!newDictionaryValues.includes(dictionaryValue))
760
+ newDictionaryValues.push(dictionaryValue);
761
+ }
762
+ else
763
+ dictionaryIndex = existing;
764
+ }
765
+ }
766
+ appends.push({ featureId: operation.featureId, geometry, timestamp, dictionaryValue, dictionaryIndex });
767
+ appendedVertices += geometry.vertices;
768
+ appendedRings += geometry.rings;
769
+ continue;
770
+ }
771
+ const row = index.get(operation.featureId);
772
+ if (row === undefined || row >= layout.rows) {
773
+ return reject("unknown-feature-id", `Feature ${operation.featureId} is not present in the batch.`, batch, {
774
+ featureId: operation.featureId,
775
+ });
776
+ }
777
+ if (tombstones.has(row)) {
778
+ return reject("deleted-feature-id", `Feature ${operation.featureId} is already tombstoned.`, batch, {
779
+ featureId: operation.featureId,
780
+ row,
781
+ });
782
+ }
783
+ if (operation.op === "delete") {
784
+ deletes.push(row);
785
+ tombstones.add(row);
786
+ continue;
787
+ }
788
+ const update = { row, featureId: operation.featureId };
789
+ if ("geometry" in operation) {
790
+ if (orderingKeys.has(inspection.geometry.field)) {
791
+ return reject("ordering-conflict", "Updating a declared sort key would break the declared order.", batch, {
792
+ field: inspection.geometry.field,
793
+ });
794
+ }
795
+ let geometry;
796
+ try {
797
+ geometry = planGeometry(operation.geometry, layout.kind, layout.names, `update(${operation.featureId})`);
798
+ }
799
+ catch (cause) {
800
+ if (cause instanceof HonuaColumnarPatchError)
801
+ return reject(cause.code, cause.message, batch, cause.detail);
802
+ throw cause;
803
+ }
804
+ const span = rowVertexSpan(inspection, row);
805
+ if (geometry.vertices !== span.vertices || geometry.rings !== span.rings) {
806
+ block("an update that changes a row's vertex or ring count moves every later row");
807
+ }
808
+ if (geometry.isNull !== !bitSet(inspection.geometry.validity, row) &&
809
+ inspection.geometry.validity === undefined) {
810
+ block("changing geometry nullability needs a validity buffer the batch does not declare");
811
+ }
812
+ update.geometry = geometry;
813
+ }
814
+ if ("timestamp" in operation) {
815
+ if (!inspection.temporal) {
816
+ return reject("schema-drift", "The patch updates a temporal column the batch does not declare.", batch);
817
+ }
818
+ if (orderingKeys.has(inspection.temporal.field)) {
819
+ return reject("ordering-conflict", "Updating a declared sort key would break the declared order.", batch, {
820
+ field: inspection.temporal.field,
821
+ });
822
+ }
823
+ const timestamp = operation.timestamp ?? null;
824
+ if (timestamp === null && inspection.temporal.validity === undefined) {
825
+ block("writing a null timestamp needs a validity buffer the batch does not declare");
826
+ }
827
+ update.timestamp = timestamp;
828
+ }
829
+ if ("dictionaryValue" in operation) {
830
+ if (!inspection.dictionary) {
831
+ return reject("schema-drift", "The patch updates a dictionary column the batch does not declare.", batch);
832
+ }
833
+ if (orderingKeys.has(inspection.dictionary.field)) {
834
+ return reject("ordering-conflict", "Updating a declared sort key would break the declared order.", batch, {
835
+ field: inspection.dictionary.field,
836
+ });
837
+ }
838
+ const value = operation.dictionaryValue ?? null;
839
+ update.dictionaryValue = value;
840
+ if (value === null) {
841
+ if (inspection.dictionary.validity === undefined) {
842
+ block("writing a null dictionary value needs a validity buffer the batch does not declare");
843
+ }
844
+ update.dictionaryIndex = null;
845
+ }
846
+ else {
847
+ const existing = dictionaryByValue.get(value);
848
+ if (existing === undefined) {
849
+ block("writing an unseen dictionary value needs a larger dictionary");
850
+ if (!newDictionaryValues.includes(value))
851
+ newDictionaryValues.push(value);
852
+ }
853
+ else
854
+ update.dictionaryIndex = existing;
855
+ }
856
+ }
857
+ updates.push(update);
858
+ updatesByRow.set(row, update);
859
+ }
860
+ return {
861
+ appends,
862
+ updates,
863
+ updatesByRow,
864
+ deletes,
865
+ tombstones,
866
+ appendedVertices,
867
+ appendedRings,
868
+ layoutBlocked,
869
+ ...(layoutBlockedReason === undefined ? {} : { layoutBlockedReason }),
870
+ newDictionaryValues,
871
+ };
872
+ }
873
+ function assertNotAborted(signal) {
874
+ if (signal?.aborted)
875
+ throw new DOMException("The operation was aborted", "AbortError");
876
+ }
877
+ function writeValidityBit(descriptor, row, valid, accounting) {
878
+ const bytes = new Uint8Array(descriptor.data, descriptor.byteOffset);
879
+ const byte = row >> 3;
880
+ const mask = 1 << (row & 7);
881
+ if (valid)
882
+ bytes[byte] |= mask;
883
+ else
884
+ bytes[byte] &= ~mask;
885
+ accounting.bytes += 1;
886
+ }
887
+ /**
888
+ * Write appended rows and updated values into the batch's existing backings and
889
+ * return the descriptors for the grown slices. Nothing here can fail: every
890
+ * bound was proven in {@link planPatch} and the capacity check.
891
+ */
892
+ function applyInPlace(layout, plan, accounting) {
893
+ const inspection = layout.inspection;
894
+ const geometry = inspection.geometry;
895
+ const descriptors = new Map(layout.descriptors);
896
+ const appended = plan.appends.length;
897
+ const rows = layout.rows;
898
+ const names = layout.names;
899
+ const grow = (id, byteLength) => {
900
+ const descriptor = descriptors.get(id);
901
+ const grown = { ...descriptor, byteLength };
902
+ descriptors.set(id, grown);
903
+ return grown;
904
+ };
905
+ // Updates first: they only rewrite bytes that already belong to the batch.
906
+ for (const update of plan.updates) {
907
+ if (update.geometry) {
908
+ const start = geometry.kind === "point"
909
+ ? update.row
910
+ : geometry.kind === "linestring"
911
+ ? geometry.offsets[update.row]
912
+ : geometry.ringOffsets[geometry.offsets[update.row]];
913
+ writePositions(layout, start, update.geometry.positions, accounting);
914
+ if (geometry.kind === "polygon" && update.geometry.ringSizes.length > 0) {
915
+ let vertex = start;
916
+ let ring = geometry.offsets[update.row];
917
+ for (const size of update.geometry.ringSizes) {
918
+ geometry.ringOffsets[ring] = vertex;
919
+ accounting.bytes += 4;
920
+ vertex += size;
921
+ ring += 1;
922
+ }
923
+ }
924
+ if (geometry.validity) {
925
+ writeValidityBit(descriptors.get(`${geometry.field}.validity`), update.row, !update.geometry.isNull, accounting);
926
+ }
927
+ }
928
+ if (update.timestamp !== undefined && inspection.temporal) {
929
+ inspection.temporal.values[update.row] = update.timestamp ?? 0n;
930
+ accounting.bytes += 8;
931
+ if (inspection.temporal.validity) {
932
+ writeValidityBit(descriptors.get(`${inspection.temporal.field}.validity`), update.row, update.timestamp !== null, accounting);
933
+ }
934
+ }
935
+ if (update.dictionaryValue !== undefined && inspection.dictionary) {
936
+ inspection.dictionary.indices[update.row] = update.dictionaryIndex ?? 0;
937
+ accounting.bytes += 4;
938
+ if (inspection.dictionary.validity) {
939
+ writeValidityBit(descriptors.get(`${inspection.dictionary.field}.validity`), update.row, update.dictionaryValue !== null, accounting);
940
+ }
941
+ }
942
+ }
943
+ if (appended === 0)
944
+ return { buffers: [...descriptors.values()], vertices: layout.vertices, rings: layout.rings };
945
+ // Appends: written past every declared slice, then the slices are grown.
946
+ let vertex = layout.vertices;
947
+ let ring = layout.rings;
948
+ const offsetsDescriptor = geometry.kind === "point" ? undefined : descriptors.get(`${geometry.field}.offsets`);
949
+ const offsets = offsetsDescriptor === undefined
950
+ ? undefined
951
+ : new Int32Array(offsetsDescriptor.data, offsetsDescriptor.byteOffset, rows + appended + 1);
952
+ const ringOffsetsDescriptor = geometry.kind === "polygon" ? descriptors.get(`${geometry.field}.ring-offsets`) : undefined;
953
+ const ringOffsets = ringOffsetsDescriptor === undefined
954
+ ? undefined
955
+ : new Int32Array(ringOffsetsDescriptor.data, ringOffsetsDescriptor.byteOffset, ring + plan.appendedRings + 1);
956
+ const temporalDescriptor = inspection.temporal === undefined ? undefined : descriptors.get(`${inspection.temporal.field}.values`);
957
+ const temporalValues = temporalDescriptor === undefined
958
+ ? undefined
959
+ : new BigInt64Array(temporalDescriptor.data, temporalDescriptor.byteOffset, rows + appended);
960
+ const dictionaryDescriptor = inspection.dictionary === undefined ? undefined : descriptors.get(`${inspection.dictionary.field}.indices`);
961
+ const dictionaryIndices = dictionaryDescriptor === undefined
962
+ ? undefined
963
+ : new Int32Array(dictionaryDescriptor.data, dictionaryDescriptor.byteOffset, rows + appended);
964
+ const featureIdDescriptor = descriptors.get(`${inspection.featureIds.field}.values`);
965
+ const featureIdValues = new Uint32Array(featureIdDescriptor.data, featureIdDescriptor.byteOffset, rows + appended);
966
+ plan.appends.forEach((append, offset) => {
967
+ const row = rows + offset;
968
+ if (offsets) {
969
+ offsets[row + 1] = geometry.kind === "polygon" ? ring + append.geometry.rings : vertex + append.geometry.vertices;
970
+ accounting.bytes += 4;
971
+ }
972
+ if (ringOffsets) {
973
+ let ringVertex = vertex;
974
+ for (const size of append.geometry.ringSizes) {
975
+ ringOffsets[ring] = ringVertex;
976
+ accounting.bytes += 4;
977
+ ringVertex += size;
978
+ ring += 1;
979
+ }
980
+ ringOffsets[ring] = ringVertex;
981
+ accounting.bytes += 4;
982
+ }
983
+ writePositions(layout, vertex, append.geometry.positions, accounting);
984
+ vertex += append.geometry.vertices;
985
+ if (geometry.validity) {
986
+ writeValidityBit(descriptors.get(`${geometry.field}.validity`), row, !append.geometry.isNull, accounting);
987
+ }
988
+ if (temporalValues) {
989
+ temporalValues[row] = append.timestamp ?? 0n;
990
+ accounting.bytes += 8;
991
+ if (inspection.temporal.validity) {
992
+ writeValidityBit(descriptors.get(`${inspection.temporal.field}.validity`), row, append.timestamp !== null, accounting);
993
+ }
994
+ }
995
+ if (dictionaryIndices) {
996
+ dictionaryIndices[row] = append.dictionaryIndex ?? 0;
997
+ accounting.bytes += 4;
998
+ if (inspection.dictionary.validity) {
999
+ writeValidityBit(descriptors.get(`${inspection.dictionary.field}.validity`), row, append.dictionaryValue !== null, accounting);
1000
+ }
1001
+ }
1002
+ featureIdValues[row] = append.featureId;
1003
+ accounting.bytes += 4;
1004
+ });
1005
+ const newRows = rows + appended;
1006
+ if (geometry.validity)
1007
+ grow(`${geometry.field}.validity`, Math.ceil(newRows / 8));
1008
+ if (offsets)
1009
+ grow(`${geometry.field}.offsets`, (newRows + 1) * 4);
1010
+ if (ringOffsets)
1011
+ grow(`${geometry.field}.ring-offsets`, (ring + 1) * 4);
1012
+ if (geometry.coordinateLayout === "interleaved") {
1013
+ grow(`${geometry.field}.coordinates`, vertex * names.length * 8);
1014
+ }
1015
+ else
1016
+ for (const name of names)
1017
+ grow(`${geometry.field}.${name}`, vertex * 8);
1018
+ if (inspection.temporal) {
1019
+ grow(`${inspection.temporal.field}.values`, newRows * 8);
1020
+ if (inspection.temporal.validity)
1021
+ grow(`${inspection.temporal.field}.validity`, Math.ceil(newRows / 8));
1022
+ }
1023
+ if (inspection.dictionary) {
1024
+ grow(`${inspection.dictionary.field}.indices`, newRows * 4);
1025
+ if (inspection.dictionary.validity)
1026
+ grow(`${inspection.dictionary.field}.validity`, Math.ceil(newRows / 8));
1027
+ }
1028
+ grow(`${inspection.featureIds.field}.values`, newRows * 4);
1029
+ return { buffers: [...descriptors.values()], vertices: vertex, rings: ring };
1030
+ }
1031
+ function writePositions(layout, vertex, positions, accounting) {
1032
+ const geometry = layout.inspection.geometry;
1033
+ const names = layout.names;
1034
+ const descriptor = geometry.coordinateLayout === "interleaved" ? layout.descriptors.get(`${geometry.field}.coordinates`) : undefined;
1035
+ if (descriptor) {
1036
+ const values = new Float64Array(descriptor.data, descriptor.byteOffset);
1037
+ positions.forEach((position, offset) => {
1038
+ const base = (vertex + offset) * names.length;
1039
+ for (let dimension = 0; dimension < names.length; dimension += 1)
1040
+ values[base + dimension] = position[dimension];
1041
+ accounting.bytes += names.length * 8;
1042
+ });
1043
+ return;
1044
+ }
1045
+ names.forEach((name, dimension) => {
1046
+ const buffer = layout.descriptors.get(`${geometry.field}.${name}`);
1047
+ const values = new Float64Array(buffer.data, buffer.byteOffset);
1048
+ positions.forEach((position, offset) => {
1049
+ values[vertex + offset] = position[dimension];
1050
+ accounting.bytes += 8;
1051
+ });
1052
+ });
1053
+ }
1054
+ /** Live source-row runs, split at every updated row so a run is a pure copy. */
1055
+ function rebuildItems(rows, tombstones, updatesByRow) {
1056
+ const items = [];
1057
+ let start = -1;
1058
+ for (let row = 0; row < rows; row += 1) {
1059
+ if (tombstones.has(row)) {
1060
+ if (start !== -1)
1061
+ items.push({ kind: "copy", start, end: row });
1062
+ start = -1;
1063
+ continue;
1064
+ }
1065
+ if (updatesByRow.has(row)) {
1066
+ if (start !== -1)
1067
+ items.push({ kind: "copy", start, end: row });
1068
+ items.push({ kind: "row", start: row, end: row + 1 });
1069
+ start = -1;
1070
+ continue;
1071
+ }
1072
+ if (start === -1)
1073
+ start = row;
1074
+ }
1075
+ if (start !== -1)
1076
+ items.push({ kind: "copy", start, end: rows });
1077
+ return items;
1078
+ }
1079
+ /**
1080
+ * Compact the live rows into fresh buffers, applying updates as they are
1081
+ * written and appending new rows at the end. The pass copies packed buffer
1082
+ * slices; it never materializes a source row as an object, so peak transient
1083
+ * memory stays proportional to the output payload rather than to the row count.
1084
+ */
1085
+ function rebuild(layout, state, plan, patch, options, limits) {
1086
+ const inspection = layout.inspection;
1087
+ const batch = inspection.batch;
1088
+ const items = rebuildItems(layout.rows, plan.tombstones, plan.updatesByRow);
1089
+ const reserve = resolveReserve(options.rebuild?.reserve ?? state.reserve, layout.kind);
1090
+ const dictionaryByValue = plan.newDictionaryValues.length === 0 ? undefined : decodeDictionaryValues(inspection);
1091
+ const dictionaryAdditions = new Map();
1092
+ if (dictionaryByValue) {
1093
+ let next = inspection.dictionary.offsets.length - 1;
1094
+ for (const value of plan.newDictionaryValues) {
1095
+ if (!dictionaryByValue.has(value)) {
1096
+ dictionaryAdditions.set(value, next);
1097
+ next += 1;
1098
+ }
1099
+ }
1100
+ }
1101
+ const resolveDictionary = (value, planned) => {
1102
+ if (value === null || value === undefined)
1103
+ return planned ?? 0;
1104
+ return planned ?? dictionaryByValue?.get(value) ?? dictionaryAdditions.get(value) ?? 0;
1105
+ };
1106
+ // Pass one: sizes.
1107
+ let rows = 0;
1108
+ let vertices = 0;
1109
+ let rings = 0;
1110
+ for (const item of items) {
1111
+ if (item.kind === "copy") {
1112
+ rows += item.end - item.start;
1113
+ const span = spanOf(inspection, item.start, item.end);
1114
+ vertices += span.vertices;
1115
+ rings += span.rings;
1116
+ continue;
1117
+ }
1118
+ rows += 1;
1119
+ const update = plan.updatesByRow.get(item.start);
1120
+ const span = update.geometry ?? rowVertexSpan(inspection, item.start);
1121
+ vertices += span.vertices;
1122
+ rings += span.rings;
1123
+ }
1124
+ rows += plan.appends.length;
1125
+ vertices += plan.appendedVertices;
1126
+ rings += plan.appendedRings;
1127
+ const geometry = inspection.geometry;
1128
+ const names = layout.names;
1129
+ const buffers = [];
1130
+ let payloadBytes = 0;
1131
+ let backingBytes = 0;
1132
+ const allocate = (id, role, field, payload, extra) => {
1133
+ const data = new ArrayBuffer(payload + extra);
1134
+ payloadBytes += payload;
1135
+ backingBytes += payload + extra;
1136
+ const descriptor = { id, role, field, data, byteOffset: 0, byteLength: payload };
1137
+ buffers.push(descriptor);
1138
+ return descriptor;
1139
+ };
1140
+ const validityBytes = Math.ceil(rows / 8);
1141
+ const reservedValidityBytes = Math.ceil((rows + reserve.rows) / 8) - validityBytes;
1142
+ const geometryValidity = geometry.validity
1143
+ ? new Uint8Array(allocate(`${geometry.field}.validity`, "validity", geometry.field, validityBytes, reservedValidityBytes).data)
1144
+ : undefined;
1145
+ const offsets = geometry.kind === "point"
1146
+ ? undefined
1147
+ : new Int32Array(allocate(`${geometry.field}.offsets`, "offsets", geometry.field, (rows + 1) * 4, reserve.rows * 4).data);
1148
+ const ringOffsets = geometry.kind === "polygon"
1149
+ ? new Int32Array(allocate(`${geometry.field}.ring-offsets`, "offsets", geometry.field, (rings + 1) * 4, reserve.rings * 4)
1150
+ .data)
1151
+ : undefined;
1152
+ const interleaved = geometry.coordinateLayout === "interleaved"
1153
+ ? new Float64Array(allocate(`${geometry.field}.coordinates`, "geometry", geometry.field, vertices * names.length * 8, reserve.vertices * names.length * 8).data)
1154
+ : undefined;
1155
+ const separated = geometry.coordinateLayout === "separated"
1156
+ ? names.map((name) => new Float64Array(allocate(`${geometry.field}.${name}`, "geometry", geometry.field, vertices * 8, reserve.vertices * 8)
1157
+ .data))
1158
+ : undefined;
1159
+ const temporal = inspection.temporal;
1160
+ const temporalValidity = temporal?.validity
1161
+ ? new Uint8Array(allocate(`${temporal.field}.validity`, "validity", temporal.field, validityBytes, reservedValidityBytes).data)
1162
+ : undefined;
1163
+ const temporalValues = temporal
1164
+ ? new BigInt64Array(allocate(`${temporal.field}.values`, "values", temporal.field, rows * 8, reserve.rows * 8).data)
1165
+ : undefined;
1166
+ const dictionary = inspection.dictionary;
1167
+ const dictionaryValidity = dictionary?.validity
1168
+ ? new Uint8Array(allocate(`${dictionary.field}.validity`, "validity", dictionary.field, validityBytes, reservedValidityBytes)
1169
+ .data)
1170
+ : undefined;
1171
+ const dictionaryIndices = dictionary
1172
+ ? new Int32Array(allocate(`${dictionary.field}.indices`, "dictionary", dictionary.field, rows * 4, reserve.rows * 4).data)
1173
+ : undefined;
1174
+ const featureIds = inspection.featureIds;
1175
+ const featureIdValues = new Uint32Array(allocate(`${featureIds.field}.values`, "values", featureIds.field, rows * 4, reserve.rows * 4).data);
1176
+ if (dictionary) {
1177
+ const encoder = new TextEncoder();
1178
+ const additions = [...dictionaryAdditions.keys()];
1179
+ const additionBytes = additions.reduce((total, value) => total + utf8Length(value), 0);
1180
+ const dictionaryOffsets = new Int32Array(allocate(`${dictionary.field}.offsets`, "offsets", dictionary.field, (dictionary.offsets.length + additions.length) * 4, 0).data);
1181
+ const dictionaryValues = new Uint8Array(allocate(`${dictionary.field}.values`, "dictionary", dictionary.field, dictionary.values.length + additionBytes, 0).data);
1182
+ dictionaryOffsets.set(dictionary.offsets);
1183
+ dictionaryValues.set(dictionary.values);
1184
+ let offset = dictionary.values.length;
1185
+ additions.forEach((value, position) => {
1186
+ const written = encoder.encodeInto(value, dictionaryValues.subarray(offset));
1187
+ offset += written.written;
1188
+ dictionaryOffsets[dictionary.offsets.length + position] = offset;
1189
+ });
1190
+ }
1191
+ // Pass two: write.
1192
+ let row = 0;
1193
+ let vertex = 0;
1194
+ let ring = 0;
1195
+ let checked = 0;
1196
+ const writeRowGeometry = (planned) => {
1197
+ if (offsets)
1198
+ offsets[row + 1] = geometry.kind === "polygon" ? ring + planned.rings : vertex + planned.vertices;
1199
+ if (ringOffsets) {
1200
+ let ringVertex = vertex;
1201
+ for (const size of planned.ringSizes) {
1202
+ ringOffsets[ring] = ringVertex;
1203
+ ringVertex += size;
1204
+ ring += 1;
1205
+ }
1206
+ ringOffsets[ring] = ringVertex;
1207
+ }
1208
+ planned.positions.forEach((position, offset) => {
1209
+ if (interleaved) {
1210
+ const base = (vertex + offset) * names.length;
1211
+ for (let dimension = 0; dimension < names.length; dimension += 1) {
1212
+ interleaved[base + dimension] = position[dimension];
1213
+ }
1214
+ }
1215
+ else
1216
+ for (let dimension = 0; dimension < names.length; dimension += 1) {
1217
+ separated[dimension][vertex + offset] = position[dimension];
1218
+ }
1219
+ });
1220
+ vertex += planned.vertices;
1221
+ if (geometryValidity && !planned.isNull)
1222
+ geometryValidity[row >> 3] |= 1 << (row & 7);
1223
+ };
1224
+ for (const item of items) {
1225
+ assertNotAborted(options.signal);
1226
+ if (item.kind === "copy") {
1227
+ const span = spanOf(inspection, item.start, item.end);
1228
+ copyCoordinates(inspection, names, interleaved, separated, span.startVertex, span.vertices, vertex);
1229
+ for (let source = item.start; source < item.end; source += 1) {
1230
+ const sourceSpan = rowVertexSpan(inspection, source);
1231
+ if (offsets) {
1232
+ offsets[row + 1] = geometry.kind === "polygon" ? ring + sourceSpan.rings : vertex + sourceSpan.vertices;
1233
+ }
1234
+ if (ringOffsets) {
1235
+ const firstRing = geometry.offsets[source];
1236
+ for (let index = 0; index < sourceSpan.rings; index += 1) {
1237
+ ringOffsets[ring + index] =
1238
+ vertex + (geometry.ringOffsets[firstRing + index] - geometry.ringOffsets[firstRing]);
1239
+ }
1240
+ ring += sourceSpan.rings;
1241
+ ringOffsets[ring] = vertex + sourceSpan.vertices;
1242
+ }
1243
+ if (geometryValidity && bitSet(geometry.validity, source))
1244
+ geometryValidity[row >> 3] |= 1 << (row & 7);
1245
+ if (temporalValues) {
1246
+ temporalValues[row] = temporal.values[source];
1247
+ if (temporalValidity && bitSet(temporal.validity, source))
1248
+ temporalValidity[row >> 3] |= 1 << (row & 7);
1249
+ }
1250
+ if (dictionaryIndices) {
1251
+ dictionaryIndices[row] = dictionary.indices[source];
1252
+ if (dictionaryValidity && bitSet(dictionary.validity, source))
1253
+ dictionaryValidity[row >> 3] |= 1 << (row & 7);
1254
+ }
1255
+ featureIdValues[row] = featureIds.values[source];
1256
+ vertex += sourceSpan.vertices;
1257
+ row += 1;
1258
+ checked += 1;
1259
+ if (checked % COLUMNAR_PATCH_ABORT_CHECK_ROWS === 0)
1260
+ assertNotAborted(options.signal);
1261
+ }
1262
+ continue;
1263
+ }
1264
+ if (item.kind === "row") {
1265
+ const source = item.start;
1266
+ const update = plan.updatesByRow.get(source);
1267
+ if (update.geometry)
1268
+ writeRowGeometry(update.geometry);
1269
+ else {
1270
+ const sourceSpan = rowVertexSpan(inspection, source);
1271
+ const startVertex = geometry.kind === "point"
1272
+ ? source
1273
+ : geometry.kind === "linestring"
1274
+ ? geometry.offsets[source]
1275
+ : geometry.ringOffsets[geometry.offsets[source]];
1276
+ copyCoordinates(inspection, names, interleaved, separated, startVertex, sourceSpan.vertices, vertex);
1277
+ if (offsets)
1278
+ offsets[row + 1] = geometry.kind === "polygon" ? ring + sourceSpan.rings : vertex + sourceSpan.vertices;
1279
+ if (ringOffsets) {
1280
+ const firstRing = geometry.offsets[source];
1281
+ for (let index = 0; index < sourceSpan.rings; index += 1) {
1282
+ ringOffsets[ring + index] =
1283
+ vertex + (geometry.ringOffsets[firstRing + index] - geometry.ringOffsets[firstRing]);
1284
+ }
1285
+ ring += sourceSpan.rings;
1286
+ ringOffsets[ring] = vertex + sourceSpan.vertices;
1287
+ }
1288
+ if (geometryValidity && bitSet(geometry.validity, source))
1289
+ geometryValidity[row >> 3] |= 1 << (row & 7);
1290
+ vertex += sourceSpan.vertices;
1291
+ }
1292
+ if (temporalValues) {
1293
+ const value = update.timestamp === undefined ? temporal.values[source] : (update.timestamp ?? 0n);
1294
+ temporalValues[row] = value;
1295
+ const valid = update.timestamp === undefined ? bitSet(temporal.validity, source) : update.timestamp !== null;
1296
+ if (temporalValidity && valid)
1297
+ temporalValidity[row >> 3] |= 1 << (row & 7);
1298
+ }
1299
+ if (dictionaryIndices) {
1300
+ const value = update.dictionaryValue === undefined
1301
+ ? dictionary.indices[source]
1302
+ : resolveDictionary(update.dictionaryValue, update.dictionaryIndex);
1303
+ dictionaryIndices[row] = value;
1304
+ const valid = update.dictionaryValue === undefined ? bitSet(dictionary.validity, source) : update.dictionaryValue !== null;
1305
+ if (dictionaryValidity && valid)
1306
+ dictionaryValidity[row >> 3] |= 1 << (row & 7);
1307
+ }
1308
+ featureIdValues[row] = featureIds.values[source];
1309
+ row += 1;
1310
+ }
1311
+ }
1312
+ for (const append of plan.appends) {
1313
+ assertNotAborted(options.signal);
1314
+ writeRowGeometry(append.geometry);
1315
+ if (temporalValues) {
1316
+ temporalValues[row] = append.timestamp ?? 0n;
1317
+ if (temporalValidity && append.timestamp !== null)
1318
+ temporalValidity[row >> 3] |= 1 << (row & 7);
1319
+ }
1320
+ if (dictionaryIndices) {
1321
+ dictionaryIndices[row] = resolveDictionary(append.dictionaryValue, append.dictionaryIndex);
1322
+ if (dictionaryValidity && append.dictionaryValue !== null)
1323
+ dictionaryValidity[row >> 3] |= 1 << (row & 7);
1324
+ }
1325
+ featureIdValues[row] = append.featureId;
1326
+ row += 1;
1327
+ }
1328
+ const identity = options.rebuild?.identity ??
1329
+ (batch.identity === undefined
1330
+ ? undefined
1331
+ : {
1332
+ ...batch.identity,
1333
+ freshness: {
1334
+ ...batch.identity.freshness,
1335
+ observedAt: patch.cursor.observedAt,
1336
+ generation: String(patch.cursor.sequence),
1337
+ },
1338
+ });
1339
+ const metadata = patchMetadataFor({
1340
+ baseRows: rows,
1341
+ baseVertices: vertices,
1342
+ reserve,
1343
+ generation: 0,
1344
+ tombstones: "",
1345
+ cursor: patch.cursor,
1346
+ });
1347
+ return {
1348
+ batch: createColumnarBatch({
1349
+ id: options.rebuild?.batchId ?? `${batch.id}~${patch.cursor.sequence}`,
1350
+ schema: { ...batch.schema, metadata: withPatchMetadata(batch.schema.metadata, metadata) },
1351
+ rowCount: rows,
1352
+ sequence: batch.sequence,
1353
+ ...(identity === undefined ? {} : { identity }),
1354
+ buffers,
1355
+ }, limits),
1356
+ payloadBytes,
1357
+ backingBytes,
1358
+ };
1359
+ }
1360
+ function spanOf(inspection, start, end) {
1361
+ const geometry = inspection.geometry;
1362
+ if (geometry.kind === "point")
1363
+ return { startVertex: start, vertices: end - start, rings: 0 };
1364
+ const firstRing = geometry.offsets[start];
1365
+ const lastRing = geometry.offsets[end];
1366
+ if (geometry.kind === "linestring") {
1367
+ return { startVertex: firstRing, vertices: lastRing - firstRing, rings: 0 };
1368
+ }
1369
+ const startVertex = geometry.ringOffsets[firstRing];
1370
+ return { startVertex, vertices: geometry.ringOffsets[lastRing] - startVertex, rings: lastRing - firstRing };
1371
+ }
1372
+ function copyCoordinates(inspection, names, interleaved, separated, sourceVertex, vertices, targetVertex) {
1373
+ if (vertices === 0)
1374
+ return;
1375
+ const geometry = inspection.geometry;
1376
+ if (interleaved) {
1377
+ const source = geometry.coordinates.interleaved;
1378
+ interleaved.set(source.subarray(sourceVertex * names.length, (sourceVertex + vertices) * names.length), targetVertex * names.length);
1379
+ return;
1380
+ }
1381
+ names.forEach((name, dimension) => {
1382
+ separated[dimension].set(geometry.coordinates[name].subarray(sourceVertex, sourceVertex + vertices), targetVertex);
1383
+ });
1384
+ }
1385
+ /**
1386
+ * Threshold evaluation in one fixed order, so a patch that crosses two rules
1387
+ * always names the same one.
1388
+ */
1389
+ function evaluateThresholds(layout, state, plan, thresholds) {
1390
+ const rowCount = layout.rows + plan.appends.length;
1391
+ const tombstoneRatio = rowCount === 0 ? 0 : plan.tombstones.count / rowCount;
1392
+ if (tombstoneRatio > thresholds.maxTombstoneRatio)
1393
+ return { rebuild: true, reason: "tombstone-ratio" };
1394
+ if (utf8Length(plan.tombstones.encode()) > thresholds.maxTombstoneOverlayBytes) {
1395
+ return { rebuild: true, reason: "tombstone-overlay" };
1396
+ }
1397
+ if (plan.appends.length > 0) {
1398
+ if (plan.appends.length > state.capacity.rows ||
1399
+ plan.appendedVertices > state.capacity.vertices ||
1400
+ plan.appendedRings > state.capacity.rings) {
1401
+ return { rebuild: true, reason: "capacity" };
1402
+ }
1403
+ const consumedRows = rowCount - state.baseRows;
1404
+ const consumedVertices = layout.vertices + plan.appendedVertices - state.baseVertices;
1405
+ const rowUtilization = state.reserve.rows === 0 ? 1 : consumedRows / state.reserve.rows;
1406
+ const vertexUtilization = state.reserve.vertices === 0 ? (consumedVertices > 0 ? 1 : 0) : consumedVertices / state.reserve.vertices;
1407
+ if (Math.max(rowUtilization, vertexUtilization) > thresholds.maxCapacityUtilization) {
1408
+ return { rebuild: true, reason: "capacity" };
1409
+ }
1410
+ if (state.baseVertices > 0 &&
1411
+ (layout.vertices + plan.appendedVertices) / state.baseVertices > thresholds.maxVertexGrowthRatio) {
1412
+ return { rebuild: true, reason: "vertex-growth" };
1413
+ }
1414
+ }
1415
+ if (plan.layoutBlocked)
1416
+ return { rebuild: true, reason: "layout" };
1417
+ return { rebuild: false };
1418
+ }
1419
+ /**
1420
+ * Apply one realtime patch to a normative GeoArrow batch.
1421
+ *
1422
+ * The call either writes into the batch's existing backings, rebuilds a
1423
+ * compacted batch, or refuses the patch without touching a byte. See the module
1424
+ * documentation for the identity, tombstone, threshold, and determinism rules.
1425
+ */
1426
+ export function applyColumnarPatch(batch, patch, options = {}) {
1427
+ const telemetry = options.telemetry;
1428
+ if (telemetry === undefined)
1429
+ return applyPatch(batch, patch, options);
1430
+ const span = beginColumnarSpan(telemetry, "columnar-patch-apply", batch?.identity, patchSpanDetail(batch, patch));
1431
+ try {
1432
+ const outcome = applyPatch(batch, patch, options);
1433
+ span?.finish(patchOutcomeDetail(outcome));
1434
+ return outcome;
1435
+ }
1436
+ catch (error) {
1437
+ span?.fail(error, error instanceof HonuaColumnarPatchError ? { code: error.code } : undefined);
1438
+ throw error;
1439
+ }
1440
+ }
1441
+ /**
1442
+ * The patch span reports the realtime cursor and sequence so a live layer's
1443
+ * update rate is observable, plus the operation count the patch declares. Every
1444
+ * read is contained: an observed operation must behave identically to an
1445
+ * unobserved one even when the patch is malformed.
1446
+ */
1447
+ function patchSpanDetail(batch, patch) {
1448
+ try {
1449
+ const cursor = patch?.cursor;
1450
+ return {
1451
+ ...(typeof batch?.id === "string" ? { batchId: batch.id } : {}),
1452
+ ...(typeof cursor?.cursor === "string" ? { cursor: cursor.cursor } : {}),
1453
+ ...(typeof cursor?.sequence === "number" ? { sequence: cursor.sequence } : {}),
1454
+ ...(typeof cursor?.observedAt === "string" ? { observedAt: cursor.observedAt } : {}),
1455
+ ...(Array.isArray(patch?.operations) ? { operations: patch.operations.length } : {}),
1456
+ };
1457
+ }
1458
+ catch {
1459
+ return undefined;
1460
+ }
1461
+ }
1462
+ /**
1463
+ * A rejection is a returned outcome rather than a throw, so it settles the span
1464
+ * as a completed observation carrying its rejection code — the same
1465
+ * discriminant the caller sees.
1466
+ */
1467
+ function patchOutcomeDetail(outcome) {
1468
+ if (outcome.outcome === "rejected") {
1469
+ return { outcome: "rejected", code: outcome.code };
1470
+ }
1471
+ return {
1472
+ outcome: outcome.outcome,
1473
+ bufferIdentityPreserved: outcome.bufferIdentityPreserved,
1474
+ ...(outcome.outcome === "rebuilt" ? { reason: outcome.reason } : {}),
1475
+ appendedRows: outcome.metrics.appendedRows,
1476
+ updatedRows: outcome.metrics.updatedRows,
1477
+ deletedRows: outcome.metrics.deletedRows,
1478
+ rowsTouched: outcome.metrics.rowsTouched,
1479
+ payloadBytesCopied: outcome.metrics.payloadBytesCopied,
1480
+ backingBytesAllocated: outcome.metrics.backingBytesAllocated,
1481
+ tombstones: outcome.metrics.tombstones,
1482
+ liveRows: outcome.metrics.liveRows,
1483
+ };
1484
+ }
1485
+ function applyPatch(batch, patch, options) {
1486
+ if (typeof patch !== "object" || patch === null || patch.kind !== COLUMNAR_PATCH_KIND) {
1487
+ throw new HonuaColumnarPatchError("invalid-patch-state", `Patch must be a ${COLUMNAR_PATCH_KIND} value.`);
1488
+ }
1489
+ if (patch.version !== COLUMNAR_PATCH_VERSION) {
1490
+ throw new HonuaColumnarPatchError("invalid-patch-state", `Patch declares version "${patch.version}"; this build implements ${COLUMNAR_PATCH_VERSION}.`);
1491
+ }
1492
+ const limits = options.limits ?? {};
1493
+ const thresholds = resolveThresholds(options.thresholds);
1494
+ const progress = options.reportProgress;
1495
+ assertNotAborted(options.signal);
1496
+ progress?.(0.05, "inspect");
1497
+ const layout = readLayout(batch, limits);
1498
+ const state = readState(layout.inspection.batch, layout);
1499
+ if (patch.schemaId !== batch.schema.id) {
1500
+ return reject("schema-drift", `Patch targets schema "${patch.schemaId}"; the batch carries "${batch.schema.id}".`, batch, {
1501
+ expected: batch.schema.id,
1502
+ actual: patch.schemaId,
1503
+ });
1504
+ }
1505
+ if (patch.geometryKind !== layout.kind) {
1506
+ return reject("geometry-kind-drift", `Patch targets ${patch.geometryKind} geometry; the batch carries ${layout.kind}.`, batch, { expected: layout.kind, actual: patch.geometryKind });
1507
+ }
1508
+ if (state.sequence !== undefined) {
1509
+ if (patch.cursor.sequence === state.sequence) {
1510
+ return reject("duplicate-sequence", `Patch sequence ${patch.cursor.sequence} was already applied.`, batch, {
1511
+ sequence: patch.cursor.sequence,
1512
+ });
1513
+ }
1514
+ if (patch.cursor.sequence < state.sequence) {
1515
+ return reject("stale-sequence", `Patch sequence ${patch.cursor.sequence} is behind the applied sequence ${state.sequence}.`, batch, { sequence: patch.cursor.sequence, applied: state.sequence });
1516
+ }
1517
+ }
1518
+ progress?.(0.2, "plan");
1519
+ const plan = planPatch(batch, patch, layout, state);
1520
+ if ("outcome" in plan)
1521
+ return plan;
1522
+ assertNotAborted(options.signal);
1523
+ const decision = evaluateThresholds(layout, state, plan, thresholds);
1524
+ if (decision.rebuild && options.allowRebuild === false) {
1525
+ return reject("rebuild-required", `Applying this patch requires a rebuild (${decision.reason}) and rebuilds are disabled.`, batch, {
1526
+ reason: decision.reason,
1527
+ ...(plan.layoutBlockedReason === undefined ? {} : { detail: plan.layoutBlockedReason }),
1528
+ });
1529
+ }
1530
+ const rowsTouched = plan.appends.length + plan.updates.length + plan.deletes.length;
1531
+ if (decision.rebuild) {
1532
+ progress?.(0.4, "rebuild");
1533
+ const rebuilt = rebuild(layout, state, plan, patch, options, limits);
1534
+ const rebuiltState = inspectColumnarPatchState(rebuilt.batch, limits);
1535
+ progress?.(1, "complete");
1536
+ return Object.freeze({
1537
+ outcome: "rebuilt",
1538
+ batch: rebuilt.batch,
1539
+ state: rebuiltState,
1540
+ metrics: Object.freeze({
1541
+ appendedRows: plan.appends.length,
1542
+ updatedRows: plan.updates.length,
1543
+ deletedRows: plan.deletes.length,
1544
+ rowsTouched,
1545
+ payloadBytesCopied: rebuilt.payloadBytes,
1546
+ metadataBytes: metadataByteLength(rebuilt.batch),
1547
+ backingBytesAllocated: rebuilt.backingBytes,
1548
+ tombstones: 0,
1549
+ liveRows: rebuilt.batch.rowCount,
1550
+ }),
1551
+ bufferIdentityPreserved: false,
1552
+ reason: decision.reason,
1553
+ });
1554
+ }
1555
+ progress?.(0.4, "apply");
1556
+ const accounting = { bytes: 0 };
1557
+ const written = applyInPlace(layout, plan, accounting);
1558
+ const rowCount = layout.rows + plan.appends.length;
1559
+ const metadata = patchMetadataFor({
1560
+ baseRows: state.baseRows,
1561
+ baseVertices: state.baseVertices,
1562
+ reserve: { rows: state.reserve.rows, vertices: state.reserve.vertices, rings: state.reserve.rings },
1563
+ generation: state.generation + 1,
1564
+ tombstones: plan.tombstones.encode(),
1565
+ cursor: patch.cursor,
1566
+ });
1567
+ const identity = batch.identity === undefined
1568
+ ? undefined
1569
+ : {
1570
+ ...batch.identity,
1571
+ freshness: {
1572
+ ...batch.identity.freshness,
1573
+ observedAt: patch.cursor.observedAt,
1574
+ generation: String(patch.cursor.sequence),
1575
+ },
1576
+ };
1577
+ const patched = createColumnarBatch({
1578
+ id: batch.id,
1579
+ schema: { ...batch.schema, metadata: withPatchMetadata(batch.schema.metadata, metadata) },
1580
+ rowCount,
1581
+ sequence: batch.sequence,
1582
+ ...(batch.rowOffset === undefined ? {} : { rowOffset: batch.rowOffset }),
1583
+ ...(identity === undefined ? {} : { identity }),
1584
+ buffers: written.buffers,
1585
+ }, limits);
1586
+ // The patched batch is the input layout with grown slices, so its state is
1587
+ // derived rather than re-validated: one patch pays for one inspection.
1588
+ const patchedState = buildState(patched.schema.metadata ?? {}, { ...layout, rows: rowCount, descriptors: descriptorMap(patched) }, written.vertices, written.rings);
1589
+ progress?.(1, "complete");
1590
+ return Object.freeze({
1591
+ outcome: "patched-in-place",
1592
+ batch: patched,
1593
+ state: patchedState,
1594
+ metrics: Object.freeze({
1595
+ appendedRows: plan.appends.length,
1596
+ updatedRows: plan.updates.length,
1597
+ deletedRows: plan.deletes.length,
1598
+ rowsTouched,
1599
+ payloadBytesCopied: accounting.bytes,
1600
+ metadataBytes: metadataByteLength(patched),
1601
+ backingBytesAllocated: 0,
1602
+ tombstones: patchedState.tombstoneCount,
1603
+ liveRows: patchedState.liveRowCount,
1604
+ }),
1605
+ bufferIdentityPreserved: true,
1606
+ });
1607
+ }
1608
+ function metadataByteLength(batch) {
1609
+ const metadata = batch.schema.metadata ?? {};
1610
+ let bytes = 0;
1611
+ for (const key of Object.values(META)) {
1612
+ const value = metadata[key];
1613
+ if (value !== undefined)
1614
+ bytes += utf8Length(key) + utf8Length(value);
1615
+ }
1616
+ return bytes;
1617
+ }
1618
+ /**
1619
+ * Register realtime patch application as a columnar worker operation, so it
1620
+ * runs off the main thread under the existing session lifecycle.
1621
+ *
1622
+ * A rejection has no batch to return, so it is raised as
1623
+ * {@link HonuaColumnarPatchError}. The worker host deliberately does not
1624
+ * forward an operation's message across the boundary, so the caller sees
1625
+ * `operation-failed` without the rejection code: call
1626
+ * {@link applyColumnarPatch} directly when the code has to be observable.
1627
+ *
1628
+ * Buffer identity is preserved inside the worker, but a worker round trip
1629
+ * transfers ownership in both directions, so a renderer bound on the main
1630
+ * thread rebinds either way. Apply patches on the thread that owns the binding
1631
+ * when preserving that binding is the point.
1632
+ */
1633
+ export function createColumnarPatchOperation(options) {
1634
+ if (typeof options !== "object" || options === null) {
1635
+ throw new HonuaColumnarPatchError("invalid-patch-state", "Columnar patch operation options must be an object.");
1636
+ }
1637
+ if (typeof options.patch !== "object" && typeof options.patch !== "function") {
1638
+ throw new HonuaColumnarPatchError("invalid-patch-state", "Columnar patch operation requires a patch or provider.");
1639
+ }
1640
+ return async (batch, context) => {
1641
+ const patch = typeof options.patch === "function" ? await options.patch(batch, context) : options.patch;
1642
+ if (context.signal.aborted)
1643
+ throw new DOMException("The operation was aborted", "AbortError");
1644
+ const outcome = applyColumnarPatch(batch, patch, {
1645
+ ...options,
1646
+ signal: context.signal,
1647
+ reportProgress: (fraction, stage) => {
1648
+ context.reportProgress(fraction, stage);
1649
+ },
1650
+ });
1651
+ if (outcome.outcome === "rejected") {
1652
+ throw new HonuaColumnarPatchError(outcome.code, outcome.message, outcome.detail);
1653
+ }
1654
+ return outcome.batch;
1655
+ };
1656
+ }