@earthmover/icechunk 2.0.0-alpha.11 → 2.0.0-alpha.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,14 +46,30 @@ In-memory storage works on all platforms. The following backends are available o
46
46
  - **HTTP** — `Storage.newHttp(baseUrl, config?)`
47
47
  - **Local Filesystem** — `Storage.newLocalFilesystem(path)`
48
48
 
49
+ ### Fetch Storage (read-only, works everywhere)
50
+
51
+ For read-only access to a publicly hosted Icechunk repository, use the built-in fetch-based storage. This works on both native and WASM builds, making it the easiest way to open a repository in the browser:
52
+
53
+ ```typescript
54
+ import { Repository } from '@earthmover/icechunk'
55
+ import { createFetchStorage } from '@earthmover/icechunk/fetch-storage'
56
+
57
+ const storage = createFetchStorage('https://my-bucket.s3.us-west-2.amazonaws.com/my-repo.icechunk')
58
+ const repo = await Repository.open(storage)
59
+ const session = await repo.readonlySession({ branch: 'main' })
60
+ const keys = await session.store.list()
61
+ ```
62
+
63
+ The repository must be on S3-compatible storage with public read access and XML listing enabled. `createFetchStorage` uses the browser `fetch` API under the hood, so it works in any environment where `fetch` is available.
64
+
49
65
  ### Custom Storage Backends
50
66
 
51
67
  For WASM builds (or any environment where the built-in backends aren't suitable), you can provide your own storage implementation in JavaScript using `Storage.newCustom()`:
52
68
 
53
69
  ```typescript
54
70
  const storage = Storage.newCustom({
55
- canWrite: async () => true,
56
- getObjectRange: async ({ path, rangeStart, rangeEnd }) => {
71
+ canWrite: async (_err, ) => true,
72
+ getObjectRange: async (_err, { path, rangeStart, rangeEnd }) => {
57
73
  const headers: Record<string, string> = {}
58
74
  if (rangeStart != null && rangeEnd != null) {
59
75
  headers['Range'] = `bytes=${rangeStart}-${rangeEnd - 1}`
@@ -61,15 +77,17 @@ const storage = Storage.newCustom({
61
77
  const resp = await fetch(`https://my-bucket.example.com/${path}`, { headers })
62
78
  return { data: new Uint8Array(await resp.arrayBuffer()), version: { etag: resp.headers.get('etag') ?? undefined } }
63
79
  },
64
- putObject: async ({ path, data, contentType }) => { /* ... */ },
65
- copyObject: async ({ from, to }) => { /* ... */ },
66
- listObjects: async (prefix) => { /* return [{ id, createdAt, sizeBytes }] */ },
67
- deleteBatch: async ({ prefix, batch }) => { /* return { deletedObjects, deletedBytes } */ },
68
- getObjectLastModified: async (path) => { /* return Date */ },
69
- getObjectConditional: async ({ path, previousVersion }) => { /* ... */ },
80
+ putObject: async (_err, { path, data, contentType }) => { /* ... */ },
81
+ copyObject: async (_err, { from, to }) => { /* ... */ },
82
+ listObjects: async (_err, prefix) => { /* return [{ id, createdAt, sizeBytes }] */ },
83
+ deleteBatch: async (_err, { prefix, batch }) => { /* return { deletedObjects, deletedBytes } */ },
84
+ getObjectLastModified: async (_err, path) => { /* return Date */ },
85
+ getObjectConditional: async (_err, { path, previousVersion }) => { /* ... */ },
70
86
  })
71
87
  ```
72
88
 
89
+ > **Note:** Callbacks use the Node.js error-first convention — the first argument is always `null` (reserved for errors) and the actual arguments follow. Use `_err` to skip it.
90
+
73
91
  This is the primary way to use cloud storage in the browser, where native Rust networking is unavailable. Each callback method maps to an operation on the underlying `Storage` trait. See the exported `Storage*` TypeScript interfaces for the full type signatures.
74
92
 
75
93
  ### Virtual Chunks
@@ -84,6 +102,18 @@ Install the package with the `--cpu=wasm32` flag to get the WASM binary:
84
102
  npm install @earthmover/icechunk --cpu=wasm32
85
103
  ```
86
104
 
105
+ To open a public repository in the browser, use fetch storage:
106
+
107
+ ```typescript
108
+ import { Repository } from '@earthmover/icechunk'
109
+ import { createFetchStorage } from '@earthmover/icechunk/fetch-storage'
110
+
111
+ const storage = createFetchStorage('https://my-bucket.s3.us-west-2.amazonaws.com/my-repo.icechunk')
112
+ const repo = await Repository.open(storage)
113
+ ```
114
+
115
+ For more control, use `Storage.newCustom()` to implement your own storage backend with any JS networking library.
116
+
87
117
  The WASM build uses `SharedArrayBuffer` for threading, which requires your server to send these headers:
88
118
 
89
119
  ```
@@ -0,0 +1,20 @@
1
+ import type { Storage } from '@earthmover/icechunk'
2
+
3
+ /**
4
+ * Create a read-only storage backend that fetches objects over HTTP.
5
+ *
6
+ * Works with any publicly accessible icechunk repository hosted on S3-compatible
7
+ * storage. Requires the bucket to support anonymous reads and S3 XML listing.
8
+ *
9
+ * @param baseUrl - Base URL of the icechunk repository (no trailing slash)
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { Repository } from '@earthmover/icechunk'
14
+ * import { createFetchStorage } from '@earthmover/icechunk/fetch-storage'
15
+ *
16
+ * const storage = createFetchStorage('https://my-bucket.s3.us-west-2.amazonaws.com/path/to/repo.icechunk')
17
+ * const repo = await Repository.open(storage)
18
+ * ```
19
+ */
20
+ export declare function createFetchStorage(baseUrl: string): Storage
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Read-only fetch-based storage backend for Icechunk.
3
+ *
4
+ * Usage:
5
+ * import { Repository } from '@earthmover/icechunk'
6
+ * import { createFetchStorage } from '@earthmover/icechunk/fetch-storage'
7
+ *
8
+ * const storage = createFetchStorage('https://my-bucket.s3.amazonaws.com/my-repo.icechunk')
9
+ * const repo = await Repository.open(storage)
10
+ */
11
+
12
+ const { Storage } = require('@earthmover/icechunk')
13
+
14
+ /**
15
+ * @param {string} baseUrl - Base URL of the icechunk repository (no trailing slash)
16
+ * @returns {Storage}
17
+ */
18
+ function createFetchStorage(baseUrl) {
19
+ // Normalize: strip trailing slash
20
+ const base = baseUrl.replace(/\/+$/, '')
21
+
22
+ function throwForStatus(resp, url) {
23
+ if (resp.ok) return
24
+ if (resp.status === 404) {
25
+ throw new Error(`ObjectNotFound: ${url}`)
26
+ }
27
+ throw new Error(`HTTP ${resp.status}: ${url}`)
28
+ }
29
+
30
+ return Storage.newCustom({
31
+ canWrite: async (_err) => false,
32
+
33
+ getObjectRange: async (_err, { path, rangeStart, rangeEnd }) => {
34
+ const url = `${base}/${path}`
35
+ const headers = {}
36
+
37
+ if (rangeStart != null && rangeEnd != null) {
38
+ headers['Range'] = `bytes=${rangeStart}-${rangeEnd - 1}`
39
+ } else if (rangeStart != null) {
40
+ headers['Range'] = `bytes=${rangeStart}-`
41
+ }
42
+
43
+ const resp = await fetch(url, { headers })
44
+ throwForStatus(resp, url)
45
+
46
+ const data = new Uint8Array(await resp.arrayBuffer())
47
+ const etag = resp.headers.get('etag') ?? undefined
48
+ return { data, version: { etag } }
49
+ },
50
+
51
+ putObject: async () => {
52
+ throw new Error('Read-only storage: putObject not supported')
53
+ },
54
+
55
+ copyObject: async () => {
56
+ throw new Error('Read-only storage: copyObject not supported')
57
+ },
58
+
59
+ listObjects: async (_err, prefix) => {
60
+ // Try S3-style XML listing
61
+ // Derive bucket URL and key prefix from the base URL
62
+ // e.g. https://bucket.s3.region.amazonaws.com/prefix -> bucket URL + key prefix
63
+ const url = new URL(base)
64
+ const keyPrefix = url.pathname.replace(/^\//, '') + '/' + prefix
65
+ const listUrl = `${url.origin}/?list-type=2&prefix=${encodeURIComponent(keyPrefix)}`
66
+
67
+ const resp = await fetch(listUrl)
68
+ throwForStatus(resp, listUrl)
69
+
70
+ const xml = await resp.text()
71
+ const results = []
72
+ const contentRegex = /<Contents>([\s\S]*?)<\/Contents>/g
73
+ let match
74
+ while ((match = contentRegex.exec(xml)) !== null) {
75
+ const block = match[1]
76
+ const key = block.match(/<Key>(.*?)<\/Key>/)?.[1]
77
+ const lastModified = block.match(/<LastModified>(.*?)<\/LastModified>/)?.[1]
78
+ const size = block.match(/<Size>(.*?)<\/Size>/)?.[1]
79
+ if (key && lastModified && size) {
80
+ const id = key.startsWith(keyPrefix) ? key.slice(keyPrefix.length) : key
81
+ results.push({
82
+ id,
83
+ createdAt: new Date(lastModified),
84
+ sizeBytes: Number(size),
85
+ })
86
+ }
87
+ }
88
+
89
+ return results
90
+ },
91
+
92
+ deleteBatch: async () => {
93
+ throw new Error('Read-only storage: deleteBatch not supported')
94
+ },
95
+
96
+ getObjectLastModified: async (_err, path) => {
97
+ const url = `${base}/${path}`
98
+ const resp = await fetch(url, { method: 'HEAD' })
99
+ throwForStatus(resp, url)
100
+ const lastModified = resp.headers.get('last-modified')
101
+ if (!lastModified) {
102
+ throw new Error(`No Last-Modified header: ${url}`)
103
+ }
104
+ return new Date(lastModified)
105
+ },
106
+
107
+ getObjectConditional: async (_err, { path, previousVersion }) => {
108
+ const url = `${base}/${path}`
109
+ const headers = {}
110
+
111
+ if (previousVersion?.etag) {
112
+ headers['If-None-Match'] = previousVersion.etag
113
+ }
114
+
115
+ const resp = await fetch(url, { headers })
116
+
117
+ if (resp.status === 304) {
118
+ return { kind: 'on_latest_version' }
119
+ }
120
+
121
+ throwForStatus(resp, url)
122
+
123
+ const data = new Uint8Array(await resp.arrayBuffer())
124
+ const etag = resp.headers.get('etag') ?? undefined
125
+ return { kind: 'modified', data, newVersion: { etag } }
126
+ },
127
+ })
128
+ }
129
+
130
+ module.exports = { createFetchStorage }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@earthmover/icechunk",
3
- "version": "2.0.0-alpha.11",
3
+ "version": "2.0.0-alpha.12",
4
4
  "description": "JavaScript/TypeScript bindings for Icechunk",
5
5
  "main": "index.js",
6
6
  "repository": {
@@ -19,10 +19,22 @@
19
19
  "science",
20
20
  "geospatial"
21
21
  ],
22
+ "exports": {
23
+ ".": {
24
+ "browser": "./browser.js",
25
+ "default": "./index.js"
26
+ },
27
+ "./fetch-storage": {
28
+ "types": "./fetch-storage.d.ts",
29
+ "default": "./fetch-storage.js"
30
+ }
31
+ },
22
32
  "files": [
23
33
  "index.d.ts",
24
34
  "index.js",
25
- "browser.js"
35
+ "browser.js",
36
+ "fetch-storage.js",
37
+ "fetch-storage.d.ts"
26
38
  ],
27
39
  "napi": {
28
40
  "binaryName": "icechunk",
@@ -108,9 +120,9 @@
108
120
  },
109
121
  "packageManager": "yarn@4.12.0",
110
122
  "optionalDependencies": {
111
- "@earthmover/icechunk-win32-x64-msvc": "2.0.0-alpha.11",
112
- "@earthmover/icechunk-linux-x64-gnu": "2.0.0-alpha.11",
113
- "@earthmover/icechunk-darwin-arm64": "2.0.0-alpha.11",
114
- "@earthmover/icechunk-wasm32-wasi": "2.0.0-alpha.11"
123
+ "@earthmover/icechunk-win32-x64-msvc": "2.0.0-alpha.12",
124
+ "@earthmover/icechunk-linux-x64-gnu": "2.0.0-alpha.12",
125
+ "@earthmover/icechunk-darwin-arm64": "2.0.0-alpha.12",
126
+ "@earthmover/icechunk-wasm32-wasi": "2.0.0-alpha.12"
115
127
  }
116
128
  }