@boxxy/store-sqlite 0.1.1 → 0.1.4
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 +8 -6
- package/dist/codecs/parse-json-text.d.ts +1 -0
- package/dist/constants.d.ts +1 -0
- package/dist/migrations/rename-relations-migration.d.ts +2 -0
- package/dist/{run-store-migrations.d.ts → migrations/run-store-migrations.d.ts} +1 -1
- package/dist/migrations/v1-tables-migration.d.ts +2 -0
- package/dist/{build-sqlite-reader.d.ts → reads/build-sqlite-reader.d.ts} +1 -1
- package/dist/reads/is-latest-of-session.d.ts +3 -0
- package/dist/{read-item-views.d.ts → reads/read-item-views.d.ts} +3 -3
- package/dist/reads/read-limit-fills.d.ts +4 -0
- package/dist/reads/read-wakes.d.ts +4 -0
- package/dist/test-utils/create-claim-bench.d.ts +8 -0
- package/dist/types.d.ts +60 -10
- package/dist/with-busy-retry.d.ts +1 -0
- package/dist/with-transaction.d.ts +3 -0
- package/dist/{apply-sqlite-change.d.ts → writes/apply-sqlite-change.d.ts} +1 -1
- package/dist/{create-sqlite-tx.d.ts → writes/create-sqlite-tx.d.ts} +1 -1
- package/dist/writes/take-write-lock.d.ts +3 -0
- package/package.json +15 -5
- package/src/codecs/parse-json-text.ts +5 -0
- package/src/{table-codecs.ts → codecs/table-codecs.ts} +52 -15
- package/src/constants.ts +1 -0
- package/src/create-sqlite-claim-store.ts +73 -51
- package/src/migrations/rename-relations-migration.ts +17 -0
- package/src/migrations/run-store-migrations.ts +42 -0
- package/src/migrations/v1-tables-migration.ts +184 -0
- package/src/reads/build-sqlite-reader.ts +484 -0
- package/src/reads/is-latest-of-session.ts +21 -0
- package/src/{read-item-views.ts → reads/read-item-views.ts} +9 -9
- package/src/reads/read-limit-fills.ts +32 -0
- package/src/reads/read-wakes.ts +34 -0
- package/src/test-utils/create-claim-bench.ts +66 -0
- package/src/test-utils/create-test-claim-store.ts +2 -1
- package/src/types.ts +60 -10
- package/src/with-busy-retry.ts +32 -0
- package/src/with-transaction.ts +29 -0
- package/src/{apply-sqlite-change.ts → writes/apply-sqlite-change.ts} +77 -74
- package/src/writes/create-sqlite-tx.ts +229 -0
- package/src/writes/take-write-lock.ts +8 -0
- package/src/build-sqlite-reader.ts +0 -321
- package/src/create-sqlite-tx.ts +0 -134
- package/src/run-store-migrations.ts +0 -20
- /package/dist/{parse-store-row.d.ts → codecs/parse-store-row.d.ts} +0 -0
- /package/dist/{table-codecs.d.ts → codecs/table-codecs.d.ts} +0 -0
- /package/dist/{to-column-name.d.ts → codecs/to-column-name.d.ts} +0 -0
- /package/dist/{to-store-columns.d.ts → codecs/to-store-columns.d.ts} +0 -0
- /package/dist/{create-tables-migration.d.ts → migrations/create-tables-migration.d.ts} +0 -0
- /package/src/{parse-store-row.ts → codecs/parse-store-row.ts} +0 -0
- /package/src/{to-column-name.ts → codecs/to-column-name.ts} +0 -0
- /package/src/{to-store-columns.ts → codecs/to-store-columns.ts} +0 -0
- /package/src/{create-tables-migration.ts → migrations/create-tables-migration.ts} +0 -0
package/README.md
CHANGED
|
@@ -29,14 +29,16 @@ The package runs on [Bun](https://bun.sh) 1.3 or newer.
|
|
|
29
29
|
|
|
30
30
|
## Features
|
|
31
31
|
|
|
32
|
-
- Items, levers, attempts, leases,
|
|
33
|
-
whole or not at all
|
|
32
|
+
- Items, levers, attempts, leases, limit seats, notes, notices and the outbox in one file, with each
|
|
33
|
+
change applied whole or not at all
|
|
34
|
+
([one write path](../../docs/design/boxxy/slice-1.md#one-write-path)).
|
|
34
35
|
- `exclusive` calls that run one at a time in a `BEGIN IMMEDIATE` transaction, and `read` calls that
|
|
35
36
|
see the last commit ([store rules](../../docs/design/boxxy/slice-1.md#store-rules)).
|
|
36
|
-
-
|
|
37
|
-
([
|
|
37
|
+
- Each item's position and state derived on each read, from the rows and the loaded rules
|
|
38
|
+
([v1](../../docs/design/boxxy/v1.md)).
|
|
38
39
|
|
|
39
40
|
## Documentation
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
[`02-
|
|
42
|
+
The [v1 tables migration](./src/migrations/v1-tables-migration.ts) creates each table, and the store
|
|
43
|
+
section of [`02-types.ts`](../../docs/design/boxxy/sketches/v1/core/02-types.ts) gives the port, the
|
|
44
|
+
seat take and the seat sync.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function parseJSONText(text: string): unknown;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const lockWaitLimitMs = 5000;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { StoreReader } from '@boxxy/model';
|
|
2
2
|
import type { Kysely } from 'kysely';
|
|
3
|
-
import type { StoreDatabase } from '
|
|
3
|
+
import type { StoreDatabase } from '../types.ts';
|
|
4
4
|
export declare function buildSQLiteReader(db: Kysely<StoreDatabase>): StoreReader;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Instant,
|
|
1
|
+
import type { Instant, ItemRules, ItemWithLevers } from '@boxxy/model';
|
|
2
2
|
import type { Expression, ExpressionBuilder, Kysely, SqlBool } from 'kysely';
|
|
3
|
-
import type { StoreDatabase } from '
|
|
3
|
+
import type { StoreDatabase } from '../types.ts';
|
|
4
4
|
export type ItemScope = (builder: ExpressionBuilder<StoreDatabase, 'items'>) => Expression<SqlBool>;
|
|
5
|
-
export declare function readItemViews(db: Kysely<StoreDatabase>, scope: ItemScope, now: Instant,
|
|
5
|
+
export declare function readItemViews(db: Kysely<StoreDatabase>, scope: ItemScope, now: Instant, rules: ItemRules): Promise<ItemWithLevers[]>;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ItemId } from '@boxxy/model';
|
|
2
|
+
export interface ClaimBenchOptions {
|
|
3
|
+
readonly itemCount: number;
|
|
4
|
+
}
|
|
5
|
+
export interface ClaimBench extends AsyncDisposable {
|
|
6
|
+
readonly claim: () => Promise<ItemId>;
|
|
7
|
+
}
|
|
8
|
+
export declare function createClaimBench(options: ClaimBenchOptions): Promise<ClaimBench>;
|
package/dist/types.d.ts
CHANGED
|
@@ -7,13 +7,16 @@ export interface StoreDatabase {
|
|
|
7
7
|
title: string;
|
|
8
8
|
outcome: string;
|
|
9
9
|
acceptance: string;
|
|
10
|
+
fields: string;
|
|
11
|
+
labels: string;
|
|
10
12
|
found_in_attempt: string | null;
|
|
11
13
|
filed_by: string;
|
|
12
14
|
filed_at: number;
|
|
13
15
|
closed_at: number | null;
|
|
14
16
|
close_reason: string | null;
|
|
17
|
+
kept_session: string | null;
|
|
15
18
|
};
|
|
16
|
-
|
|
19
|
+
relations: {
|
|
17
20
|
source: string;
|
|
18
21
|
target: string;
|
|
19
22
|
type: string;
|
|
@@ -28,6 +31,7 @@ export interface StoreDatabase {
|
|
|
28
31
|
until: number | null;
|
|
29
32
|
expires_at: number | null;
|
|
30
33
|
reason: string | null;
|
|
34
|
+
phase: string | null;
|
|
31
35
|
actor: string;
|
|
32
36
|
at: number;
|
|
33
37
|
};
|
|
@@ -40,23 +44,42 @@ export interface StoreDatabase {
|
|
|
40
44
|
recipe_hash: string;
|
|
41
45
|
rules_version: string;
|
|
42
46
|
pool: string;
|
|
43
|
-
slot: number;
|
|
44
|
-
slot_gen: number;
|
|
45
47
|
lease_gen: number;
|
|
46
48
|
spawn_request: string;
|
|
47
49
|
spawn_request_hash: string;
|
|
48
50
|
state: string;
|
|
49
51
|
failure: string | null;
|
|
50
52
|
atc_session: string | null;
|
|
51
|
-
|
|
53
|
+
caller: string | null;
|
|
52
54
|
claimed_at: number;
|
|
53
55
|
started_at: number | null;
|
|
54
56
|
ended_at: number | null;
|
|
57
|
+
fields: string;
|
|
58
|
+
mode: string;
|
|
59
|
+
round: number;
|
|
60
|
+
input: string | null;
|
|
61
|
+
hold_gen: number;
|
|
62
|
+
close_out: string | null;
|
|
63
|
+
close_out_hash: string | null;
|
|
64
|
+
close_out_fact: string | null;
|
|
65
|
+
closing: string | null;
|
|
66
|
+
closing_at: number | null;
|
|
67
|
+
suspend_requested_at: number | null;
|
|
68
|
+
slept_at: number | null;
|
|
69
|
+
wake_requested_at: number | null;
|
|
70
|
+
woken_at: number | null;
|
|
71
|
+
outcome: string | null;
|
|
72
|
+
returned_outcome: string | null;
|
|
73
|
+
strayed_at: number | null;
|
|
74
|
+
session_stopped_at: number | null;
|
|
75
|
+
session_forgotten_at: number | null;
|
|
55
76
|
};
|
|
56
77
|
attempt_sessions: {
|
|
57
78
|
attempt: string;
|
|
58
79
|
agent_session: string;
|
|
59
80
|
atc_session: string;
|
|
81
|
+
product: string;
|
|
82
|
+
source: string;
|
|
60
83
|
seen_at: number;
|
|
61
84
|
};
|
|
62
85
|
leases: {
|
|
@@ -67,23 +90,31 @@ export interface StoreDatabase {
|
|
|
67
90
|
taken_at: number;
|
|
68
91
|
released_at: number | null;
|
|
69
92
|
};
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
93
|
+
limits: {
|
|
94
|
+
name: string;
|
|
95
|
+
max: number;
|
|
96
|
+
counts: string;
|
|
97
|
+
};
|
|
98
|
+
limit_seats: {
|
|
99
|
+
limit: string;
|
|
100
|
+
attempt: string;
|
|
101
|
+
hold_gen: number;
|
|
102
|
+
over_cap: number;
|
|
103
|
+
taken_at: number;
|
|
104
|
+
released_at: number | null;
|
|
76
105
|
};
|
|
77
106
|
decisions: {
|
|
78
107
|
id: string;
|
|
79
108
|
attempt: string;
|
|
80
109
|
item: string;
|
|
110
|
+
kind: string;
|
|
81
111
|
pool: string;
|
|
82
112
|
rules_version: string;
|
|
83
113
|
snapshot_hash: string;
|
|
84
114
|
store_version: number;
|
|
85
115
|
pepys_offsets: string;
|
|
86
116
|
faults: string;
|
|
117
|
+
limits: string;
|
|
87
118
|
at: number;
|
|
88
119
|
};
|
|
89
120
|
evidence: {
|
|
@@ -99,6 +130,8 @@ export interface StoreDatabase {
|
|
|
99
130
|
repo: string;
|
|
100
131
|
number: number;
|
|
101
132
|
attempt: string;
|
|
133
|
+
github_id: number;
|
|
134
|
+
url: string;
|
|
102
135
|
state: string;
|
|
103
136
|
merged_at: number | null;
|
|
104
137
|
merge_commit: string | null;
|
|
@@ -109,6 +142,21 @@ export interface StoreDatabase {
|
|
|
109
142
|
heartbeat_at: number;
|
|
110
143
|
paused: number;
|
|
111
144
|
};
|
|
145
|
+
notes: {
|
|
146
|
+
item: string;
|
|
147
|
+
seq: number;
|
|
148
|
+
text: string;
|
|
149
|
+
by: string;
|
|
150
|
+
at: number;
|
|
151
|
+
};
|
|
152
|
+
notices: {
|
|
153
|
+
id: Generated<number>;
|
|
154
|
+
item: string;
|
|
155
|
+
reason: string;
|
|
156
|
+
at: number;
|
|
157
|
+
tries: number;
|
|
158
|
+
sent_at: number | null;
|
|
159
|
+
};
|
|
112
160
|
outbox: {
|
|
113
161
|
seq: Generated<number>;
|
|
114
162
|
fact_id: string;
|
|
@@ -118,5 +166,7 @@ export interface StoreDatabase {
|
|
|
118
166
|
at: number;
|
|
119
167
|
payload: string;
|
|
120
168
|
sent_at: number | null;
|
|
169
|
+
parked_at: number | null;
|
|
170
|
+
parked_reason: string | null;
|
|
121
171
|
};
|
|
122
172
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function withBusyRetry(waitLimitMs: number, work: () => unknown): Promise<void>;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
import type { ApplyResult, StoreChange } from '@boxxy/model';
|
|
2
2
|
import type { Kysely } from 'kysely';
|
|
3
|
-
import type { StoreDatabase } from '
|
|
3
|
+
import type { StoreDatabase } from '../types.ts';
|
|
4
4
|
export declare function applySQLiteChange(db: Kysely<StoreDatabase>, change: StoreChange): Promise<ApplyResult>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@boxxy/store-sqlite",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "boxxy's store on SQLite",
|
|
5
5
|
"homepage": "https://github.com/zgeoff/boxxy/tree/main/adapters/store-sqlite#readme",
|
|
6
6
|
"bugs": "https://github.com/zgeoff/boxxy/issues",
|
|
@@ -26,20 +26,30 @@
|
|
|
26
26
|
"access": "public"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
|
+
"bench": "rig bench",
|
|
29
30
|
"build": "rm -rf dist && tsrs -p tsconfig.build.json",
|
|
30
31
|
"typecheck": "tsrs --noEmit"
|
|
31
32
|
},
|
|
32
33
|
"dependencies": {
|
|
33
|
-
"@boxxy/content-hash": "0.1.
|
|
34
|
-
"@boxxy/model": "0.1.
|
|
34
|
+
"@boxxy/content-hash": "0.1.4",
|
|
35
|
+
"@boxxy/model": "0.1.4",
|
|
35
36
|
"@zgeoff/kysely-bun-sqlite": "0.3.2",
|
|
36
|
-
"kysely": "0.29.
|
|
37
|
+
"kysely": "0.29.8",
|
|
37
38
|
"tiny-invariant": "1.3.3",
|
|
38
39
|
"ts-pattern": "5.9.2",
|
|
39
40
|
"zod": "4.6.7"
|
|
40
41
|
},
|
|
41
42
|
"devDependencies": {
|
|
42
|
-
"@boxxy/dispatch": "0.1.
|
|
43
|
+
"@boxxy/dispatch": "0.1.4",
|
|
44
|
+
"@zgeoff/rig": "0.2.3"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@boxxy/dispatch": "0.1.4"
|
|
48
|
+
},
|
|
49
|
+
"peerDependenciesMeta": {
|
|
50
|
+
"@boxxy/dispatch": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
43
53
|
},
|
|
44
54
|
"engines": {
|
|
45
55
|
"bun": ">=1.3",
|
|
@@ -17,22 +17,25 @@ export const tableCodecs: { readonly [T in TableName]: TableCodec<Tables[T]> } =
|
|
|
17
17
|
'title',
|
|
18
18
|
'outcome',
|
|
19
19
|
'acceptance',
|
|
20
|
+
'fields',
|
|
21
|
+
'labels',
|
|
20
22
|
'foundInAttempt',
|
|
21
23
|
'filedBy',
|
|
22
24
|
'filedAt',
|
|
23
25
|
'closedAt',
|
|
24
26
|
'closeReason',
|
|
27
|
+
'keptSession',
|
|
25
28
|
],
|
|
26
|
-
json: ['acceptance'],
|
|
29
|
+
json: ['acceptance', 'fields', 'labels', 'keptSession'],
|
|
27
30
|
flags: [],
|
|
28
31
|
},
|
|
29
|
-
|
|
32
|
+
relations: {
|
|
30
33
|
fields: ['source', 'target', 'type', 'addedBy', 'addedAt', 'removedAt'],
|
|
31
34
|
json: [],
|
|
32
35
|
flags: [],
|
|
33
36
|
},
|
|
34
37
|
levers: {
|
|
35
|
-
fields: ['seq', 'kind', 'item', 'until', 'expiresAt', 'reason', 'actor', 'at'],
|
|
38
|
+
fields: ['seq', 'kind', 'item', 'until', 'expiresAt', 'reason', 'phase', 'actor', 'at'],
|
|
36
39
|
json: [],
|
|
37
40
|
flags: [],
|
|
38
41
|
},
|
|
@@ -46,24 +49,41 @@ export const tableCodecs: { readonly [T in TableName]: TableCodec<Tables[T]> } =
|
|
|
46
49
|
'recipeHash',
|
|
47
50
|
'rulesVersion',
|
|
48
51
|
'pool',
|
|
49
|
-
'slot',
|
|
50
|
-
'slotGen',
|
|
51
52
|
'leaseGen',
|
|
52
53
|
'spawnRequest',
|
|
53
54
|
'spawnRequestHash',
|
|
54
55
|
'state',
|
|
55
56
|
'failure',
|
|
56
57
|
'atcSession',
|
|
57
|
-
'
|
|
58
|
+
'caller',
|
|
58
59
|
'claimedAt',
|
|
59
60
|
'startedAt',
|
|
60
61
|
'endedAt',
|
|
62
|
+
'fields',
|
|
63
|
+
'mode',
|
|
64
|
+
'round',
|
|
65
|
+
'input',
|
|
66
|
+
'holdGen',
|
|
67
|
+
'closeOut',
|
|
68
|
+
'closeOutHash',
|
|
69
|
+
'closeOutFact',
|
|
70
|
+
'closing',
|
|
71
|
+
'closingAt',
|
|
72
|
+
'suspendRequestedAt',
|
|
73
|
+
'sleptAt',
|
|
74
|
+
'wakeRequestedAt',
|
|
75
|
+
'wokenAt',
|
|
76
|
+
'outcome',
|
|
77
|
+
'returnedOutcome',
|
|
78
|
+
'strayedAt',
|
|
79
|
+
'sessionStoppedAt',
|
|
80
|
+
'sessionForgottenAt',
|
|
61
81
|
],
|
|
62
|
-
json: ['spawnRequest', 'closeOut'],
|
|
82
|
+
json: ['spawnRequest', 'caller', 'fields', 'input', 'closeOut'],
|
|
63
83
|
flags: [],
|
|
64
84
|
},
|
|
65
85
|
attempt_sessions: {
|
|
66
|
-
fields: ['attempt', 'agentSession', 'atcSession', 'seenAt'],
|
|
86
|
+
fields: ['attempt', 'agentSession', 'atcSession', 'product', 'source', 'seenAt'],
|
|
67
87
|
json: [],
|
|
68
88
|
flags: [],
|
|
69
89
|
},
|
|
@@ -72,25 +92,22 @@ export const tableCodecs: { readonly [T in TableName]: TableCodec<Tables[T]> } =
|
|
|
72
92
|
json: [],
|
|
73
93
|
flags: [],
|
|
74
94
|
},
|
|
75
|
-
pool_slots: {
|
|
76
|
-
fields: ['pool', 'slot', 'holder', 'gen', 'retired'],
|
|
77
|
-
json: [],
|
|
78
|
-
flags: ['retired'],
|
|
79
|
-
},
|
|
80
95
|
decisions: {
|
|
81
96
|
fields: [
|
|
82
97
|
'id',
|
|
83
98
|
'attempt',
|
|
84
99
|
'item',
|
|
100
|
+
'kind',
|
|
85
101
|
'pool',
|
|
86
102
|
'rulesVersion',
|
|
87
103
|
'snapshotHash',
|
|
88
104
|
'storeVersion',
|
|
89
105
|
'pepysOffsets',
|
|
90
106
|
'faults',
|
|
107
|
+
'limits',
|
|
91
108
|
'at',
|
|
92
109
|
],
|
|
93
|
-
json: ['pepysOffsets', 'faults'],
|
|
110
|
+
json: ['pepysOffsets', 'faults', 'limits'],
|
|
94
111
|
flags: [],
|
|
95
112
|
},
|
|
96
113
|
evidence: {
|
|
@@ -99,7 +116,17 @@ export const tableCodecs: { readonly [T in TableName]: TableCodec<Tables[T]> } =
|
|
|
99
116
|
flags: [],
|
|
100
117
|
},
|
|
101
118
|
pull_requests: {
|
|
102
|
-
fields: [
|
|
119
|
+
fields: [
|
|
120
|
+
'repo',
|
|
121
|
+
'number',
|
|
122
|
+
'attempt',
|
|
123
|
+
'githubId',
|
|
124
|
+
'url',
|
|
125
|
+
'state',
|
|
126
|
+
'mergedAt',
|
|
127
|
+
'mergeCommit',
|
|
128
|
+
'checkedAt',
|
|
129
|
+
],
|
|
103
130
|
json: [],
|
|
104
131
|
flags: [],
|
|
105
132
|
},
|
|
@@ -108,4 +135,14 @@ export const tableCodecs: { readonly [T in TableName]: TableCodec<Tables[T]> } =
|
|
|
108
135
|
json: [],
|
|
109
136
|
flags: ['paused'],
|
|
110
137
|
},
|
|
138
|
+
notes: {
|
|
139
|
+
fields: ['item', 'seq', 'text', 'by', 'at'],
|
|
140
|
+
json: [],
|
|
141
|
+
flags: [],
|
|
142
|
+
},
|
|
143
|
+
notices: {
|
|
144
|
+
fields: ['id', 'item', 'reason', 'at', 'tries', 'sentAt'],
|
|
145
|
+
json: [],
|
|
146
|
+
flags: [],
|
|
147
|
+
},
|
|
111
148
|
};
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const lockWaitLimitMs = 5000;
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { Database } from 'bun:sqlite';
|
|
2
|
-
import type { ClaimStore } from '@boxxy/model';
|
|
2
|
+
import type { ClaimStore, StoreReader } from '@boxxy/model';
|
|
3
3
|
import { buildSQLiteDialect } from '@zgeoff/kysely-bun-sqlite';
|
|
4
4
|
import { Kysely, sql } from 'kysely';
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { runStoreMigrations } from './run-store-migrations.ts';
|
|
5
|
+
import { lockWaitLimitMs } from './constants.ts';
|
|
6
|
+
import { runStoreMigrations } from './migrations/run-store-migrations.ts';
|
|
7
|
+
import { buildSQLiteReader } from './reads/build-sqlite-reader.ts';
|
|
9
8
|
import type { StoreDatabase } from './types.ts';
|
|
9
|
+
import { withBusyRetry } from './with-busy-retry.ts';
|
|
10
|
+
import { withTransaction } from './with-transaction.ts';
|
|
11
|
+
import { createSQLiteTx } from './writes/create-sqlite-tx.ts';
|
|
12
|
+
import { takeWriteLock } from './writes/take-write-lock.ts';
|
|
10
13
|
|
|
11
14
|
export interface SQLiteClaimStore extends ClaimStore {
|
|
12
15
|
close: () => Promise<void>;
|
|
@@ -15,78 +18,97 @@ export interface SQLiteClaimStore extends ClaimStore {
|
|
|
15
18
|
export async function createSQLiteClaimStore(path: string): Promise<SQLiteClaimStore> {
|
|
16
19
|
await using opened = new AsyncDisposableStack();
|
|
17
20
|
|
|
18
|
-
const writer =
|
|
21
|
+
const writer = new Database(path, { readwrite: true, create: true });
|
|
19
22
|
|
|
20
23
|
opened.defer(() => {
|
|
21
24
|
writer.close();
|
|
22
25
|
});
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
writer.run('PRAGMA busy_timeout = 0');
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
29
|
+
// The file keeps its journal mode, so a read handle never sets it. SQLite fails these at once,
|
|
30
|
+
// whatever the busy timeout, while another handle switches the file to WAL or recovers it.
|
|
31
|
+
await withBusyRetry(lockWaitLimitMs, () => {
|
|
32
|
+
writer.run('PRAGMA journal_mode = WAL');
|
|
33
|
+
writer.run('PRAGMA synchronous = NORMAL');
|
|
28
34
|
});
|
|
29
35
|
|
|
30
36
|
const writerDB = new Kysely<StoreDatabase>({ dialect: buildSQLiteDialect(writer) });
|
|
31
|
-
const readerDB = new Kysely<StoreDatabase>({ dialect: buildSQLiteDialect(reader) });
|
|
32
37
|
|
|
33
|
-
opened.defer(() => readerDB.destroy());
|
|
34
38
|
opened.defer(() => writerDB.destroy());
|
|
35
39
|
|
|
36
40
|
await runStoreMigrations(writerDB);
|
|
37
41
|
|
|
38
42
|
const handles = opened.move();
|
|
39
43
|
|
|
44
|
+
const reads = new Set<Promise<unknown>>();
|
|
45
|
+
|
|
46
|
+
let isClosed = false;
|
|
47
|
+
|
|
40
48
|
return {
|
|
41
49
|
// Kysely holds the one connection of a handle from acquire to release, so two calls on this
|
|
42
|
-
// store run one at a time, and
|
|
50
|
+
// store run one at a time, and the write lock makes another store's writer wait.
|
|
43
51
|
exclusive: (work) =>
|
|
44
|
-
writerDB
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
writerDB.connection().execute((db) =>
|
|
53
|
+
withTransaction(
|
|
54
|
+
db,
|
|
55
|
+
(locked) => takeWriteLock(locked, lockWaitLimitMs),
|
|
56
|
+
() => work(createSQLiteTx(db)),
|
|
57
|
+
),
|
|
58
|
+
),
|
|
59
|
+
|
|
60
|
+
read: async (work) => {
|
|
61
|
+
if (isClosed) {
|
|
62
|
+
throw new Error('the store is closed');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const reading = withReadHandle(path, work);
|
|
66
|
+
|
|
67
|
+
reads.add(reading);
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
return await reading;
|
|
71
|
+
} finally {
|
|
72
|
+
reads.delete(reading);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
close: async () => {
|
|
77
|
+
isClosed = true;
|
|
78
|
+
|
|
79
|
+
await Promise.allSettled(reads);
|
|
80
|
+
await handles.disposeAsync();
|
|
81
|
+
},
|
|
55
82
|
};
|
|
56
83
|
}
|
|
57
84
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
85
|
+
// A handle of its own per call, so a read sees the last commit and never a running exclusive
|
|
86
|
+
// call's writes, and a read started inside another read never waits for the outer one's handle.
|
|
87
|
+
async function withReadHandle<T>(
|
|
88
|
+
path: string,
|
|
89
|
+
work: (reader: StoreReader) => Promise<T>,
|
|
90
|
+
): Promise<T> {
|
|
91
|
+
await using opened = new AsyncDisposableStack();
|
|
64
92
|
|
|
65
|
-
|
|
66
|
-
}
|
|
93
|
+
// without create, a missing file fails the open, where the default makes an empty file
|
|
94
|
+
const handle = new Database(path, { readwrite: true, create: false });
|
|
67
95
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
work: () => Promise<T>,
|
|
72
|
-
): Promise<T> {
|
|
73
|
-
await begin.execute(db);
|
|
96
|
+
opened.defer(() => {
|
|
97
|
+
handle.close();
|
|
98
|
+
});
|
|
74
99
|
|
|
75
|
-
|
|
76
|
-
|
|
100
|
+
handle.run(`PRAGMA busy_timeout = ${lockWaitLimitMs}`);
|
|
101
|
+
handle.run('PRAGMA synchronous = NORMAL');
|
|
77
102
|
|
|
78
|
-
|
|
103
|
+
const readerDB = new Kysely<StoreDatabase>({ dialect: buildSQLiteDialect(handle) });
|
|
79
104
|
|
|
80
|
-
|
|
81
|
-
} catch (error) {
|
|
82
|
-
// SQLite ends the transaction by itself after some errors, such as a full disk, and ROLLBACK
|
|
83
|
-
// then fails; its error joins the first one, never replaces it
|
|
84
|
-
await sql`ROLLBACK`.execute(db).catch((rollbackError: unknown) => {
|
|
85
|
-
throw new AggregateError([error, rollbackError], 'the work failed, and so did its rollback', {
|
|
86
|
-
cause: error,
|
|
87
|
-
});
|
|
88
|
-
});
|
|
105
|
+
opened.defer(() => readerDB.destroy());
|
|
89
106
|
|
|
90
|
-
|
|
91
|
-
|
|
107
|
+
return await readerDB.connection().execute((db) =>
|
|
108
|
+
withTransaction(
|
|
109
|
+
db,
|
|
110
|
+
(connection) => sql`BEGIN`.execute(connection),
|
|
111
|
+
() => work(buildSQLiteReader(db)),
|
|
112
|
+
),
|
|
113
|
+
);
|
|
92
114
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { sql } from 'kysely';
|
|
2
|
+
import type { Kysely } from 'kysely';
|
|
3
|
+
import type { Migration } from 'kysely/migration';
|
|
4
|
+
|
|
5
|
+
const statements = [
|
|
6
|
+
sql`ALTER TABLE links RENAME TO relations`,
|
|
7
|
+
sql`DROP INDEX links_live`,
|
|
8
|
+
sql`CREATE UNIQUE INDEX relations_live ON relations (source, target, type) WHERE removed_at IS NULL`,
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
export const renameRelationsMigration: Migration = {
|
|
12
|
+
up: async (db: Kysely<unknown>) => {
|
|
13
|
+
for (const statement of statements) {
|
|
14
|
+
await statement.execute(db);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
};
|