@mark1russell7/sqlite 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +187 -2
- package/build/database.d.ts +36 -0
- package/build/database.d.ts.map +1 -0
- package/build/database.js +47 -0
- package/build/database.js.map +1 -0
- package/build/event-log.d.ts +135 -0
- package/build/event-log.d.ts.map +1 -0
- package/build/event-log.js +201 -0
- package/build/event-log.js.map +1 -0
- package/build/faults.d.ts +41 -0
- package/build/faults.d.ts.map +1 -0
- package/build/faults.js +90 -0
- package/build/faults.js.map +1 -0
- package/build/index.d.ts +13 -0
- package/build/index.js +6 -0
- package/build/migrate.d.ts +57 -0
- package/build/migrate.d.ts.map +1 -0
- package/build/migrate.js +75 -0
- package/build/migrate.js.map +1 -0
- package/build/transaction.d.ts +30 -0
- package/build/transaction.d.ts.map +1 -0
- package/build/transaction.js +36 -0
- package/build/transaction.js.map +1 -0
- package/build/warning.d.ts +32 -0
- package/build/warning.d.ts.map +1 -0
- package/build/warning.js +49 -0
- package/build/warning.js.map +1 -0
- package/package.json +71 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mark Russell
|
|
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,188 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @mark1russell7/sqlite
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Databases on one computer through `node:sqlite`. The package has the connection with its pragmas, the migrations by schema version and the write transaction. It also has a typed append-only event log and the faults of SQLite. It has no dependencies.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @mark1russell7/sqlite
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
The package needs Node.js 22.16 or later.
|
|
10
|
+
|
|
11
|
+
## The connection
|
|
12
|
+
|
|
13
|
+
`openDatabase` opens a `DatabaseSync` and sets its pragmas. It sets the busy timeout first, because the change to WAL needs the lock of the file.
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { openDatabase } from "@mark1russell7/sqlite";
|
|
17
|
+
|
|
18
|
+
const db = openDatabase("data/app.db", { synchronous: "NORMAL", busyTimeoutMs: 5000, makeFolder: true });
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
| Option | Default | What it sets |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| `wal` | `true` | The write-ahead log. Then readers do not stop a writer. |
|
|
24
|
+
| `synchronous` | `FULL` | The pragma `synchronous`. With WAL, `NORMAL` can lose the last commits at a power cut. The file stays good. |
|
|
25
|
+
| `busyTimeoutMs` | `0` | The time that a statement waits for the lock of a different connection. With `0`, the statement fails at once. |
|
|
26
|
+
| `makeFolder` | `false` | The function makes the folder of the file. `folderMode` sets the mode of the new folders. |
|
|
27
|
+
|
|
28
|
+
Each default other than `wal` is the default of SQLite.
|
|
29
|
+
|
|
30
|
+
## Migrations
|
|
31
|
+
|
|
32
|
+
`migrate` does the steps that a file does not have. The `user_version` of the file is the count of steps that it has. A step is SQL text or a function.
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
import { migrate, type Migration } from "@mark1russell7/sqlite";
|
|
36
|
+
|
|
37
|
+
const steps: Migration[] = [
|
|
38
|
+
"CREATE TABLE notes (id TEXT PRIMARY KEY, text TEXT NOT NULL) STRICT",
|
|
39
|
+
(db) => db.exec("CREATE INDEX notes_text ON notes (text)"),
|
|
40
|
+
];
|
|
41
|
+
migrate(db, steps); // { from: 0, to: 2 }
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
- Each step is one transaction with its new `user_version`. A step that fails rolls back alone.
|
|
45
|
+
- The function reads the version again in each transaction, so two processes can migrate one file at the same time.
|
|
46
|
+
- A file with more steps than the list is from a newer version of the app, and the function throws.
|
|
47
|
+
- Do not change a step that a file can have. Add a new step.
|
|
48
|
+
|
|
49
|
+
### A version for each part of a file
|
|
50
|
+
|
|
51
|
+
A library and the app can keep their tables in one file. Each part then has its own list of steps and its own version. The option `part` names the part.
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import { migrate, schemaVersion } from "@mark1russell7/sqlite";
|
|
55
|
+
|
|
56
|
+
migrate(db, appSteps, { part: "app" });
|
|
57
|
+
schemaVersion(db, "app"); // the count of steps of the part app
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
- The versions of the parts are in the table `schema_versions`. The `user_version` of the file does not change.
|
|
61
|
+
- A migration without `part` uses the `user_version`, as before.
|
|
62
|
+
- The option `name` sets the name of the database in the error of a newer file.
|
|
63
|
+
|
|
64
|
+
## Transactions
|
|
65
|
+
|
|
66
|
+
`transaction` does the work in one `BEGIN IMMEDIATE` transaction and gives the result of the work. The write lock comes at the start, so a different writer cannot cause a failure in the middle of the work.
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import { transaction } from "@mark1russell7/sqlite";
|
|
70
|
+
|
|
71
|
+
const added = transaction(db, () => {
|
|
72
|
+
db.prepare("INSERT INTO notes VALUES (?, ?)").run("n1", "first");
|
|
73
|
+
return 1;
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- When the work or the commit throws, the function rolls back and throws the same error.
|
|
78
|
+
- The work is synchronous. A work that gives a promise is an error.
|
|
79
|
+
- A call in a transaction throws, because SQLite refuses a nested `BEGIN`.
|
|
80
|
+
- `onRollbackError` gets the error of a rollback that did not end.
|
|
81
|
+
|
|
82
|
+
## The event log
|
|
83
|
+
|
|
84
|
+
The event log is a typed append-only list of events, and a grow-only set: an event that the log has already changes nothing. Thus two copies that exchange their events get the same set, in each order of arrival.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { openEventLog, type LogRecord } from "@mark1russell7/sqlite";
|
|
88
|
+
|
|
89
|
+
interface Added extends LogRecord {
|
|
90
|
+
readonly type: "added";
|
|
91
|
+
readonly name: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const names = new Set<string>();
|
|
95
|
+
const accept = (event: Added): string | undefined => {
|
|
96
|
+
if (names.has(event.name)) return "the name is in use";
|
|
97
|
+
names.add(event.name);
|
|
98
|
+
return undefined;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const log = openEventLog("data/events.db", (value) => value as Added);
|
|
102
|
+
for (const event of log.read()) accept(event);
|
|
103
|
+
log.append({ id: "e1", at: new Date().toISOString(), type: "added", name: "a" }, accept); // { kind: "added", seq: 1 }
|
|
104
|
+
log.append({ id: "e1", at: "2026-11-14T00:00:00Z", type: "added", name: "b" }); // { kind: "conflict", seq: 1, stored: ... }
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`append` gives one result, and `appendAll` gives one result for each event, in one transaction:
|
|
108
|
+
|
|
109
|
+
| Result | When | What changes |
|
|
110
|
+
| --- | --- | --- |
|
|
111
|
+
| `added` | The log does not have the id, and `accept` accepts the event. | The log writes the event at the position `seq`. |
|
|
112
|
+
| `duplicate` | The log has an equal event with the same id. | Nothing. `accept` does not get the event. |
|
|
113
|
+
| `conflict` | The log has a different event with the same id. | Nothing. The result has the stored event. |
|
|
114
|
+
| `refused` | `accept` gives a problem. | Nothing. The result has the problem. |
|
|
115
|
+
|
|
116
|
+
- The default equality compares the JSON text of the two events. The option `equal` sets a different equality.
|
|
117
|
+
- Give each event an id from its content, for example a hash. Then a second report of the same fact is a duplicate.
|
|
118
|
+
- `accept` is optional. It gets only an event with a new id, before the write. Then the state of the caller and the file stay equal.
|
|
119
|
+
|
|
120
|
+
### Reads
|
|
121
|
+
|
|
122
|
+
- `read()` gives the events in the order of the log. `entries()` gives each event with its position `seq`.
|
|
123
|
+
- The option `after` gives only the events after a position. `types` gives only the events of some types. `limit` sets the highest count.
|
|
124
|
+
- `head()` gives the position of the last event. A read with `after: head` later gives only the new events.
|
|
125
|
+
- `get(id)` gives the event with an id and its position.
|
|
126
|
+
|
|
127
|
+
### A log in the file of the app
|
|
128
|
+
|
|
129
|
+
`eventLog` opens a log in a table of a connection of the caller. The app keeps its own tables in the same file, and one file can have many logs.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
import { eventLog, migrate, openDatabase } from "@mark1russell7/sqlite";
|
|
133
|
+
|
|
134
|
+
const db = openDatabase("data/app.db", { busyTimeoutMs: 5000 });
|
|
135
|
+
migrate(db, appSteps, { part: "app" });
|
|
136
|
+
const reports = eventLog(db, { parse: parseReport, table: "reports" });
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
- The log keeps its version in the part `event-log:<table>`. `EVENT_LOG_SCHEMA_VERSION` is the version that it writes.
|
|
140
|
+
- The log does not close the connection. A call of the log in a transaction of the caller throws.
|
|
141
|
+
- `openEventLog` opens a log in its own file, with the table `events`. That log owns the `user_version` of its file.
|
|
142
|
+
|
|
143
|
+
### A failed write
|
|
144
|
+
|
|
145
|
+
- A failed write or `accept` rolls back and breaks the log. Each later call throws a `BrokenLogError` until `recover`.
|
|
146
|
+
- After `recover`, the caller replays the log, because its state can have the events of the failed write.
|
|
147
|
+
- `recover` throws when the rollback did not end. Then the error has `stuck: true`. Open the log again.
|
|
148
|
+
- A lock that is not free at the start does not break the log. The call throws, and nothing changes.
|
|
149
|
+
|
|
150
|
+
## The faults of SQLite
|
|
151
|
+
|
|
152
|
+
`sqliteFault` gives the kind of an error of SQLite and the action that it needs. The function reads the result code, not the text of the message.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
import { sqliteFault } from "@mark1russell7/sqlite";
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
log.append(event);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
const fault = sqliteFault(error);
|
|
161
|
+
if (fault?.action === "retry") schedule(event);
|
|
162
|
+
else throw error;
|
|
163
|
+
}
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
| Action | Kinds | What to do |
|
|
167
|
+
| --- | --- | --- |
|
|
168
|
+
| `retry` | `busy`, `locked`, `aborted`, `interrupted`, `protocol`, `schema` | Do the work again later. |
|
|
169
|
+
| `quarantine` | `corrupt`, `not-a-database` | Move the file away. Make the data again from its source or from a copy. |
|
|
170
|
+
| `operator` | `full`, `io`, `read-only`, `cannot-open`, `permission`, `no-memory`, `no-lfs`, `other` | Tell a person: the computer needs more disk space, an access, a path or memory. |
|
|
171
|
+
| `bug` | `sql`, `constraint`, `mismatch`, `misuse`, `range`, `too-big`, `not-found`, `internal`, `authorization` | Correct the program. The same work fails again. |
|
|
172
|
+
|
|
173
|
+
- The function reads the error and then its chain of causes. Thus it finds the fault in a `BrokenLogError`.
|
|
174
|
+
- It gives `undefined` for an error that is not of SQLite.
|
|
175
|
+
- `SQLITE_FAULT_ACTIONS` is the table of the action of each kind.
|
|
176
|
+
|
|
177
|
+
## The experimental warning
|
|
178
|
+
|
|
179
|
+
Node.js 22 writes "SQLite is an experimental feature" to the standard error at the first load of `node:sqlite`. The entry `@mark1russell7/sqlite/warning` removes only that warning. The entry does not import `node:sqlite`.
|
|
180
|
+
|
|
181
|
+
```ts
|
|
182
|
+
import { quietSqliteWarning } from "@mark1russell7/sqlite/warning";
|
|
183
|
+
|
|
184
|
+
quietSqliteWarning();
|
|
185
|
+
const { openDatabase } = await import("@mark1russell7/sqlite");
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
A static import of `node:sqlite` loads before the code of the module. Thus import the database code after the filter, with `import()`. The function gives a function that removes the filter.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
/** The values of the pragma `synchronous`. */
|
|
3
|
+
export type Synchronous = "OFF" | "NORMAL" | "FULL" | "EXTRA";
|
|
4
|
+
/** The options of `openDatabase`. Each default is the default of SQLite, but the journal is WAL by default. */
|
|
5
|
+
export interface OpenOptions {
|
|
6
|
+
/**
|
|
7
|
+
* This value turns on the write-ahead log (WAL). The default is true. Then readers do not stop a writer. A database
|
|
8
|
+
* with no file keeps its journal in memory.
|
|
9
|
+
*/
|
|
10
|
+
readonly wal?: boolean | undefined;
|
|
11
|
+
/**
|
|
12
|
+
* This value is the pragma `synchronous`. The default is `FULL`: a commit is on the disk when the call ends. With WAL,
|
|
13
|
+
* `NORMAL` can lose the last commits at a power cut, and the file stays good.
|
|
14
|
+
*/
|
|
15
|
+
readonly synchronous?: Synchronous | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* This value is the time in milliseconds that a statement waits for the lock of a different connection. The default
|
|
18
|
+
* is 0: then the statement fails at once with "database is locked".
|
|
19
|
+
*/
|
|
20
|
+
readonly busyTimeoutMs?: number | undefined;
|
|
21
|
+
/** This value makes the folder of the file when the folder is not there. The default is false. */
|
|
22
|
+
readonly makeFolder?: boolean | undefined;
|
|
23
|
+
/** This value is the mode of the folders that `makeFolder` makes, for example `0o700`. */
|
|
24
|
+
readonly folderMode?: number | undefined;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* This function opens a database, and makes the file when it is not there. `:memory:` gives a database with no file.
|
|
28
|
+
*
|
|
29
|
+
* The function sets the busy timeout first, because the change to WAL needs the lock of the file. When a pragma
|
|
30
|
+
* fails, the function closes the connection and throws the error.
|
|
31
|
+
*
|
|
32
|
+
* @param path - The path of the file, or `:memory:`.
|
|
33
|
+
* @param options - The pragmas and the folder.
|
|
34
|
+
*/
|
|
35
|
+
export declare function openDatabase(path: string, options?: OpenOptions): DatabaseSync;
|
|
36
|
+
//# sourceMappingURL=database.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.d.ts","names":[],"sources":["../src/database.ts"],"mappings":";;YAUY;;iBAKK;;;;;WAKN;;;;;WAKA,cAAc;;;;;WAKd;;WAEA;;WAEA;;;;;;;;;;;wBAYK,aAAa,cAAc,UAAS,cAAmB"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
// src/database.ts
|
|
5
|
+
/**
|
|
6
|
+
* The connection to a database on one computer: a `DatabaseSync` of `node:sqlite` with the pragmas set.
|
|
7
|
+
*
|
|
8
|
+
* @packageDocumentation
|
|
9
|
+
*/
|
|
10
|
+
const SYNCHRONOUS = [
|
|
11
|
+
"OFF",
|
|
12
|
+
"NORMAL",
|
|
13
|
+
"FULL",
|
|
14
|
+
"EXTRA"
|
|
15
|
+
];
|
|
16
|
+
/**
|
|
17
|
+
* This function opens a database, and makes the file when it is not there. `:memory:` gives a database with no file.
|
|
18
|
+
*
|
|
19
|
+
* The function sets the busy timeout first, because the change to WAL needs the lock of the file. When a pragma
|
|
20
|
+
* fails, the function closes the connection and throws the error.
|
|
21
|
+
*
|
|
22
|
+
* @param path - The path of the file, or `:memory:`.
|
|
23
|
+
* @param options - The pragmas and the folder.
|
|
24
|
+
*/
|
|
25
|
+
function openDatabase(path, options = {}) {
|
|
26
|
+
const synchronous = options.synchronous ?? "FULL";
|
|
27
|
+
if (!SYNCHRONOUS.includes(synchronous)) throw new RangeError(`The value of synchronous must be one of ${SYNCHRONOUS.join(", ")}, not ${String(synchronous)}.`);
|
|
28
|
+
const busyTimeoutMs = options.busyTimeoutMs ?? 0;
|
|
29
|
+
if (!Number.isInteger(busyTimeoutMs) || busyTimeoutMs < 0) throw new RangeError(`The busy timeout must be a whole number of milliseconds of 0 or more, not ${String(busyTimeoutMs)}.`);
|
|
30
|
+
if (options.makeFolder === true && path !== ":memory:") mkdirSync(dirname(path), options.folderMode === void 0 ? { recursive: true } : {
|
|
31
|
+
recursive: true,
|
|
32
|
+
mode: options.folderMode
|
|
33
|
+
});
|
|
34
|
+
const db = new DatabaseSync(path);
|
|
35
|
+
try {
|
|
36
|
+
db.exec(`PRAGMA busy_timeout = ${String(busyTimeoutMs)}`);
|
|
37
|
+
if (options.wal ?? true) db.exec("PRAGMA journal_mode = WAL");
|
|
38
|
+
db.exec(`PRAGMA synchronous = ${synchronous}`);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
db.close();
|
|
41
|
+
throw error;
|
|
42
|
+
}
|
|
43
|
+
return db;
|
|
44
|
+
}
|
|
45
|
+
export { openDatabase };
|
|
46
|
+
|
|
47
|
+
//# sourceMappingURL=database.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"database.js","names":[],"sources":["../src/database.ts"],"sourcesContent":["/**\n * The connection to a database on one computer: a `DatabaseSync` of `node:sqlite` with the pragmas set.\n *\n * @packageDocumentation\n */\nimport { mkdirSync } from \"node:fs\";\nimport { dirname } from \"node:path\";\nimport { DatabaseSync } from \"node:sqlite\";\n\n/** The values of the pragma `synchronous`. */\nexport type Synchronous = \"OFF\" | \"NORMAL\" | \"FULL\" | \"EXTRA\";\n\nconst SYNCHRONOUS: readonly Synchronous[] = [\"OFF\", \"NORMAL\", \"FULL\", \"EXTRA\"];\n\n/** The options of `openDatabase`. Each default is the default of SQLite, but the journal is WAL by default. */\nexport interface OpenOptions {\n /**\n * This value turns on the write-ahead log (WAL). The default is true. Then readers do not stop a writer. A database\n * with no file keeps its journal in memory.\n */\n readonly wal?: boolean | undefined;\n /**\n * This value is the pragma `synchronous`. The default is `FULL`: a commit is on the disk when the call ends. With WAL,\n * `NORMAL` can lose the last commits at a power cut, and the file stays good.\n */\n readonly synchronous?: Synchronous | undefined;\n /**\n * This value is the time in milliseconds that a statement waits for the lock of a different connection. The default\n * is 0: then the statement fails at once with \"database is locked\".\n */\n readonly busyTimeoutMs?: number | undefined;\n /** This value makes the folder of the file when the folder is not there. The default is false. */\n readonly makeFolder?: boolean | undefined;\n /** This value is the mode of the folders that `makeFolder` makes, for example `0o700`. */\n readonly folderMode?: number | undefined;\n}\n\n/**\n * This function opens a database, and makes the file when it is not there. `:memory:` gives a database with no file.\n *\n * The function sets the busy timeout first, because the change to WAL needs the lock of the file. When a pragma\n * fails, the function closes the connection and throws the error.\n *\n * @param path - The path of the file, or `:memory:`.\n * @param options - The pragmas and the folder.\n */\nexport function openDatabase(path: string, options: OpenOptions = {}): DatabaseSync {\n const synchronous = options.synchronous ?? \"FULL\";\n if (!SYNCHRONOUS.includes(synchronous)) {\n throw new RangeError(`The value of synchronous must be one of ${SYNCHRONOUS.join(\", \")}, not ${String(synchronous)}.`);\n }\n const busyTimeoutMs = options.busyTimeoutMs ?? 0;\n if (!Number.isInteger(busyTimeoutMs) || busyTimeoutMs < 0) {\n throw new RangeError(`The busy timeout must be a whole number of milliseconds of 0 or more, not ${String(busyTimeoutMs)}.`);\n }\n if (options.makeFolder === true && path !== \":memory:\") {\n mkdirSync(dirname(path), options.folderMode === undefined ? { recursive: true } : { recursive: true, mode: options.folderMode });\n }\n const db = new DatabaseSync(path);\n try {\n db.exec(`PRAGMA busy_timeout = ${String(busyTimeoutMs)}`);\n if (options.wal ?? true) db.exec(\"PRAGMA journal_mode = WAL\");\n db.exec(`PRAGMA synchronous = ${synchronous}`);\n } catch (error) {\n db.close();\n throw error;\n }\n return db;\n}\n"],"mappings":";;;;;;;;;AAYA,MAAM,cAAsC;CAAC;CAAO;CAAU;CAAQ;AAAO;;;;;;;;;;AAkC7E,SAAgB,aAAa,MAAc,UAAuB,CAAC,GAAiB;CAClF,MAAM,cAAc,QAAQ,eAAe;CAC3C,IAAI,CAAC,YAAY,SAAS,WAAW,GACnC,MAAM,IAAI,WAAW,2CAA2C,YAAY,KAAK,IAAI,EAAE,QAAQ,OAAO,WAAW,EAAE,EAAE;CAEvH,MAAM,gBAAgB,QAAQ,iBAAiB;CAC/C,IAAI,CAAC,OAAO,UAAU,aAAa,KAAK,gBAAgB,GACtD,MAAM,IAAI,WAAW,6EAA6E,OAAO,aAAa,EAAE,EAAE;CAE5H,IAAI,QAAQ,eAAe,QAAQ,SAAS,YAC1C,UAAU,QAAQ,IAAI,GAAG,QAAQ,eAAe,KAAA,IAAY,EAAE,WAAW,KAAK,IAAI;EAAE,WAAW;EAAM,MAAM,QAAQ;CAAW,CAAC;CAEjI,MAAM,KAAK,IAAI,aAAa,IAAI;CAChC,IAAI;EACF,GAAG,KAAK,yBAAyB,OAAO,aAAa,GAAG;EACxD,IAAI,QAAQ,OAAO,MAAM,GAAG,KAAK,2BAA2B;EAC5D,GAAG,KAAK,wBAAwB,aAAa;CAC/C,SAAS,OAAO;EACd,GAAG,MAAM;EACT,MAAM;CACR;CACA,OAAO;AACT"}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A typed append-only log of events in SQLite. The log is a grow-only set of events with an order: an event that the log
|
|
3
|
+
* has already changes nothing. The caller keeps a state, and the log keeps the file equal to it.
|
|
4
|
+
*
|
|
5
|
+
* @packageDocumentation
|
|
6
|
+
*/
|
|
7
|
+
import { OpenOptions } from "./database.js";
|
|
8
|
+
import { DatabaseSync } from "node:sqlite";
|
|
9
|
+
/** The fields that the log reads from each event. The log keeps the full event as JSON. */
|
|
10
|
+
export interface LogRecord {
|
|
11
|
+
readonly id: string;
|
|
12
|
+
readonly at: string;
|
|
13
|
+
readonly type: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* This function accepts an event into the state of the caller, or refuses it. It gives a problem when it refuses the
|
|
17
|
+
* event, and then it must not change the state.
|
|
18
|
+
*/
|
|
19
|
+
export type Accept<E, P> = (event: E) => P | undefined;
|
|
20
|
+
/** This function tells if a new event is equal to the stored event with the same id. */
|
|
21
|
+
export type Equal<E> = (stored: E, event: E) => boolean;
|
|
22
|
+
/**
|
|
23
|
+
* The result of the append of one event.
|
|
24
|
+
*
|
|
25
|
+
* - `added`: the log did not have the id, `accept` accepted the event, and the log wrote it at the position `seq`.
|
|
26
|
+
* - `duplicate`: the log has an equal event at the position `seq`. Nothing changed, and `accept` did not get the event.
|
|
27
|
+
* - `conflict`: the log has a different event with the same id at the position `seq`. Nothing changed.
|
|
28
|
+
* - `refused`: `accept` refused the event with the problem `problem`. Nothing changed.
|
|
29
|
+
*/
|
|
30
|
+
export type Appended<E, P> = {
|
|
31
|
+
readonly kind: "added";
|
|
32
|
+
readonly seq: number;
|
|
33
|
+
} | {
|
|
34
|
+
readonly kind: "duplicate";
|
|
35
|
+
readonly seq: number;
|
|
36
|
+
} | {
|
|
37
|
+
readonly kind: "conflict";
|
|
38
|
+
readonly seq: number;
|
|
39
|
+
readonly stored: E;
|
|
40
|
+
} | {
|
|
41
|
+
readonly kind: "refused";
|
|
42
|
+
readonly problem: P;
|
|
43
|
+
};
|
|
44
|
+
/** One event of a log and its position. The first position is 1, and each new event gets a higher position. */
|
|
45
|
+
export interface LogEntry<E> {
|
|
46
|
+
readonly seq: number;
|
|
47
|
+
readonly event: E;
|
|
48
|
+
}
|
|
49
|
+
/** The options of a read of a log. */
|
|
50
|
+
export interface ReadOptions {
|
|
51
|
+
/** This value is a position. The read gives only the events after it. The default is 0, so the read starts at the first event. */
|
|
52
|
+
readonly after?: number | undefined;
|
|
53
|
+
/** This value is a list of types. The read gives only the events of these types. The default is each type. */
|
|
54
|
+
readonly types?: readonly string[] | undefined;
|
|
55
|
+
/** This value is the highest count of events that the read gives. The default is no limit. */
|
|
56
|
+
readonly limit?: number | undefined;
|
|
57
|
+
}
|
|
58
|
+
/** An open log. */
|
|
59
|
+
export interface EventLog<E extends LogRecord> {
|
|
60
|
+
/** This function gives the events of the log, in the order of the log. */
|
|
61
|
+
read(options?: ReadOptions): Generator<E>;
|
|
62
|
+
/** This function gives the events of the log with their positions, in the order of the log. */
|
|
63
|
+
entries(options?: ReadOptions): Generator<LogEntry<E>>;
|
|
64
|
+
/** This function gives the event with an id and its position, or `undefined` when the log does not have the id. */
|
|
65
|
+
get(id: string): LogEntry<E> | undefined;
|
|
66
|
+
/** This function gives the position of the last event, or 0 for an empty log. A read after it gives only new events. */
|
|
67
|
+
head(): number;
|
|
68
|
+
/**
|
|
69
|
+
* This function appends one event. An event that the log has already changes nothing. `accept` gets only an event with
|
|
70
|
+
* a new id, before the write. The default `accept` accepts each event.
|
|
71
|
+
*/
|
|
72
|
+
append<P = never>(event: E, accept?: Accept<E, P>): Appended<E, P>;
|
|
73
|
+
/** This function does `append` for each event, in one transaction, and gives the result of each event in order. */
|
|
74
|
+
appendAll<P = never>(events: Iterable<E>, accept?: Accept<E, P>): Appended<E, P>[];
|
|
75
|
+
/** This function gives the count of events in the log. */
|
|
76
|
+
count(): number;
|
|
77
|
+
/**
|
|
78
|
+
* This function makes a broken log usable again. It throws a `BrokenLogError` when the rollback of the failed write did
|
|
79
|
+
* not end, and then the log stays broken. After it, the caller replays the log, so its state is equal to the file again.
|
|
80
|
+
*/
|
|
81
|
+
recover(): void;
|
|
82
|
+
}
|
|
83
|
+
/** A log that owns its connection: `openEventLog` gives it. */
|
|
84
|
+
export interface EventLogFile<E extends LogRecord> extends EventLog<E> {
|
|
85
|
+
/** This function closes the connection of the log. */
|
|
86
|
+
close(): void;
|
|
87
|
+
}
|
|
88
|
+
/** The error of a call to a broken log. `stuck` is true when the rollback of the failed write did not end. */
|
|
89
|
+
export declare class BrokenLogError extends Error {
|
|
90
|
+
override readonly name = "BrokenLogError";
|
|
91
|
+
readonly stuck: boolean;
|
|
92
|
+
constructor(message: string, stuck: boolean, cause: unknown);
|
|
93
|
+
}
|
|
94
|
+
/** The options of `eventLog`. */
|
|
95
|
+
export interface EventLogOptions<E> {
|
|
96
|
+
/** This function changes a stored JSON value into an event. It throws on a value that is not an event. */
|
|
97
|
+
readonly parse: (value: unknown) => E;
|
|
98
|
+
/** This value is the name of the table of the log. The default is `events`. A file can have many logs in different tables. */
|
|
99
|
+
readonly table?: string | undefined;
|
|
100
|
+
/** This function tells if a new event is equal to the stored event with its id. The default compares the JSON text. */
|
|
101
|
+
readonly equal?: Equal<E> | undefined;
|
|
102
|
+
}
|
|
103
|
+
/** The options of `openEventLog`: the options of the connection, and the equality of events. */
|
|
104
|
+
export interface EventLogFileOptions<E> extends OpenOptions {
|
|
105
|
+
/** This function tells if a new event is equal to the stored event with its id. The default compares the JSON text. */
|
|
106
|
+
readonly equal?: Equal<E> | undefined;
|
|
107
|
+
}
|
|
108
|
+
/** The schema version of a log: the `user_version` of a file of `openEventLog`, or the version of the part `event-log:<table>`. */
|
|
109
|
+
export declare const EVENT_LOG_SCHEMA_VERSION: number;
|
|
110
|
+
/**
|
|
111
|
+
* This function opens a log in a table of a connection of the caller. The log does not close the connection.
|
|
112
|
+
*
|
|
113
|
+
* The log keeps its schema version in the part `event-log:<table>` of the file (refer to `migrate`). Thus the app keeps its own
|
|
114
|
+
* tables and its own steps in the same file. A call of the log in a transaction of the caller throws, and the log does not break.
|
|
115
|
+
*
|
|
116
|
+
* The state of the caller and the file stay equal: `accept` comes before the write. A write or an `accept` can fail with an
|
|
117
|
+
* error. Then the transaction rolls back, and the log is broken. Each later call throws a `BrokenLogError` until `recover`.
|
|
118
|
+
* The state of the caller can keep the events of the failed write, so the caller replays the log after `recover`.
|
|
119
|
+
*
|
|
120
|
+
* @param db - The connection. Set its pragmas with `openDatabase`.
|
|
121
|
+
* @param options - The parse of the events, the table and the equality of events.
|
|
122
|
+
*/
|
|
123
|
+
export declare function eventLog<E extends LogRecord>(db: DatabaseSync, options: EventLogOptions<E>): EventLog<E>;
|
|
124
|
+
/**
|
|
125
|
+
* This function opens a log in its own file, and makes the file when it is not there. `:memory:` gives a log with no file.
|
|
126
|
+
*
|
|
127
|
+
* The log owns the connection and the `user_version` of its file, and its table is `events`. A file of a newer schema
|
|
128
|
+
* version gives an error. The rules of `eventLog` apply.
|
|
129
|
+
*
|
|
130
|
+
* @param path - The path of the file, or `:memory:`.
|
|
131
|
+
* @param parse - The function that changes a stored JSON value into an event. It throws on a value that is not an event.
|
|
132
|
+
* @param options - The pragmas of the connection, and the equality of events. The defaults are the defaults of `openDatabase`.
|
|
133
|
+
*/
|
|
134
|
+
export declare function openEventLog<E extends LogRecord>(path: string, parse: (value: unknown) => E, options?: EventLogFileOptions<E>): EventLogFile<E>;
|
|
135
|
+
//# sourceMappingURL=event-log.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"event-log.d.ts","names":[],"sources":["../src/event-log.ts"],"mappings":";;;;;;;;;iBAYiB;WACN;WACA;WACA;;;;;;YAOC,OAAO,GAAG,MAAM,OAAO,MAAM;;YAG7B,MAAM,MAAM,QAAQ,GAAG,OAAO;;;;;;;;;YAU9B,SAAS,GAAG;WACT;WAAwB;;WACxB;WAA4B;;WAC5B;WAA2B;WAAsB,QAAQ;;WACzD;WAA0B,SAAS;;;iBAGjC,SAAS;WACf;WACA,OAAO;;;iBAID;;WAEN;;WAEA;;WAEA;;;iBAIM,SAAS,UAAU;;EAElC,KAAK,UAAU,cAAc,UAAU;;EAEvC,QAAQ,UAAU,cAAc,UAAU,SAAS;;EAEnD,IAAI,aAAa,SAAS;;EAE1B;;;;;EAKA,OAAO,WAAW,OAAO,GAAG,SAAS,OAAO,GAAG,KAAK,SAAS,GAAG;;EAEhE,UAAU,WAAW,QAAQ,SAAS,IAAI,SAAS,OAAO,GAAG,KAAK,SAAS,GAAG;;EAE9E;;;;;EAKA;;;iBAIe,aAAa,UAAU,mBAAmB,SAAS;;EAElE;;;qBAIW,uBAAuB;oBAChB;WACT;EAET,YAAY,iBAAiB,gBAAgB;;;iBAO9B,gBAAgB;;WAEtB,QAAQ,mBAAmB;;WAE3B;;WAEA,QAAQ,MAAM;;;iBAIR,oBAAoB,WAAW;;WAErC,QAAQ,MAAM;;;qBAkBZ;;;;;;;;;;;;;;wBA8BG,SAAS,UAAU,WAAW,IAAI,cAAc,SAAS,gBAAgB,KAAK,SAAS;;;;;;;;;;;wBAiBvF,aAAa,UAAU,WAAW,cAAc,QAAQ,mBAAmB,GAAG,UAAS,oBAAoB,KAAU,aAAa"}
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { openDatabase } from "./database.js";
|
|
2
|
+
import { SCHEMA_VERSIONS_TABLE, migrate } from "./migrate.js";
|
|
3
|
+
import { transaction } from "./transaction.js";
|
|
4
|
+
// src/event-log.ts
|
|
5
|
+
/** The error of a call to a broken log. `stuck` is true when the rollback of the failed write did not end. */
|
|
6
|
+
var BrokenLogError = class extends Error {
|
|
7
|
+
name = "BrokenLogError";
|
|
8
|
+
stuck;
|
|
9
|
+
constructor(message, stuck, cause) {
|
|
10
|
+
super(message, { cause });
|
|
11
|
+
this.stuck = stuck;
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
/** The steps of the schema of a log in a table. Do not change a step: add a new step. */
|
|
15
|
+
function logSteps(table) {
|
|
16
|
+
return [`CREATE TABLE ${table} (
|
|
17
|
+
seq INTEGER PRIMARY KEY,
|
|
18
|
+
id TEXT NOT NULL UNIQUE,
|
|
19
|
+
at TEXT NOT NULL,
|
|
20
|
+
type TEXT NOT NULL,
|
|
21
|
+
body TEXT NOT NULL
|
|
22
|
+
) STRICT`, `CREATE INDEX ${table}_type ON ${table} (type, seq)`];
|
|
23
|
+
}
|
|
24
|
+
/** The schema version of a log: the `user_version` of a file of `openEventLog`, or the version of the part `event-log:<table>`. */
|
|
25
|
+
const EVENT_LOG_SCHEMA_VERSION = logSteps("events").length;
|
|
26
|
+
/** The default name of the table of a log. */
|
|
27
|
+
const DEFAULT_TABLE = "events";
|
|
28
|
+
/** This function throws when a name cannot be the table of a log. */
|
|
29
|
+
function checkTable(table) {
|
|
30
|
+
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table) || table.toLowerCase().startsWith("sqlite_") || table === "schema_versions") throw new RangeError(`The table of a log must be a name of letters, digits and underscores, and not ${SCHEMA_VERSIONS_TABLE} or sqlite_*, not ${JSON.stringify(table)}.`);
|
|
31
|
+
}
|
|
32
|
+
/** This function throws when a value is not a whole number at or above a lowest value. */
|
|
33
|
+
function checkWhole(what, value, lowest) {
|
|
34
|
+
if (!Number.isInteger(value) || value < lowest) throw new RangeError(`${what} must be a whole number of ${String(lowest)} or more, not ${String(value)}.`);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* This function opens a log in a table of a connection of the caller. The log does not close the connection.
|
|
38
|
+
*
|
|
39
|
+
* The log keeps its schema version in the part `event-log:<table>` of the file (refer to `migrate`). Thus the app keeps its own
|
|
40
|
+
* tables and its own steps in the same file. A call of the log in a transaction of the caller throws, and the log does not break.
|
|
41
|
+
*
|
|
42
|
+
* The state of the caller and the file stay equal: `accept` comes before the write. A write or an `accept` can fail with an
|
|
43
|
+
* error. Then the transaction rolls back, and the log is broken. Each later call throws a `BrokenLogError` until `recover`.
|
|
44
|
+
* The state of the caller can keep the events of the failed write, so the caller replays the log after `recover`.
|
|
45
|
+
*
|
|
46
|
+
* @param db - The connection. Set its pragmas with `openDatabase`.
|
|
47
|
+
* @param options - The parse of the events, the table and the equality of events.
|
|
48
|
+
*/
|
|
49
|
+
function eventLog(db, options) {
|
|
50
|
+
const table = options.table ?? DEFAULT_TABLE;
|
|
51
|
+
checkTable(table);
|
|
52
|
+
migrate(db, logSteps(table), {
|
|
53
|
+
part: `event-log:${table}`,
|
|
54
|
+
name: `log ${table}`
|
|
55
|
+
});
|
|
56
|
+
return attach(db, table, options.parse, options.equal);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* This function opens a log in its own file, and makes the file when it is not there. `:memory:` gives a log with no file.
|
|
60
|
+
*
|
|
61
|
+
* The log owns the connection and the `user_version` of its file, and its table is `events`. A file of a newer schema
|
|
62
|
+
* version gives an error. The rules of `eventLog` apply.
|
|
63
|
+
*
|
|
64
|
+
* @param path - The path of the file, or `:memory:`.
|
|
65
|
+
* @param parse - The function that changes a stored JSON value into an event. It throws on a value that is not an event.
|
|
66
|
+
* @param options - The pragmas of the connection, and the equality of events. The defaults are the defaults of `openDatabase`.
|
|
67
|
+
*/
|
|
68
|
+
function openEventLog(path, parse, options = {}) {
|
|
69
|
+
const db = openDatabase(path, options);
|
|
70
|
+
try {
|
|
71
|
+
migrate(db, logSteps(DEFAULT_TABLE), { name: "log" });
|
|
72
|
+
return {
|
|
73
|
+
...attach(db, DEFAULT_TABLE, parse, options.equal),
|
|
74
|
+
close() {
|
|
75
|
+
db.close();
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
} catch (error) {
|
|
79
|
+
db.close();
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/** This function makes the log object on a table with a current schema. */
|
|
84
|
+
function attach(db, table, parse, equal) {
|
|
85
|
+
const insert = db.prepare(`INSERT INTO ${table} (id, at, type, body) VALUES (?, ?, ?, ?)`);
|
|
86
|
+
const byId = db.prepare(`SELECT seq, body FROM ${table} WHERE id = ?`);
|
|
87
|
+
const readSql = `SELECT seq, body FROM ${table} WHERE seq > ? AND (? IS NULL OR type IN (SELECT value FROM json_each(?))) ORDER BY seq LIMIT ?`;
|
|
88
|
+
/** The error of a failed write. A broken log refuses each call until `recover`. */
|
|
89
|
+
let broken;
|
|
90
|
+
/** The error of a rollback that did not end. A log with this error cannot recover. */
|
|
91
|
+
let stuck;
|
|
92
|
+
const onRollbackError = (error) => {
|
|
93
|
+
stuck = error;
|
|
94
|
+
};
|
|
95
|
+
function guard() {
|
|
96
|
+
if (broken !== void 0) throw new BrokenLogError("The log is broken by an earlier error. Open it again and replay it.", stuck !== void 0, broken);
|
|
97
|
+
}
|
|
98
|
+
function decode(row) {
|
|
99
|
+
try {
|
|
100
|
+
return parse(JSON.parse(row.body));
|
|
101
|
+
} catch (error) {
|
|
102
|
+
throw new Error(`The event at the position ${String(row.seq)} of the log is not valid.`, { cause: error });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function write(event, accept) {
|
|
106
|
+
const body = JSON.stringify(event);
|
|
107
|
+
const row = byId.get(event.id);
|
|
108
|
+
if (row !== void 0) {
|
|
109
|
+
if (equal === void 0 && row.body === body) return {
|
|
110
|
+
kind: "duplicate",
|
|
111
|
+
seq: row.seq
|
|
112
|
+
};
|
|
113
|
+
const stored = decode(row);
|
|
114
|
+
if (equal?.(stored, event) === true) return {
|
|
115
|
+
kind: "duplicate",
|
|
116
|
+
seq: row.seq
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
kind: "conflict",
|
|
120
|
+
seq: row.seq,
|
|
121
|
+
stored
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const problem = accept?.(event);
|
|
125
|
+
if (problem !== void 0) return {
|
|
126
|
+
kind: "refused",
|
|
127
|
+
problem
|
|
128
|
+
};
|
|
129
|
+
const result = insert.run(event.id, event.at, event.type, body);
|
|
130
|
+
return {
|
|
131
|
+
kind: "added",
|
|
132
|
+
seq: Number(result.lastInsertRowid)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/** This function does the work in one transaction. A failure after the `BEGIN` breaks the log. */
|
|
136
|
+
function inTransaction(work) {
|
|
137
|
+
guard();
|
|
138
|
+
let began = false;
|
|
139
|
+
try {
|
|
140
|
+
return transaction(db, () => {
|
|
141
|
+
began = true;
|
|
142
|
+
return work();
|
|
143
|
+
}, { onRollbackError });
|
|
144
|
+
} catch (error) {
|
|
145
|
+
if (began) broken = error;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function* entries(options = {}) {
|
|
150
|
+
guard();
|
|
151
|
+
const after = options.after ?? 0;
|
|
152
|
+
checkWhole("The position after", after, 0);
|
|
153
|
+
if (options.limit !== void 0) checkWhole("The limit of a read", options.limit, 1);
|
|
154
|
+
const types = options.types === void 0 ? null : JSON.stringify(options.types);
|
|
155
|
+
const rows = db.prepare(readSql).iterate(after, types, types, options.limit ?? -1);
|
|
156
|
+
for (const row of rows) yield {
|
|
157
|
+
seq: row.seq,
|
|
158
|
+
event: decode(row)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
return {
|
|
162
|
+
*read(options) {
|
|
163
|
+
for (const entry of entries(options)) yield entry.event;
|
|
164
|
+
},
|
|
165
|
+
entries,
|
|
166
|
+
get(id) {
|
|
167
|
+
guard();
|
|
168
|
+
const row = byId.get(id);
|
|
169
|
+
return row === void 0 ? void 0 : {
|
|
170
|
+
seq: row.seq,
|
|
171
|
+
event: decode(row)
|
|
172
|
+
};
|
|
173
|
+
},
|
|
174
|
+
head() {
|
|
175
|
+
guard();
|
|
176
|
+
return db.prepare(`SELECT COALESCE(MAX(seq), 0) AS head FROM ${table}`).get().head;
|
|
177
|
+
},
|
|
178
|
+
append(event, accept) {
|
|
179
|
+
return inTransaction(() => write(event, accept));
|
|
180
|
+
},
|
|
181
|
+
appendAll(events, accept) {
|
|
182
|
+
return inTransaction(() => {
|
|
183
|
+
const results = [];
|
|
184
|
+
for (const event of events) results.push(write(event, accept));
|
|
185
|
+
return results;
|
|
186
|
+
});
|
|
187
|
+
},
|
|
188
|
+
count() {
|
|
189
|
+
guard();
|
|
190
|
+
return db.prepare(`SELECT count(*) AS n FROM ${table}`).get().n;
|
|
191
|
+
},
|
|
192
|
+
recover() {
|
|
193
|
+
if (broken === void 0) return;
|
|
194
|
+
if (stuck !== void 0 || db.isTransaction) throw new BrokenLogError("The rollback of the failed write did not end. Open the log again and replay it.", true, stuck ?? broken);
|
|
195
|
+
broken = void 0;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
export { BrokenLogError, EVENT_LOG_SCHEMA_VERSION, eventLog, openEventLog };
|
|
200
|
+
|
|
201
|
+
//# sourceMappingURL=event-log.js.map
|