├── .gitignore ├── LICENSE ├── README.md ├── index.test.mjs ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Tom MacWright 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # memory-geojson (experimental 🧪) 2 | 3 | A memory-efficient GeoJSON representation. 4 | 5 | This is not a new format. It's not meant to be serialized, and it doesn't 6 | add any features on top of the GeoJSON format. 7 | 8 | What it attempts to do is provide an in-memory representation of GeoJSON 9 | that uses TypedArrays to store flattened coordinates. The main benefits 10 | and goals are: 11 | 12 | - Reduce memory requirements of GeoJSON data. 13 | - Support [transferrable](https://developer.mozilla.org/en-US/docs/Glossary/Transferable_objects) or 14 | [shared](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SharedArrayBuffer) 15 | array buffers which make communication with WebWorkers much 16 | faster. 17 | 18 | The GeoJSON format is almost perfect, but the way it represents 19 | coordinates with nested arrays can be a performance issue. This 20 | is an experiment to support flattened arrays. 21 | 22 | Heavy inspiration taken from [mapshaper](https://github.com/mbloch/mapshaper) 23 | perhaps the only tool that I know of that has a strategy of 24 | flattening those arrays. 25 | 26 | ## Storage scheme 27 | 28 | _This might change, I'm just braindumping what's in the code right now_. 29 | 30 | This takes GeoJSON as input and stores it in three objects: 31 | 32 | 1. An array of objects called "featureProperties". This is where the "properties" 33 | data goes, as well as any data like bounding boxes or arbitrary data 34 | attached to the Feature object. 35 | 2. A Uint32Array of indexes, which informs the reader of the types of geometries 36 | and lengths of coordinate rings. 37 | 3. A Float64Array of coordinates. 38 | 3. A Uint32Array of lookup offsets into the indexes & coordinates arrays. 39 | 40 | Basically it's an offset based system. You read the file from 41 | the start, and let's say the first feature has a Point geometry. 42 | Point has a geometry code of 0, which informs the reader to 43 | read the first 3 numbers from the coordinate array and move on. 44 | 45 | If the next geometry is a LineString (code 2), then the reader 46 | reads the next number from the indexes array, which contains 47 | the number of coordinates in the linestring. Given that information, 48 | it reads that number of coordinates and produces a LineString geometry. 49 | 50 | ## Discussion 51 | 52 | This schema has tradeoffs. 53 | 54 | - ~~Seeking is difficult, there is currently no affordance for 55 | randomly jumping to and extracting a geometry. It's not entirely 56 | clear that this is necessary - skipping would be simple to implement. 57 | However, something like an index-of-indexes could be constructed.~~ 58 | Seeking to a specific feature is implemented! 59 | - ~~It's not clear yet how to encode the z index, the 3rd item in a 60 | GeoJSON Position. Right now this defaults that 3rd item to 0, but 61 | that is not ideal: a coordinate with z=0 is not the same as a coordinate 62 | with no z value. The latter implies that the z value is unknown, not 0.~~ 63 | z indexes are encoded as NaN. 64 | - Could it be done with just one array, instead of separate 65 | arrays for indexes and coordinates? 66 | - Should coordinates be Float32? I suspect that, while this would make 67 | encoding lossy (JavaScript floats are 52-bit), it would easily satisfy 68 | geospatial accuracy needs while halving the space requirement. 69 | - Some data updates in this format would be very expensive, and also 70 | updates would require some fairly custom operations. For example, 71 | adding a new coordinate in the middle of a line, in the a feature 72 | in the middle of the dataset would require, probably, 73 | intelligently updating the LineString length, slicing the dataset 74 | into two TypedArrays, and plopping the new coordinate 75 | in the middle. It's doable, but makes updates much less 76 | obvious. 77 | - Is it useful, or beneficial, to get fancy with properties? GeoJSON 78 | files certainly tend to share property names and often values, so 79 | it's conceivable that a bunch of features with a value for a property 80 | like "x" could have their values of "x" encoded as a flat array, 81 | hence saving valuable object space. But doing this well and not 82 | accidentally _increasing_ the memory requirements of some datasets 83 | seems like it would require compression-like logic. 84 | 85 | ## Running it 86 | 87 | This repo is using Node's built-in test framework (as of Node v18). 88 | So, have Node v18 and run `node --test`. No deps are required so far. 89 | 90 | ## Future 91 | 92 | The future of this would be to use it in [Placemark](https://www.placemark.io/), 93 | which would benefit from a more efficient memory encoding of features. 94 | 95 | ## Prior art 96 | 97 | - [mapshaper](https://github.com/mbloch/mapshaper) 98 | - [visgl geojsonToBinary](https://github.com/visgl/loaders.gl/blob/master/modules/gis/docs/api-reference/geojson-to-binary.md) 99 | - [geo-arrow-spec](https://github.com/geopandas/geo-arrow-spec/blob/main/format.md) 100 | -------------------------------------------------------------------------------- /index.test.mjs: -------------------------------------------------------------------------------- 1 | import { describe, test } from "node:test"; 2 | import assert from "assert"; 3 | 4 | const input = [ 5 | { 6 | type: "Feature", 7 | properties: { 8 | x: 1, 9 | }, 10 | geometry: null, 11 | }, 12 | { 13 | type: "Feature", 14 | properties: { 15 | x: 1, 16 | }, 17 | geometry: { 18 | type: "MultiPolygon", 19 | coordinates: [ 20 | [ 21 | [ 22 | [1, 2, 0], 23 | [3, 4, 0], 24 | [1, 2, 0], 25 | ], 26 | [ 27 | [8, 7, 0], 28 | [2, 7, 0], 29 | [8, 7, 0], 30 | ], 31 | ], 32 | ], 33 | }, 34 | }, 35 | { 36 | type: "Feature", 37 | properties: { 38 | x: 1, 39 | }, 40 | geometry: { 41 | type: "Point", 42 | coordinates: [42.32, 24.2, 20], 43 | }, 44 | }, 45 | { 46 | type: "Feature", 47 | properties: { 48 | x: 1, 49 | }, 50 | geometry: { 51 | type: "GeometryCollection", 52 | geometries: [ 53 | { 54 | type: "Point", 55 | coordinates: [42.32, 24.2], 56 | }, 57 | ], 58 | }, 59 | }, 60 | { 61 | type: "Feature", 62 | properties: { 63 | x: 1, 64 | }, 65 | geometry: { 66 | type: "Point", 67 | coordinates: [42.32, 24.2, 0], 68 | }, 69 | }, 70 | { 71 | type: "Feature", 72 | properties: { 73 | x: 1, 74 | }, 75 | geometry: { 76 | type: "LineString", 77 | coordinates: [ 78 | [1, 2, 0], 79 | [2, 3, 0], 80 | [3, 4, 0], 81 | ], 82 | }, 83 | }, 84 | { 85 | type: "Feature", 86 | properties: { 87 | x: 1, 88 | }, 89 | geometry: { 90 | type: "MultiLineString", 91 | coordinates: [ 92 | [ 93 | [1, 2, 0], 94 | [3, 4, 0], 95 | ], 96 | [ 97 | [8, 7, 0], 98 | [8, 7, 0], 99 | ], 100 | ], 101 | }, 102 | }, 103 | ]; 104 | 105 | /** 106 | * Codes for each of the geometry types. 107 | */ 108 | const GEOMETRY_TYPES = { 109 | Point: 0, 110 | MultiPoint: 1, 111 | LineString: 2, 112 | MultiLineString: 3, 113 | Polygon: 4, 114 | MultiPolygon: 5, 115 | GeometryCollection: 6, 116 | None: 7, 117 | }; 118 | 119 | const GEOMETRY_TYPES_INVERT = Object.fromEntries( 120 | Object.entries(GEOMETRY_TYPES).map((entry) => entry.reverse()) 121 | ); 122 | 123 | function writeInt(typedArrayWrapper, index, val) { 124 | const typedArray = typedArrayWrapper._; 125 | const len = typedArray.length; 126 | if (index === len - 1) { 127 | const newArray = new Uint32Array(len * 2); 128 | newArray.set(typedArray); 129 | newArray[index] = val; 130 | typedArrayWrapper._ = newArray; 131 | } else { 132 | typedArray[index] = val; 133 | } 134 | } 135 | 136 | /** 137 | * Write a coordinate, a single number, to the array. 138 | * This will enlarge the array by a factor of 2 139 | * if it's too small. 140 | */ 141 | function writeCoordinate(typedArrayWrapper, index, val) { 142 | const typedArray = typedArrayWrapper._; 143 | const len = typedArray.length; 144 | if (index === len - 4) { 145 | const newArray = new Float64Array(len * 2); 146 | newArray.set(typedArray); 147 | newArray[index] = val; 148 | typedArrayWrapper._ = newArray; 149 | } else { 150 | typedArray[index] = val; 151 | } 152 | } 153 | 154 | function writePosition(coordinateArray, coordinate, coordinateIndex) { 155 | writeCoordinate(coordinateArray, coordinateIndex++, coordinate[0]); 156 | writeCoordinate(coordinateArray, coordinateIndex++, coordinate[1]); 157 | writeCoordinate(coordinateArray, coordinateIndex++, coordinate[2] ?? NONE); 158 | return coordinateIndex; 159 | } 160 | 161 | /** 162 | * z values are optional in coordinates. To make 163 | * them work with strictly-typed arrays that don't support 164 | * null, they're stored as NaN. 165 | */ 166 | const NONE = NaN; 167 | function isSome(value) { 168 | return !isNaN(value); 169 | } 170 | 171 | function toMemory(features) { 172 | /** 173 | * These arrays are wrapped in objects so they can be 174 | * expanded when necessary. The object provides interior 175 | * mutability - TypedArrays can't be resized in place, 176 | * they need to be copied. 177 | * 178 | * NOTE: maybe we can estimate better how big to make 179 | * these initially, based on the number of features. 180 | */ 181 | const coordinateArray = { _: new Float64Array(8) }; 182 | /** 183 | * Counts and identifiers. This contains 184 | * the geometry type field, the counts of rings, 185 | * coordinates. 186 | */ 187 | const indexes = { _: new Uint32Array(8) }; 188 | const lookup = { _: new Uint32Array(8) }; 189 | 190 | let indexIndex = 0; 191 | let coordinateIndex = 0; 192 | let simpleFeatures = []; 193 | 194 | function writeGeometry(geometry) { 195 | if (!geometry) { 196 | writeInt(indexes, indexIndex++, GEOMETRY_TYPES.None); 197 | return; 198 | } 199 | 200 | writeInt(indexes, indexIndex++, GEOMETRY_TYPES[geometry.type]); 201 | 202 | switch (geometry.type) { 203 | case "GeometryCollection": { 204 | writeInt(indexes, indexIndex++, geometry.geometries.length); 205 | for (let geom of geometry.geometries) { 206 | writeGeometry(geom); 207 | } 208 | break; 209 | } 210 | case "Point": { 211 | const coordinate = geometry.coordinates; 212 | coordinateIndex = writePosition( 213 | coordinateArray, 214 | coordinate, 215 | coordinateIndex 216 | ); 217 | break; 218 | } 219 | case "MultiPoint": 220 | case "LineString": { 221 | writeInt(indexes, indexIndex++, geometry.coordinates.length); 222 | for (let coordinate of geometry.coordinates) { 223 | coordinateIndex = writePosition( 224 | coordinateArray, 225 | coordinate, 226 | coordinateIndex 227 | ); 228 | } 229 | break; 230 | } 231 | case "MultiLineString": 232 | case "Polygon": { 233 | writeInt(indexes, indexIndex++, geometry.coordinates.length); 234 | for (let ring of geometry.coordinates) { 235 | writeInt(indexes, indexIndex++, ring.length); 236 | for (let coordinate of ring) { 237 | coordinateIndex = writePosition( 238 | coordinateArray, 239 | coordinate, 240 | coordinateIndex 241 | ); 242 | } 243 | } 244 | break; 245 | } 246 | case "MultiPolygon": { 247 | writeInt(indexes, indexIndex++, geometry.coordinates.length); 248 | for (let polygon of geometry.coordinates) { 249 | writeInt(indexes, indexIndex++, polygon.length); 250 | for (let ring of polygon) { 251 | writeInt(indexes, indexIndex++, ring.length); 252 | for (let coordinate of ring) { 253 | coordinateIndex = writePosition( 254 | coordinateArray, 255 | coordinate, 256 | coordinateIndex 257 | ); 258 | } 259 | } 260 | } 261 | break; 262 | } 263 | } 264 | } 265 | 266 | let lookupI = 0; 267 | for (let i = 0; i < features.length; i++) { 268 | writeInt(lookup, lookupI++, indexIndex); 269 | writeInt(lookup, lookupI++, coordinateIndex); 270 | const feature = features[i]; 271 | writeGeometry(feature.geometry); 272 | const { geometry, type, ...rest } = feature; 273 | simpleFeatures.push(rest); 274 | } 275 | 276 | return { 277 | coordinateArray: coordinateArray._.subarray(0, coordinateIndex), 278 | indexes: indexes._.subarray(0, indexIndex), 279 | lookup: lookup._.subarray(0, lookupI), 280 | featureProperties: simpleFeatures, 281 | }; 282 | } 283 | 284 | function getCoordinates(coordinateIndex, coordinateArray) { 285 | if (true) { 286 | const coordinates = [ 287 | coordinateArray[coordinateIndex++], 288 | coordinateArray[coordinateIndex++], 289 | ]; 290 | const z = coordinateArray[coordinateIndex++]; 291 | if (isSome(z)) coordinates.push(z); 292 | return [coordinates, coordinateIndex]; 293 | } else { 294 | const z = coordinateArray[coordinateIndex + 2]; 295 | const size = isSome(z) ? 3 : 2; 296 | let coordinates = coordinateArray.slice( 297 | coordinateIndex, 298 | coordinateIndex + size 299 | ); 300 | coordinateIndex += 3; 301 | return [coordinates, coordinateIndex]; 302 | } 303 | } 304 | 305 | function decodeGeometry(indexIndex, indexes, coordinateIndex, coordinateArray) { 306 | const geometryCode = indexes[indexIndex++]; 307 | let geometryType = GEOMETRY_TYPES_INVERT[geometryCode]; 308 | let coordinates; 309 | 310 | if (geometryType === undefined) { 311 | throw new Error(`Unexpected geometry type code ${geometryCode}`); 312 | } 313 | 314 | switch (geometryType) { 315 | case "None": { 316 | return [indexIndex, coordinateIndex, null]; 317 | } 318 | case "GeometryCollection": { 319 | const len = indexes[indexIndex++]; 320 | const geometries = []; 321 | 322 | for (let i = 0; i < len; i++) { 323 | let geometry; 324 | [indexIndex, coordinateIndex, geometry] = decodeGeometry( 325 | indexIndex, 326 | indexes, 327 | coordinateIndex, 328 | coordinateArray 329 | ); 330 | geometries.push(geometry); 331 | } 332 | 333 | return [ 334 | indexIndex, 335 | coordinateIndex, 336 | { 337 | type: "GeometryCollection", 338 | geometries, 339 | }, 340 | ]; 341 | } 342 | case "Point": { 343 | [coordinates, coordinateIndex] = getCoordinates( 344 | coordinateIndex, 345 | coordinateArray 346 | ); 347 | break; 348 | } 349 | case "MultiPoint": 350 | case "LineString": { 351 | const len = indexes[indexIndex++]; 352 | coordinates = []; 353 | for (let i = 0; i < len; i++) { 354 | let position; 355 | [position, coordinateIndex] = getCoordinates( 356 | coordinateIndex, 357 | coordinateArray 358 | ); 359 | coordinates.push(position); 360 | } 361 | break; 362 | } 363 | case "Polygon": 364 | case "MultiLineString": { 365 | const ringLength = indexes[indexIndex++]; 366 | coordinates = []; 367 | for (let i = 0; i < ringLength; i++) { 368 | const len = indexes[indexIndex++]; 369 | const ring = []; 370 | for (let i = 0; i < len; i++) { 371 | let position; 372 | [position, coordinateIndex] = getCoordinates( 373 | coordinateIndex, 374 | coordinateArray 375 | ); 376 | ring.push(position); 377 | } 378 | coordinates.push(ring); 379 | } 380 | break; 381 | } 382 | case "MultiPolygon": { 383 | const polygonLength = indexes[indexIndex++]; 384 | coordinates = []; 385 | for (let i = 0; i < polygonLength; i++) { 386 | const polygon = []; 387 | const ringLength = indexes[indexIndex++]; 388 | for (let j = 0; j < ringLength; j++) { 389 | const len = indexes[indexIndex++]; 390 | const ring = []; 391 | for (let i = 0; i < len; i++) { 392 | let position; 393 | [position, coordinateIndex] = getCoordinates( 394 | coordinateIndex, 395 | coordinateArray 396 | ); 397 | ring.push(position); 398 | } 399 | polygon.push(ring); 400 | } 401 | coordinates.push(polygon); 402 | } 403 | break; 404 | } 405 | } 406 | return [ 407 | indexIndex, 408 | coordinateIndex, 409 | { 410 | type: geometryType, 411 | coordinates, 412 | }, 413 | ]; 414 | } 415 | 416 | function arrayFromMemory({ coordinateArray, indexes, featureProperties }) { 417 | const features = []; 418 | 419 | let indexIndex = 0; 420 | let coordinateIndex = 0; 421 | 422 | for (let i = 0; i < featureProperties.length; i++) { 423 | let geometry; 424 | [indexIndex, coordinateIndex, geometry] = decodeGeometry( 425 | indexIndex, 426 | indexes, 427 | coordinateIndex, 428 | coordinateArray 429 | ); 430 | features.push({ 431 | ...featureProperties[i], 432 | type: "Feature", 433 | geometry, 434 | }); 435 | } 436 | 437 | return features; 438 | } 439 | 440 | function featureFromMemory( 441 | { coordinateArray, lookup, indexes, featureProperties }, 442 | i 443 | ) { 444 | let indexIndex = lookup[i * 2]; 445 | let coordinateIndex = lookup[i * 2 + 1]; 446 | 447 | let geometry; 448 | [indexIndex, coordinateIndex, geometry] = decodeGeometry( 449 | indexIndex, 450 | indexes, 451 | coordinateIndex, 452 | coordinateArray 453 | ); 454 | return { 455 | ...featureProperties[i], 456 | type: "Feature", 457 | geometry, 458 | }; 459 | } 460 | 461 | function deleteFeature( 462 | { coordinateArray, lookup, indexes, featureProperties }, 463 | i 464 | ) { 465 | let indexIndex = lookup[i * 2]; 466 | let coordinateIndex = lookup[i * 2 + 1]; 467 | 468 | let nextIndexIndex = lookup[(i + 1) * 2]; 469 | let nextCoordinateIndex = lookup[(i + 1) * 2 + 1]; 470 | 471 | // Simple case - this is the last feature. 472 | if (nextIndexIndex === undefined) { 473 | return { 474 | lookup: lookup.subarray(0, i * 2), 475 | indexes: indexes.subarray(0, indexIndex), 476 | coordinateArray: coordinateArray.subarray(0, coordinateIndex), 477 | featureProperties: featureProperties.slice(0, i), 478 | }; 479 | } 480 | 481 | throw new Error("TODO"); 482 | } 483 | 484 | describe("memory-geojson", () => { 485 | test("round-trip", () => { 486 | const memory = toMemory(input); 487 | // console.log(memory); 488 | assert.deepStrictEqual(arrayFromMemory(memory), input); 489 | }); 490 | test("seek", () => { 491 | const memory = toMemory(input); 492 | for (let i = 0; i < input.length; i++) { 493 | assert.deepStrictEqual(featureFromMemory(memory, i), input[i]); 494 | } 495 | }); 496 | test("remove", () => { 497 | const memory = toMemory(input); 498 | const newMemory = deleteFeature(memory, input.length - 1); 499 | assert.equal(newMemory.lookup.length, memory.lookup.length - 2); 500 | for (let i = 0; i < input.length - 1; i++) { 501 | assert.deepStrictEqual(featureFromMemory(memory, i), input[i]); 502 | } 503 | }); 504 | }); 505 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "devDependencies": { 3 | "prettier": "^2.7.1" 4 | }, 5 | "scripts": { 6 | "test": "node --test" 7 | }, 8 | "volta": { 9 | "node": "18.7.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | prettier@^2.7.1: 6 | version "2.7.1" 7 | resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.7.1.tgz#e235806850d057f97bb08368a4f7d899f7760c64" 8 | integrity sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g== 9 | --------------------------------------------------------------------------------