@statewalker/webrun-files-sqlite 0.10.2 → 0.11.0
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 +182 -140
- package/dist/blocks/sqlite-files-api.d.ts +3 -3
- package/dist/blocks/sqlite-files-api.d.ts.map +1 -1
- package/dist/bytes.d.ts +1 -1
- package/dist/bytes.d.ts.map +1 -1
- package/dist/cjs/index.cjs +81 -112
- package/dist/esm/index.js +82 -110
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/run-statements.d.ts +15 -0
- package/dist/run-statements.d.ts.map +1 -0
- package/dist/sqlar-files-api.d.ts +2 -2
- package/dist/sqlar-files-api.d.ts.map +1 -1
- package/package.json +12 -4
- package/src/blocks/sqlite-files-api.ts +40 -31
- package/src/bytes.ts +1 -3
- package/src/index.ts +0 -9
- package/src/run-statements.ts +24 -0
- package/src/sqlar-files-api.ts +20 -25
- package/dist/drivers.d.ts +0 -73
- package/dist/drivers.d.ts.map +0 -1
- package/dist/sql.types.d.ts +0 -25
- package/dist/sql.types.d.ts.map +0 -1
- package/src/drivers.ts +0 -133
- package/src/sql.types.ts +0 -25
package/README.md
CHANGED
|
@@ -1,179 +1,212 @@
|
|
|
1
1
|
# @statewalker/webrun-files-sqlite
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
## What it is
|
|
4
|
+
|
|
5
|
+
Two implementations of `FilesApi` from `@statewalker/webrun-files` that store files in a SQLite
|
|
6
|
+
database, through `@statewalker/db-api`'s `Db`: any `@statewalker/db-sqlite-*` driver — `node:sqlite`,
|
|
7
|
+
the browser (sqlite-wasm), or a Durable Object's storage:
|
|
8
|
+
|
|
9
|
+
- `SqliteFilesApi` streams content as fixed-size blocks (1 MiB by default, optionally deflated per
|
|
10
|
+
block), shares identical content between paths, and makes every metadata change one transaction.
|
|
11
|
+
Files of any size.
|
|
12
|
+
- `SqlarFilesApi` stores each file as one row of a [SQLite Archive](https://sqlite.org/sqlar.html),
|
|
13
|
+
readable by `sqlite3 -A`. Whole-file reads and writes.
|
|
14
|
+
|
|
15
|
+
## Why it exists
|
|
16
|
+
|
|
17
|
+
Durable Objects offer SQLite and nothing like a file system, and a single SQLite file is a
|
|
18
|
+
convenient, transactional store elsewhere too. Storing a file as one blob breaks there: Durable
|
|
19
|
+
Objects cap a row at 2 MB, and a blob is read and written whole. `SqliteFilesApi` cuts
|
|
20
|
+
content into blocks that stay under the cap and can be streamed and range-read. `SqlarFilesApi`
|
|
21
|
+
exists for the opposite need: an archive other SQLAR tools can open, in runtimes without a row
|
|
22
|
+
limit.
|
|
5
23
|
|
|
6
24
|
| | `SqliteFilesApi` | `SqlarFilesApi` |
|
|
7
25
|
| --- | --- | --- |
|
|
8
|
-
| Storage | paths
|
|
26
|
+
| Storage | paths -> contents -> blocks of at most 1.5 MiB | one row per file, SQLite Archive format |
|
|
9
27
|
| File size | unbounded; streamed block by block | one blob, read and written whole |
|
|
10
|
-
| Durable Objects
|
|
28
|
+
| Durable Objects | yes, every row stays under the 2 MB limit | only files whose stored row is under 2 MB |
|
|
11
29
|
| Readable by `sqlite3 -A` | no | yes |
|
|
12
30
|
| Identical content | stored once, shared by every path | stored per path |
|
|
13
31
|
|
|
14
|
-
Use `SqliteFilesApi` by default
|
|
15
|
-
SQLAR tools, in a runtime without a row limit (Node, Deno, Bun, a browser SQLite).
|
|
32
|
+
Use `SqliteFilesApi` by default.
|
|
16
33
|
|
|
17
|
-
##
|
|
34
|
+
## How to use
|
|
18
35
|
|
|
19
36
|
```bash
|
|
20
|
-
|
|
37
|
+
pnpm add @statewalker/webrun-files-sqlite @statewalker/webrun-files @statewalker/db-sqlite-node
|
|
21
38
|
```
|
|
22
39
|
|
|
23
|
-
|
|
40
|
+
Pick the driver for your runtime: `@statewalker/db-sqlite-node` on Node, `@statewalker/db-sqlite-do`
|
|
41
|
+
in a Durable Object, `@statewalker/db-sqlite-browser` in the browser.
|
|
24
42
|
|
|
25
|
-
|
|
43
|
+
`pako` is not a dependency: pass the module to `pakoCodec` / `pakoDeflateCodec` if you use them.
|
|
26
44
|
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
One entry point, `@statewalker/webrun-files-sqlite`: ESM (`dist/esm/index.js`), CommonJS
|
|
46
|
+
(`dist/cjs/index.cjs`), types (`dist/index.d.ts`); sources in `src/`. The package imports no
|
|
47
|
+
database driver itself; you pass a `Db` in, so it bundles for any runtime.
|
|
29
48
|
|
|
30
49
|
```typescript
|
|
31
|
-
import {
|
|
50
|
+
import { newNodeSqliteDb } from '@statewalker/db-sqlite-node';
|
|
51
|
+
import { SqliteFilesApi } from '@statewalker/webrun-files-sqlite';
|
|
52
|
+
|
|
53
|
+
const db = await newNodeSqliteDb({ path: 'files.db' }); // node:sqlite
|
|
54
|
+
// or, in a Durable Object (ctx.storage, not ctx.storage.sql):
|
|
55
|
+
// import { newDoSqliteDb } from '@statewalker/db-sqlite-do';
|
|
56
|
+
// const db = newDoSqliteDb(this.ctx.storage);
|
|
32
57
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
new D1SqlDriver(env.DB); // a D1 binding
|
|
58
|
+
const files = new SqliteFilesApi(db, { blockSize: 1024 * 1024 });
|
|
59
|
+
await files.init(); // creates tables and indexes if missing; await it before any other call
|
|
36
60
|
```
|
|
37
61
|
|
|
38
|
-
|
|
62
|
+
### `SqliteFilesApi` options
|
|
39
63
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
| Option | Default | |
|
|
65
|
+
| --- | --- | --- |
|
|
66
|
+
| `compression` | `webDeflateCodec()` where `CompressionStream` exists, else none | `null` stores uncompressed; `pakoDeflateCodec(pako)` for runtimes without Compression Streams |
|
|
67
|
+
| `tablePrefix` | `"fs_"` | several file systems in one database |
|
|
68
|
+
| `blockSize` | 1 MiB | uncompressed bytes per block for new writes, 1 byte to 1.5 MiB |
|
|
45
69
|
|
|
46
|
-
|
|
47
|
-
/** Rows as objects keyed by column name. */
|
|
48
|
-
all<T>(sql: string, ...params: unknown[]): T[] | Promise<T[]>;
|
|
49
|
-
/** One statement, for its effect. */
|
|
50
|
-
run(sql: string, ...params: unknown[]): void | Promise<void>;
|
|
51
|
-
/**
|
|
52
|
-
* The statements in order, as one transaction: all apply or none do. Returns the number of rows
|
|
53
|
-
* each statement changed. No statement may depend on another's result.
|
|
54
|
-
*/
|
|
55
|
-
transaction(statements: SqlStatement[]): number[] | Promise<number[]>;
|
|
56
|
-
}
|
|
57
|
-
```
|
|
70
|
+
### `SqlarFilesApi` codecs (`codec` option)
|
|
58
71
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
| Codec | Uses | When |
|
|
73
|
+
| --- | --- | --- |
|
|
74
|
+
| `defaultCodec()` | `webCodec()` or `rawCodec()` | the default: `webCodec()` where `CompressionStream` exists, `rawCodec()` elsewhere |
|
|
75
|
+
| `webCodec({ minSize? })` | `CompressionStream("deflate")` | browsers, Workers, Node, Deno, Bun |
|
|
76
|
+
| `pakoCodec(pako, { minSize?, level? })` | an injected `pako` module | runtimes without Compression Streams |
|
|
77
|
+
| `rawCodec()` | nothing | never compresses, and refuses to read compressed rows |
|
|
63
78
|
|
|
64
|
-
##
|
|
79
|
+
## Examples
|
|
80
|
+
|
|
81
|
+
### Stream a large file through `SqliteFilesApi`
|
|
65
82
|
|
|
66
83
|
```typescript
|
|
67
|
-
import {
|
|
68
|
-
import {
|
|
84
|
+
import { newNodeSqliteDb } from '@statewalker/db-sqlite-node';
|
|
85
|
+
import { SqliteFilesApi } from '@statewalker/webrun-files-sqlite';
|
|
69
86
|
|
|
70
|
-
const files = new SqliteFilesApi(
|
|
71
|
-
await files.init();
|
|
87
|
+
const files = new SqliteFilesApi(await newNodeSqliteDb({ path: 'files.db' }));
|
|
88
|
+
await files.init();
|
|
72
89
|
|
|
73
|
-
await files.write(
|
|
74
|
-
for await (const chunk of files.read(
|
|
90
|
+
await files.write('/video.mp4', chunks); // any Iterable or AsyncIterable of Uint8Array
|
|
91
|
+
for await (const chunk of files.read('/video.mp4', { start: 10_000_000, length: 65_536 })) {
|
|
75
92
|
// only the block holding this range is fetched and inflated
|
|
76
93
|
}
|
|
77
94
|
```
|
|
78
95
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
### How content is stored
|
|
82
|
-
|
|
83
|
-
- `fs_paths(pid, path UNIQUE, fid, mtime)` — a path points at a content, or is a directory (`fid` NULL).
|
|
84
|
-
- `fs_files(fid, size, compression, block_size, hash, updated)` — an immutable content: its
|
|
85
|
-
uncompressed size, `"none"` or `"deflate"`, the block size it was written with, the SHA-256 of its
|
|
86
|
-
bytes, and when it last changed (`size` and `hash` are NULL while it is being written).
|
|
87
|
-
- `fs_blocks(fid, shift, block)`, keyed by `(fid, shift)` — the content cut into blocks of
|
|
88
|
-
`block_size` uncompressed bytes (only the last one shorter); `shift` is the block's offset in the
|
|
89
|
-
uncompressed content, always a multiple of `block_size`. Each block of a `deflate` content is its
|
|
90
|
-
own zlib stream, so a range read fetches and inflates exactly the blocks the range touches, found
|
|
91
|
-
by key.
|
|
96
|
+
### Inside a Durable Object
|
|
92
97
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
### Streaming
|
|
98
|
-
|
|
99
|
-
`write` pulls the source only while it builds the current block and stores each block before
|
|
100
|
-
pulling for the next; `read` fetches a block only when its consumer asks for bytes beyond the
|
|
101
|
-
previous one. Memory is bounded by one block plus one source chunk, and the tests measure both
|
|
102
|
-
bounds. An `AbortSignal` passed to `read` is checked before every block.
|
|
103
|
-
|
|
104
|
-
`list` is streamed too: it reads `fs_paths` in pages of 256 by key, in the index's order — which is
|
|
105
|
-
the path order every `FilesApi` lists in — and `{ after }` is simply where the first page starts.
|
|
98
|
+
```typescript
|
|
99
|
+
import { newDoSqliteDb } from '@statewalker/db-sqlite-do';
|
|
100
|
+
import { SqliteFilesApi } from '@statewalker/webrun-files-sqlite';
|
|
106
101
|
|
|
107
|
-
|
|
102
|
+
const files = new SqliteFilesApi(newDoSqliteDb(this.ctx.storage));
|
|
103
|
+
await files.init();
|
|
104
|
+
```
|
|
108
105
|
|
|
109
|
-
|
|
110
|
-
| --- | --- | --- |
|
|
111
|
-
| `compression` | `webDeflateCodec()` where `CompressionStream` exists, else none | `null` stores uncompressed; `pakoDeflateCodec(pako)` for runtimes without Compression Streams |
|
|
112
|
-
| `tablePrefix` | `"fs_"` | several file systems in one database |
|
|
113
|
-
| `blockSize` | 1 MiB | uncompressed bytes per block for new writes, 1 to 1.5 MiB (the cap keeps a deflated block under the 2 MB row limit). Larger means fewer rows — better where statements are costly, as on D1; smaller means cheaper small range reads. Each content records its own size, so changing this never affects existing files. |
|
|
106
|
+
### Clean up interrupted writes
|
|
114
107
|
|
|
115
|
-
|
|
108
|
+
```typescript
|
|
109
|
+
const removed = await files.sweep({ olderThan: 60 * 60_000 }); // contents untouched for an hour
|
|
110
|
+
```
|
|
116
111
|
|
|
117
|
-
|
|
118
|
-
path contains the other.
|
|
119
|
-
- A write is invisible until complete: if it fails for any reason, the path keeps its previous
|
|
120
|
-
content and the partly written content is deleted.
|
|
121
|
-
- A read whose content is removed underneath it throws `changed during read`. A block whose
|
|
122
|
-
uncompressed length is not what its position implies throws `block at <shift> holds … bytes`,
|
|
123
|
-
before any wrong byte reaches the reader.
|
|
112
|
+
### A portable archive with `SqlarFilesApi`
|
|
124
113
|
|
|
125
|
-
|
|
114
|
+
```typescript
|
|
115
|
+
import { newNodeSqliteDb } from '@statewalker/db-sqlite-node';
|
|
116
|
+
import * as pako from 'pako';
|
|
117
|
+
import { readText, writeText } from '@statewalker/webrun-files';
|
|
118
|
+
import { pakoCodec, SqlarFilesApi } from '@statewalker/webrun-files-sqlite';
|
|
126
119
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
120
|
+
const files = new SqlarFilesApi(await newNodeSqliteDb({ path: 'site.sqlar' }), {
|
|
121
|
+
codec: pakoCodec(pako, { level: 9 }),
|
|
122
|
+
});
|
|
123
|
+
await files.init(); // creates the sqlar table if missing
|
|
131
124
|
|
|
132
|
-
|
|
133
|
-
|
|
125
|
+
await writeText(files, '/docs/index.md', '# Hello');
|
|
126
|
+
console.log(await readText(files, '/docs/index.md'));
|
|
134
127
|
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
await files.symlink('/latest.md', './docs/index.md');
|
|
129
|
+
await files.readlink('/latest.md'); // "./docs/index.md"
|
|
137
130
|
```
|
|
138
131
|
|
|
139
|
-
|
|
140
|
-
milliseconds, with their blocks. A write refreshes its content with every block, so only a write
|
|
141
|
-
stalled longer than `olderThan` can be swept — and it then fails rather than creating a broken path.
|
|
142
|
-
Choose a threshold above the longest pause a source may take. `sweep` never runs on its own.
|
|
132
|
+
## Internals
|
|
143
133
|
|
|
144
|
-
|
|
134
|
+
### Why every step runs through `inTransaction`, and what breaks it
|
|
145
135
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
136
|
+
Each step that changes what a path shows runs as one SQLite transaction through `inTransaction`
|
|
137
|
+
from `@statewalker/db-sqlite`. It runs as `BEGIN IMMEDIATE … COMMIT` behind a mutex on Node and in
|
|
138
|
+
the browser, and through `ctx.storage.transaction()` in a Durable Object, where `sql.exec` rejects
|
|
139
|
+
`BEGIN`. The rows a statement changed come from `SELECT changes()` inside that transaction. Steps
|
|
140
|
+
never depend on reading another statement's result: conditions live in SQL (`WHERE EXISTS …`).
|
|
150
141
|
|
|
151
|
-
|
|
152
|
-
|
|
142
|
+
`inTransaction` finds the driver's transaction support through a hook on the `Db` it was given.
|
|
143
|
+
**A `Db` wrapped in your own proxy (logging, metrics) loses the hook.** The first `write`,
|
|
144
|
+
`remove`, `copy`, `move` or `sweep` then rejects with: `inTransaction: this Db has no SQLite
|
|
145
|
+
transaction hook. It is not a @statewalker/db-sqlite* Db, or it wraps one (a wrapper object loses
|
|
146
|
+
the hook - pass the driver's Db itself), or it is the handle of a running transaction (nesting is
|
|
147
|
+
not supported).` Pass the driver's `Db` itself, or forward `db[SQLITE_TRANSACTION]` (exported by
|
|
148
|
+
`@statewalker/db-sqlite`) in your wrapper.
|
|
153
149
|
|
|
154
|
-
|
|
155
|
-
|
|
150
|
+
### How `SqliteFilesApi` stores content
|
|
151
|
+
|
|
152
|
+
```
|
|
153
|
+
fs_paths(pid, path UNIQUE, fid, mtime) a path -> a content, or a directory (fid NULL)
|
|
154
|
+
|
|
|
155
|
+
v
|
|
156
|
+
fs_files(fid, size, compression, block_size, an immutable content: size, "none"/"deflate",
|
|
157
|
+
hash, updated) block size, SHA-256, last change
|
|
158
|
+
|
|
|
159
|
+
v
|
|
160
|
+
fs_blocks(fid, shift, block) the content in blocks of block_size bytes;
|
|
161
|
+
shift = offset in the uncompressed content
|
|
156
162
|
```
|
|
157
163
|
|
|
158
|
-
|
|
164
|
+
Each block of a `deflate` content is its own zlib stream, so a range read fetches and inflates
|
|
165
|
+
exactly the blocks it touches, found by key. `copy` adds path rows pointing at the same content; a
|
|
166
|
+
write whose bytes match an existing content (same SHA-256, size, compression and block size) shares
|
|
167
|
+
it too. A content and its blocks are deleted when the last path pointing at it goes.
|
|
159
168
|
|
|
160
|
-
###
|
|
169
|
+
### Why the block size is capped at 1.5 MiB
|
|
161
170
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
| `rawCodec()` | nothing | Default elsewhere; never compresses, and refuses to read compressed rows |
|
|
171
|
+
A deflated block can be slightly larger than its input. 1.5 MiB keeps even incompressible input
|
|
172
|
+
under the 2 MB row limit of Durable Objects. Larger blocks mean fewer rows; smaller blocks make
|
|
173
|
+
small range reads cheaper. Each content records its own block size, so changing `blockSize` never
|
|
174
|
+
affects existing files.
|
|
167
175
|
|
|
168
|
-
|
|
169
|
-
import * as pako from "pako";
|
|
170
|
-
const files = new SqlarFilesApi(driver, { codec: pakoCodec(pako) });
|
|
171
|
-
```
|
|
176
|
+
### Streaming and memory
|
|
172
177
|
|
|
173
|
-
|
|
174
|
-
|
|
178
|
+
`write` pulls the source only while it builds the current block and stores each block before
|
|
179
|
+
pulling the next; `read` fetches a block only when its consumer asks for bytes beyond the previous
|
|
180
|
+
one. Memory is bounded by one block plus one source chunk; the tests measure both bounds. An
|
|
181
|
+
`AbortSignal` passed to `read` is checked before every block. `list` reads `fs_paths` in pages of
|
|
182
|
+
256 by key, in index order, which is the `FilesApi` path order, and `{ after }` is where the first
|
|
183
|
+
page starts.
|
|
184
|
+
|
|
185
|
+
### Atomic steps, and why `sweep` exists
|
|
186
|
+
|
|
187
|
+
Streaming happens outside transactions. Every step that changes what a path shows is one
|
|
188
|
+
transaction: storing each block, finishing a write, `remove`, `copy` and `move`. A failure inside
|
|
189
|
+
any of them leaves the database as it was; `move` onto an existing target never loses the target.
|
|
190
|
+
A write stays invisible until it finishes: if it fails, the path keeps its previous content and the
|
|
191
|
+
partial content is deleted.
|
|
192
|
+
|
|
193
|
+
If the process dies mid-stream, the partial content is left behind, unreferenced. `sweep` deletes
|
|
194
|
+
contents that no path references and that have not been updated for `olderThan` milliseconds, with
|
|
195
|
+
their blocks. A write refreshes its content with every block, so only a write stalled longer than
|
|
196
|
+
`olderThan` can be swept, and it then fails rather than creating a broken path. Choose a threshold
|
|
197
|
+
above the longest pause a source may take. `sweep` never runs on its own.
|
|
198
|
+
|
|
199
|
+
### What `SqliteFilesApi` throws
|
|
200
|
+
|
|
201
|
+
- The constructor throws `SqliteFilesApi: blockSize must be an integer from 1 to 1572864, got ...`
|
|
202
|
+
and `SqliteFilesApi: tablePrefix must be a plain SQL identifier, got ...`.
|
|
203
|
+
- `copy` and `move` replace the target's subtree rather than merging into it, and throw when one
|
|
204
|
+
path contains the other.
|
|
205
|
+
- A read whose content is removed underneath it throws `changed during read`.
|
|
206
|
+
- A block whose uncompressed length does not match its position throws `block at <shift> holds ...
|
|
207
|
+
bytes`, before any wrong byte reaches the reader.
|
|
175
208
|
|
|
176
|
-
### How
|
|
209
|
+
### How `SqlarFilesApi` stores entries
|
|
177
210
|
|
|
178
211
|
| Entry | `name` | `sz` | `data` |
|
|
179
212
|
| --- | --- | --- | --- |
|
|
@@ -182,30 +215,39 @@ Inputs shorter than `minSize` (64 bytes by default) are stored without attemptin
|
|
|
182
215
|
| Directory | | `0` | `NULL` |
|
|
183
216
|
| Symlink | | `-1` | target as text |
|
|
184
217
|
|
|
185
|
-
`mtime` is stored in seconds; `lastModified` reports
|
|
186
|
-
|
|
187
|
-
directory
|
|
218
|
+
`mtime` is stored in seconds; `lastModified` reports milliseconds. Inputs shorter than `minSize`
|
|
219
|
+
(64 bytes by default) are stored without trying to compress. All codecs read and write the same
|
|
220
|
+
zlib format. Archives written by other tools without directory rows still behave as trees: a path
|
|
221
|
+
that is only a prefix of other names is a directory.
|
|
188
222
|
|
|
189
|
-
|
|
223
|
+
`FileKind` has no symlink, so `stats`, `exists`, `read` and `list` follow links as POSIX `stat()`
|
|
224
|
+
does. A dangling link, or a chain longer than 8, behaves as a missing path. `remove`, `copy` and
|
|
225
|
+
`move` act on the link itself. Only the final path component is resolved through symlinks.
|
|
190
226
|
|
|
191
|
-
|
|
192
|
-
`stat()` does. A dangling link, or a chain of more than 8, behaves as a missing path. `remove`,
|
|
193
|
-
`copy` and `move` act on the link itself. Two extension methods give access to links:
|
|
227
|
+
### `SqlarFilesApi` limits
|
|
194
228
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
229
|
+
- A file is one blob: `write` buffers it, `read` inflates it whole and yields one chunk.
|
|
230
|
+
- On Durable Objects the stored (possibly compressed) size of one file must stay under 2 MB, since
|
|
231
|
+
SQLAR keeps each file in one row. A larger file fails the `write` with `string or blob too big`
|
|
232
|
+
— a value limit, not corruption. Store big files with `SqliteFilesApi`, whose blocks stay under
|
|
233
|
+
the limit.
|
|
234
|
+
- Multi-row mutations (`copy`, `move`, `remove` of a tree) are not wrapped in a transaction: a
|
|
235
|
+
failure part-way leaves the rows already changed in place, and a `move` can leave the entry under
|
|
236
|
+
both paths.
|
|
237
|
+
- `list` loads every row under the directory, then sorts the entries into path order before
|
|
238
|
+
applying `after`.
|
|
239
|
+
|
|
240
|
+
### Dependencies
|
|
241
|
+
|
|
242
|
+
- `@statewalker/db-api` — the `Db` contract both implementations run on.
|
|
243
|
+
- `@statewalker/db-sqlite` — `inTransaction`, for `SqliteFilesApi`'s atomic steps.
|
|
244
|
+
- `@statewalker/webrun-files` — the `FilesApi` contract and path helpers.
|
|
199
245
|
|
|
200
|
-
|
|
246
|
+
The browser driver (`@statewalker/db-sqlite-browser`) is supported through the same `Db` contract, but
|
|
247
|
+
this package's tests run on `node:sqlite` and the Durable Object fake only.
|
|
201
248
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
of one file is capped there. Use `SqliteFilesApi` in those runtimes.
|
|
205
|
-
- Multi-row mutations (`copy`, `move`, `remove` of a tree) are not wrapped in a transaction.
|
|
206
|
-
- Only the final path component is resolved through symlinks.
|
|
207
|
-
- `list` loads every row under the directory, then sorts the entries into path order (implicit
|
|
208
|
-
directories and resolved links do not sit where the rows do) before applying `after`.
|
|
249
|
+
The drivers (`@statewalker/db-sqlite-node`, `@statewalker/db-sqlite-do`, …) are the caller's
|
|
250
|
+
choice and are not dependencies; neither is `pako`, which is passed in.
|
|
209
251
|
|
|
210
252
|
## License
|
|
211
253
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import type { Db } from "@statewalker/db-api";
|
|
1
2
|
import type { FileInfo, FileStats, FilesApi, ListOptions, ReadOptions } from "@statewalker/webrun-files";
|
|
2
|
-
import type { SqlDriver } from "../sql.types.js";
|
|
3
3
|
import { type StreamCodec } from "./stream-codec.js";
|
|
4
4
|
export interface SqliteFilesApiOptions {
|
|
5
5
|
/** Prefix of the three table names. Defaults to "fs_". */
|
|
@@ -20,7 +20,7 @@ export interface SqliteFilesApiOptions {
|
|
|
20
20
|
* A FilesApi over three SQLite tables: paths point at immutable contents, and
|
|
21
21
|
* a content is a sequence of bounded, optionally compressed blocks. Reads and
|
|
22
22
|
* writes stream one block at a time, so no file is ever held whole and no row
|
|
23
|
-
* outgrows the 2 MB limit of Durable Objects
|
|
23
|
+
* outgrows the 2 MB row limit of Durable Objects.
|
|
24
24
|
*
|
|
25
25
|
* Streaming happens outside transactions; every step that changes what a path
|
|
26
26
|
* shows is one transaction, so an interruption leaves at most an unreferenced
|
|
@@ -28,7 +28,7 @@ export interface SqliteFilesApiOptions {
|
|
|
28
28
|
*/
|
|
29
29
|
export declare class SqliteFilesApi implements FilesApi {
|
|
30
30
|
#private;
|
|
31
|
-
constructor(
|
|
31
|
+
constructor(db: Db, opts?: SqliteFilesApiOptions);
|
|
32
32
|
/** Creates the tables and indexes if missing. Await it before any other call. */
|
|
33
33
|
init(): Promise<void>;
|
|
34
34
|
read(path: string, options?: ReadOptions): AsyncIterable<Uint8Array>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-files-api.d.ts","sourceRoot":"","sources":["../../src/blocks/sqlite-files-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;
|
|
1
|
+
{"version":3,"file":"sqlite-files-api.d.ts","sourceRoot":"","sources":["../../src/blocks/sqlite-files-api.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,qBAAqB,CAAC;AAE9C,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAMnC,OAAO,EAAE,KAAK,WAAW,EAAmB,MAAM,mBAAmB,CAAC;AAOtE,MAAM,WAAW,qBAAqB;IACpC,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB;;;OAGG;IACH,WAAW,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACjC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAcD;;;;;;;;;GASG;AACH,qBAAa,cAAe,YAAW,QAAQ;;IAS7C,YAAY,EAAE,EAAE,EAAE,EAAE,IAAI,GAAE,qBAA0B,EAqBnD;IAED,iFAAiF;IAC3E,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CA0B1B;IAIM,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,aAAa,CAAC,UAAU,CAAC,CA8C9E;IAID;;;;OAIG;IACG,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC,CAuEf;IA8CK,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQvC;IAIK,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAKxD;IAEK,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAE3C;IAEM,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,aAAa,CAAC,QAAQ,CAAC,CA4B5E;IAIK,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAG3C;IAgCK,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAE3D;IAEK,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAE3D;IA6CD;;;;;;;;OAQG;IACG,KAAK,CAAC,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAqB3D;CAqEF"}
|
package/dist/bytes.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/** BLOBs arrive as Uint8Array from every @statewalker/db-sqlite driver; SQLAR may hold TEXT. */
|
|
2
2
|
export declare function toBytes(value: unknown): Uint8Array;
|
|
3
3
|
//# sourceMappingURL=bytes.d.ts.map
|
package/dist/bytes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bytes.d.ts","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,
|
|
1
|
+
{"version":3,"file":"bytes.d.ts","sourceRoot":"","sources":["../src/bytes.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,wBAAgB,OAAO,CAAC,KAAK,EAAE,OAAO,GAAG,UAAU,CAIlD"}
|