@craft-native/ts-maps 0.0.37

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/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ # MIT License
2
+
3
+ Copyright (c) 2025 Stacks.js
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # @craft-native/ts-maps
2
+
3
+ Craft SDK bindings for the [ts-maps](https://github.com/stacksjs/ts-maps) interactive map runtime.
4
+
5
+ This package is the TypeScript side of `MapProvider.ts_maps` in Craft's Zig core. It ships a `MapView` component you can drop into any Craft app (desktop, mobile, WebView) to get a cross-platform interactive map with zero API keys.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add @craft-native/ts-maps
11
+ ```
12
+
13
+ ## Quick start
14
+
15
+ ```ts
16
+ import { createMapView, defaultMapConfiguration, tsMapsProvider } from '@craft-native/ts-maps'
17
+
18
+ const view = createMapView({
19
+ configuration: { ...defaultMapConfiguration, provider: tsMapsProvider },
20
+ initialCamera: {
21
+ center: { latitude: 37.7749, longitude: -122.4194 },
22
+ zoom: 12,
23
+ pitch: 0,
24
+ heading: 0,
25
+ },
26
+ markers: [
27
+ {
28
+ id: 1,
29
+ coordinate: { latitude: 37.7749, longitude: -122.4194 },
30
+ title: 'San Francisco',
31
+ subtitle: null,
32
+ color: 'red',
33
+ is_draggable: false,
34
+ is_selected: false,
35
+ custom_icon: null,
36
+ anchor_x: 0.5,
37
+ anchor_y: 1,
38
+ },
39
+ ],
40
+ onTap: (coord) => console.log('tapped', coord),
41
+ onMarkerTap: (id, coord) => console.log('marker', id, coord),
42
+ })
43
+
44
+ document.body.appendChild(view.container)
45
+ ```
46
+
47
+ ## API overview
48
+
49
+ | Export | What it is |
50
+ | --- | --- |
51
+ | `createMapView(props)` | Factory returning a `MapViewInstance` with `id`, `container`, `map`, and imperative methods (`setCamera`, `addMarker`, `fitToMarkers`, …). |
52
+ | `MapViewProps` / `MapViewInstance` | Types for the component. |
53
+ | `Coordinate`, `MapRegion`, `MapCamera`, `MapMarker`, … | TypeScript mirrors of the structs in `packages/zig/src/maps.zig`. Field names match exactly so JSON round-trips are free. |
54
+ | `craftCoordToLatLng`, `latLngToCraftCoord`, `craftRegionToBounds`, `craftCameraToOptions`, `craftMarkerToMarker`, … | Pure adapter functions between Craft and ts-maps types. |
55
+ | `tsMapsProvider` | Constant `'ts_maps'` that matches the Zig enum tag. |
56
+ | Everything from `ts-maps` | `TsMap`, `LatLng`, `Marker`, `Polyline`, `marker()`, `tileLayer()`, etc. are re-exported so you only need one import. |
57
+
58
+ ## Bridge protocol
59
+
60
+ When running inside a Craft WebView, `createMapView` registers itself with `getBridge()` and listens for the methods declared in `./bridge-protocol`:
61
+
62
+ - `setCamera`, `setRegion`, `addMarker`, `removeMarker`, `addPolyline`, `addPolygon`, `addCircle`, `fitToMarkers`, `selectMarker`, `deselectAll`, `setConfiguration`.
63
+
64
+ It emits these events back:
65
+
66
+ - `map:tap`, `map:markerTap`, `map:regionChanged`, `map:cameraChanged`.
67
+
68
+ If no bridge is available (browser-only, tests, etc.) the component silently falls back to local-only behavior — event callbacks passed via props still fire.
69
+
70
+ ## Development
71
+
72
+ ```bash
73
+ bun run typecheck
74
+ bun test
75
+ bunx --bun pickier .
76
+ ```
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@craft-native/ts-maps",
3
+ "version": "0.0.37",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Craft SDK bindings for the ts-maps interactive map runtime. Provides a cross-platform MapView component backed by the MapProvider.ts_maps variant in the Zig core.",
7
+ "author": "Chris Breuer <chris@stacksjs.org>",
8
+ "license": "MIT",
9
+ "homepage": "https://github.com/home-lang/craft#readme",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/home-lang/craft.git",
13
+ "directory": "packages/ts-maps"
14
+ },
15
+ "keywords": [
16
+ "craft",
17
+ "ts-maps",
18
+ "maps",
19
+ "webview",
20
+ "cross-platform",
21
+ "native"
22
+ ],
23
+ "exports": {
24
+ ".": {
25
+ "types": "./dist/index.d.ts",
26
+ "import": "./dist/index.js"
27
+ },
28
+ "./adapters": {
29
+ "types": "./dist/adapters.d.ts",
30
+ "import": "./dist/adapters.js"
31
+ },
32
+ "./types": {
33
+ "types": "./dist/types.d.ts",
34
+ "import": "./dist/types.js"
35
+ },
36
+ "./bridge-protocol": {
37
+ "types": "./dist/bridge-protocol.d.ts",
38
+ "import": "./dist/bridge-protocol.js"
39
+ }
40
+ },
41
+ "module": "./dist/index.js",
42
+ "types": "./dist/index.d.ts",
43
+ "files": [
44
+ "dist",
45
+ "src",
46
+ "README.md"
47
+ ],
48
+ "scripts": {
49
+ "build": "bun -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && bun build src/index.ts src/adapters.ts src/types.ts src/bridge-protocol.ts --outdir dist --format esm --target browser && bun run build:types",
50
+ "build:types": "bun --bun tsc -p tsconfig.build.json && bun scripts/finalize-types.ts",
51
+ "lint": "bunx --bun pickier lint . --config ../../pickier.config.ts --max-warnings 9999",
52
+ "lint:fix": "bunx --bun pickier lint . --fix --config ../../pickier.config.ts --max-warnings 9999",
53
+ "test": "bun test",
54
+ "typecheck": "bun --bun tsc --noEmit"
55
+ },
56
+ "devDependencies": {
57
+ "@types/bun": "^1.2.0",
58
+ "ts-maps": "^0.2.7",
59
+ "very-happy-dom": "^0.1.4"
60
+ }
61
+ }
@@ -0,0 +1,243 @@
1
+ /**
2
+ * Filesystem-backed tile persistence for `ts-maps` `TileCache`.
3
+ *
4
+ * Wraps Craft's `fs` bridge (which transparently falls back to `node:fs`
5
+ * when running outside a Craft WebView) so that tile bytes for an
6
+ * offline region are written to an app-sandboxed directory rather than
7
+ * IndexedDB. Returned objects plug straight into `TileCache` via its
8
+ * `backend` option.
9
+ *
10
+ * Each cached tile is serialised as a single `.tsm` file containing:
11
+ *
12
+ * magic 4 bytes "TSMT"
13
+ * version 1 byte 0x01
14
+ * keyLen 4 bytes little-endian uint32
15
+ * mimeLen 4 bytes little-endian uint32
16
+ * reservedLen 4 bytes little-endian uint32 (0)
17
+ * addedAt 8 bytes little-endian float64 (ms epoch)
18
+ * reserved 8 bytes little-endian float64 (0)
19
+ * dataLen 4 bytes little-endian uint32
20
+ * key keyLen bytes UTF-8
21
+ * mime mimeLen bytes UTF-8
22
+ * data dataLen bytes payload
23
+ *
24
+ * Filenames are `<hash>.tsm` where `<hash>` is a 64-bit FNV-1a of the
25
+ * URL (two 32-bit passes with different seeds) rendered in base36. That
26
+ * keeps filenames short, URL-safe, and collision-resistant enough for
27
+ * the tile counts an offline region realistically produces.
28
+ *
29
+ * @module @craft-native/ts-maps/FilesystemTileBackend
30
+ */
31
+
32
+ import type { Tile, TileCacheBackend } from 'ts-maps'
33
+ // Import the fs helpers directly from their source module rather than from
34
+ // the `craft-native` package root, matching how `MapView` talks to
35
+ // the bridge — keeps the type graph narrow.
36
+ import { fs, readBinaryFile, writeBinaryFile } from '../../typescript/src/api/fs'
37
+
38
+ export interface FilesystemBackendOptions {
39
+ /**
40
+ * Directory where tile files are stored. Created on first write if it
41
+ * doesn't exist. On Craft apps this is typically something like
42
+ * `app.dataDir + '/tiles'` — the caller resolves the platform-specific
43
+ * path and passes it in.
44
+ */
45
+ baseDir: string
46
+ }
47
+
48
+ const MAGIC = [0x54, 0x53, 0x4D, 0x54] // "TSMT"
49
+ const VERSION = 0x01
50
+ const FILE_SUFFIX = '.tsm'
51
+
52
+ /**
53
+ * Construct a {@link TileCacheBackend} that persists tiles under `baseDir`.
54
+ * Pass the returned object to `new TileCache({ backend })` (or to the
55
+ * cache used by `saveOfflineRegion`).
56
+ */
57
+ export function createFilesystemBackend(opts: FilesystemBackendOptions): TileCacheBackend {
58
+ const baseDir = stripTrailingSlash(opts.baseDir)
59
+ let ensured = false
60
+
61
+ async function ensureDir(): Promise<void> {
62
+ if (ensured)
63
+ return
64
+ if (!(await fs.exists(baseDir)))
65
+ await fs.mkdir(baseDir)
66
+ ensured = true
67
+ }
68
+
69
+ function filePath(key: string): string {
70
+ return `${baseDir}/${hashKey(key)}${FILE_SUFFIX}`
71
+ }
72
+
73
+ return {
74
+ async get(key: string): Promise<Tile | undefined> {
75
+ const path = filePath(key)
76
+ if (!(await fs.exists(path)))
77
+ return undefined
78
+ try {
79
+ const bytes = await readBinaryFile(path)
80
+ const decoded = decodeTile(bytes)
81
+ // Guard against FNV collisions: if the stored key doesn't match
82
+ // the requested URL we treat it as a miss rather than serving the
83
+ // wrong payload.
84
+ if (decoded.key !== key)
85
+ return undefined
86
+ return decoded
87
+ }
88
+ catch {
89
+ return undefined
90
+ }
91
+ },
92
+
93
+ async put(tile: Tile): Promise<void> {
94
+ await ensureDir()
95
+ const payload = encodeTile(tile)
96
+ await writeBinaryFile(filePath(tile.key), payload)
97
+ },
98
+
99
+ async delete(key: string): Promise<void> {
100
+ const path = filePath(key)
101
+ try {
102
+ if (await fs.exists(path))
103
+ await fs.remove(path)
104
+ }
105
+ catch {
106
+ // swallow: a missing tile is not an error for our caller
107
+ }
108
+ },
109
+
110
+ async clear(): Promise<void> {
111
+ try {
112
+ if (!(await fs.exists(baseDir)))
113
+ return
114
+ const entries = await fs.readDir(baseDir)
115
+ for (const name of entries) {
116
+ if (!name.endsWith(FILE_SUFFIX))
117
+ continue
118
+ try {
119
+ await fs.remove(`${baseDir}/${name}`)
120
+ }
121
+ catch { /* ignore per-entry failure */ }
122
+ }
123
+ }
124
+ catch { /* ignore */ }
125
+ },
126
+
127
+ async all(): Promise<Tile[]> {
128
+ if (!(await fs.exists(baseDir)))
129
+ return []
130
+ let entries: string[]
131
+ try {
132
+ entries = await fs.readDir(baseDir)
133
+ }
134
+ catch {
135
+ return []
136
+ }
137
+ const out: Tile[] = []
138
+ for (const name of entries) {
139
+ if (!name.endsWith(FILE_SUFFIX))
140
+ continue
141
+ try {
142
+ const bytes = await readBinaryFile(`${baseDir}/${name}`)
143
+ out.push(decodeTile(bytes))
144
+ }
145
+ catch {
146
+ // skip unreadable / corrupt entries
147
+ }
148
+ }
149
+ return out
150
+ },
151
+ }
152
+ }
153
+
154
+ // ---------------------------------------------------------------------------
155
+ // Encoding
156
+ // ---------------------------------------------------------------------------
157
+
158
+ function encodeTile(tile: Tile): Uint8Array {
159
+ const enc = new TextEncoder()
160
+ const key = enc.encode(tile.key)
161
+ const mime = enc.encode(tile.mime)
162
+ const reserved = new Uint8Array()
163
+
164
+ const headerLen = 4 + 1 + 4 + 4 + 4 + 8 + 8 + 4
165
+ const total = headerLen + key.length + mime.length + reserved.length + tile.data.length
166
+ const buf = new Uint8Array(total)
167
+ const view = new DataView(buf.buffer)
168
+
169
+ let o = 0
170
+ buf[o++] = MAGIC[0]
171
+ buf[o++] = MAGIC[1]
172
+ buf[o++] = MAGIC[2]
173
+ buf[o++] = MAGIC[3]
174
+ buf[o++] = VERSION
175
+ view.setUint32(o, key.length, true); o += 4
176
+ view.setUint32(o, mime.length, true); o += 4
177
+ view.setUint32(o, reserved.length, true); o += 4
178
+ view.setFloat64(o, tile.addedAt, true); o += 8
179
+ view.setFloat64(o, 0, true); o += 8
180
+ view.setUint32(o, tile.data.length, true); o += 4
181
+
182
+ buf.set(key, o); o += key.length
183
+ buf.set(mime, o); o += mime.length
184
+ buf.set(reserved, o); o += reserved.length
185
+ buf.set(tile.data, o)
186
+
187
+ return buf
188
+ }
189
+
190
+ function decodeTile(bytes: Uint8Array): Tile {
191
+ if (bytes.length < 37)
192
+ throw new Error('truncated tile record')
193
+ if (bytes[0] !== MAGIC[0] || bytes[1] !== MAGIC[1] || bytes[2] !== MAGIC[2] || bytes[3] !== MAGIC[3])
194
+ throw new Error('bad tile magic')
195
+ if (bytes[4] !== VERSION)
196
+ throw new Error(`unsupported tile version ${bytes[4]}`)
197
+
198
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
199
+ let o = 5
200
+ const keyLen = view.getUint32(o, true); o += 4
201
+ const mimeLen = view.getUint32(o, true); o += 4
202
+ const reservedLen = view.getUint32(o, true); o += 4
203
+ const addedAt = view.getFloat64(o, true); o += 8
204
+ o += 8
205
+ const dataLen = view.getUint32(o, true); o += 4
206
+
207
+ const dec = new TextDecoder()
208
+ const key = dec.decode(bytes.subarray(o, o + keyLen)); o += keyLen
209
+ const mime = dec.decode(bytes.subarray(o, o + mimeLen)); o += mimeLen
210
+ o += reservedLen
211
+ const data = bytes.subarray(o, o + dataLen).slice()
212
+
213
+ return {
214
+ key,
215
+ data,
216
+ mime,
217
+ addedAt,
218
+ bytes: data.length,
219
+ }
220
+ }
221
+
222
+ // ---------------------------------------------------------------------------
223
+ // Hash. Two FNV-1a-32 passes with different seeds — effectively a 64-bit
224
+ // hash. Good enough for tile-scale workloads; `get()` re-verifies the
225
+ // stored key against the request to catch the collision case.
226
+ // ---------------------------------------------------------------------------
227
+
228
+ function hashKey(key: string): string {
229
+ let a = 0x811C9DC5 >>> 0
230
+ let b = 0xCBF29CE4 >>> 0
231
+ for (let i = 0; i < key.length; i++) {
232
+ const c = key.charCodeAt(i)
233
+ a ^= c
234
+ a = Math.imul(a, 0x01000193) >>> 0
235
+ b ^= c
236
+ b = Math.imul(b, 0x00000105) >>> 0
237
+ }
238
+ return `${a.toString(36)}${b.toString(36)}`
239
+ }
240
+
241
+ function stripTrailingSlash(path: string): string {
242
+ return path.replace(/\/+$/, '')
243
+ }