@zukhruf/sqlite-data-version 0.0.0-stage → 0.3.31
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 +21 -0
- package/README.md +100 -2
- package/dist/data-version.d.ts +8 -0
- package/dist/data-version.d.ts.map +1 -0
- package/dist/data-version.js +35 -0
- package/dist/data-version.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/package.json +40 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ezzabuzaid
|
|
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
CHANGED
|
@@ -1,3 +1,101 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @zukhruf/sqlite-data-version
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Some programs keep their data in a SQLite database, and write it while they run. Another program wants to know when the data changes, so that it reads again. A file-change event does not tell it: the owner keeps the file open and commits through a WAL, and the event comes only when the file closes. SQLite gives a number for this: `PRAGMA data_version`. It changes on each commit by another connection. This package keeps its own read-only connection to the database, and gives that number. Poll it, and read again when it changes.
|
|
4
|
+
|
|
5
|
+
The words in these documents have one meaning each. See the glossary in [CONTEXT.md](./CONTEXT.md).
|
|
6
|
+
|
|
7
|
+
## Use it
|
|
8
|
+
|
|
9
|
+
This project is an experiment. It uses only `node:sqlite`.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @zukhruf/sqlite-data-version
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { mkdtempDisposable } from 'node:fs/promises';
|
|
17
|
+
import { tmpdir } from 'node:os';
|
|
18
|
+
import { join } from 'node:path';
|
|
19
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
20
|
+
|
|
21
|
+
import { DataVersion } from '@zukhruf/sqlite-data-version';
|
|
22
|
+
|
|
23
|
+
class Unavailable extends Error {
|
|
24
|
+
constructor(path: string, cause: unknown) {
|
|
25
|
+
super(`Cannot read ${path}`, { cause });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
await using scratch = await mkdtempDisposable(join(tmpdir(), 'data-version-'));
|
|
30
|
+
const path = join(scratch.path, 'notes.sqlite');
|
|
31
|
+
|
|
32
|
+
// The owner: another connection that writes the database through a WAL.
|
|
33
|
+
using owner = new DatabaseSync(path);
|
|
34
|
+
owner.exec(`PRAGMA journal_mode = WAL;
|
|
35
|
+
CREATE TABLE notes (id INTEGER PRIMARY KEY, title TEXT)`);
|
|
36
|
+
|
|
37
|
+
using version = new DataVersion(path, Unavailable);
|
|
38
|
+
let seen = version.current;
|
|
39
|
+
for (const title of ['first', 'second']) {
|
|
40
|
+
owner.prepare('INSERT INTO notes (title) VALUES (?)').run(title);
|
|
41
|
+
const now = version.current;
|
|
42
|
+
console.log(now !== seen ? `changed after ${title}` : 'no change');
|
|
43
|
+
seen = now;
|
|
44
|
+
}
|
|
45
|
+
console.log(version.current === seen ? 'no change' : 'changed');
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Output:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
changed after first
|
|
52
|
+
changed after second
|
|
53
|
+
no change
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## The rules
|
|
57
|
+
|
|
58
|
+
- `new DataVersion(path, unavailable)` opens its own read-only connection to the database. It does not share a connection with a reader. [ADR 0001](./docs/adr/0001-poll-data-version.md) tells why.
|
|
59
|
+
- `current` gives the data version now. Compare it with the value that you read before. A different value tells that another connection committed. The same value tells that no other connection committed.
|
|
60
|
+
- The number tells only that a change occurred. It does not count the commits, and it does not tell what changed.
|
|
61
|
+
- A commit by the same connection does not change the number. The `DataVersion` connection never writes, so each commit that it sees comes from another connection.
|
|
62
|
+
- Dispose closes the connection. Use `using`, or call `[Symbol.dispose]()`.
|
|
63
|
+
- The caller gives the error class. Thus the error names what the caller needs, for example the access that the user must give.
|
|
64
|
+
|
|
65
|
+
## Errors
|
|
66
|
+
|
|
67
|
+
| Error | When |
|
|
68
|
+
| ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `new unavailable(path, cause)` | SQLite cannot open the file: `SQLITE_CANTOPEN` (14), for a missing file or a file that the system denies, a privacy denial too. |
|
|
70
|
+
| The error of SQLite | All other failures. |
|
|
71
|
+
|
|
72
|
+
## The same concepts in other systems
|
|
73
|
+
|
|
74
|
+
| System | What it does |
|
|
75
|
+
| -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
76
|
+
| SQLite `PRAGMA data_version` | It gives a number that changes when another connection commits. The SQLite documentation tells to use it to find out when to read again. |
|
|
77
|
+
| File-change events (FSEvents, inotify) | They tell that a file changed. A process that keeps the file open and writes a WAL can commit many times before a write that an event reports. |
|
|
78
|
+
| Datasette `immutable` mode | It opens a file that nobody writes, so the data never changes. This package is the opposite case: the owner writes the file. |
|
|
79
|
+
|
|
80
|
+
## Documentation
|
|
81
|
+
|
|
82
|
+
- [0001 Poll data_version](./docs/adr/0001-poll-data-version.md)
|
|
83
|
+
- [Code copied into this package](./docs/copied-code.md)
|
|
84
|
+
|
|
85
|
+
## Development
|
|
86
|
+
|
|
87
|
+
This package is part of the [zukhruf](../../../README.md) workspace. Run the commands from the workspace root.
|
|
88
|
+
|
|
89
|
+
```sh
|
|
90
|
+
npm install
|
|
91
|
+
npx nx run sqlite-data-version:test # builds, then runs the tests in src/
|
|
92
|
+
npx nx run sqlite-data-version:typecheck # formats, lints, then type checks
|
|
93
|
+
npx nx run sqlite-data-version:build # compiles src/ to dist/
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
src/
|
|
98
|
+
index.ts the exports
|
|
99
|
+
data-version.ts DataVersion and UnavailableError
|
|
100
|
+
data-version.test.ts the tests, on real SQLite files
|
|
101
|
+
```
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type UnavailableError = new (path: string, cause: unknown) => Error;
|
|
2
|
+
export declare class DataVersion implements Disposable {
|
|
3
|
+
#private;
|
|
4
|
+
constructor(path: string, unavailable: UnavailableError);
|
|
5
|
+
get current(): number;
|
|
6
|
+
[Symbol.dispose](): void;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=data-version.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-version.d.ts","sourceRoot":"","sources":["../src/data-version.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,gBAAgB,GAAG,KAAK,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,KAAK,CAAC;AAuB3E,qBAAa,WAAY,YAAW,UAAU;;gBAIhC,IAAI,EAAE,MAAM,EAAE,WAAW,EAAE,gBAAgB;IAKvD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,IAAI;CAGzB"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
function open(path, unavailable) {
|
|
3
|
+
try {
|
|
4
|
+
return new DatabaseSync(path, { readOnly: true });
|
|
5
|
+
}
|
|
6
|
+
catch (cause) {
|
|
7
|
+
if (cause instanceof Error &&
|
|
8
|
+
'errcode' in cause &&
|
|
9
|
+
// SQLite's CANTOPEN (14). A missing or denied file fails the open()
|
|
10
|
+
// syscall, and SQLite reports that as CANTOPEN, a privacy denial too.
|
|
11
|
+
// SQLite gives AUTH only for an authorizer, and this private connection
|
|
12
|
+
// has none. Any other failure is not one that access can fix.
|
|
13
|
+
cause.errcode === 14)
|
|
14
|
+
throw new unavailable(path, cause);
|
|
15
|
+
throw cause;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// The owner commits through a WAL it keeps open, and file-change events
|
|
19
|
+
// report a write only when the file closes. SQLite's data_version changes on
|
|
20
|
+
// every commit by another connection, so polling current sees each one.
|
|
21
|
+
export class DataVersion {
|
|
22
|
+
#database;
|
|
23
|
+
#version;
|
|
24
|
+
constructor(path, unavailable) {
|
|
25
|
+
this.#database = open(path, unavailable);
|
|
26
|
+
this.#version = this.#database.prepare('PRAGMA data_version');
|
|
27
|
+
}
|
|
28
|
+
get current() {
|
|
29
|
+
return Number(this.#version.get()?.data_version);
|
|
30
|
+
}
|
|
31
|
+
[Symbol.dispose]() {
|
|
32
|
+
this.#database.close();
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//# sourceMappingURL=data-version.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"data-version.js","sourceRoot":"","sources":["../src/data-version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAsB,MAAM,aAAa,CAAC;AAM/D,SAAS,IAAI,CAAC,IAAY,EAAE,WAA6B;IACvD,IAAI,CAAC;QACH,OAAO,IAAI,YAAY,CAAC,IAAI,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;IACpD,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IACE,KAAK,YAAY,KAAK;YACtB,SAAS,IAAI,KAAK;YAClB,oEAAoE;YACpE,sEAAsE;YACtE,wEAAwE;YACxE,8DAA8D;YAC9D,KAAK,CAAC,OAAO,KAAK,EAAE;YAEpB,MAAM,IAAI,WAAW,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACrC,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC;AAED,wEAAwE;AACxE,6EAA6E;AAC7E,wEAAwE;AACxE,MAAM,OAAO,WAAW;IACb,SAAS,CAAe;IACxB,QAAQ,CAAgB;IAEjC,YAAY,IAAY,EAAE,WAA6B;QACrD,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC,IAAI,EAAE,WAAW,CAAC,CAAC;QACzC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAChE,CAAC;IAED,IAAI,OAAO;QACT,OAAO,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,YAAY,CAAC,CAAC;IACnD,CAAC;IAED,CAAC,MAAM,CAAC,OAAO,CAAC;QACd,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;IACzB,CAAC;CACF"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,KAAK,gBAAgB,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAyB,MAAM,mBAAmB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,42 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zukhruf/sqlite-data-version",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.31",
|
|
4
|
+
"description": "The data version of a SQLite database that another process owns and writes. It polls PRAGMA data_version on its own read-only connection, which changes on each commit by another connection, also while the owner keeps the file open.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/JanuaryLabs/zukhruf",
|
|
9
|
+
"directory": "packages/sqlite/data-version"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/JanuaryLabs/zukhruf/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/JanuaryLabs/zukhruf/tree/main/packages/sqlite/data-version#readme",
|
|
15
|
+
"keywords": [
|
|
16
|
+
"sqlite",
|
|
17
|
+
"node-sqlite",
|
|
18
|
+
"data-version",
|
|
19
|
+
"pragma",
|
|
20
|
+
"wal",
|
|
21
|
+
"change-detection"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"exports": {
|
|
25
|
+
"./package.json": "./package.json",
|
|
26
|
+
".": {
|
|
27
|
+
"@zukhruf/source": "./src/index.ts",
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"!dist/spec/**",
|
|
36
|
+
"!**/*.tsbuildinfo",
|
|
37
|
+
"!**/*.test.*"
|
|
38
|
+
],
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|