@boxxy/store-sqlite 0.1.0 → 0.1.3
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 +24 -3
- package/dist/codecs/parse-json-text.d.ts +1 -0
- package/dist/codecs/parse-store-row.d.ts +2 -0
- package/dist/codecs/table-codecs.d.ts +10 -0
- package/dist/codecs/to-column-name.d.ts +1 -0
- package/dist/codecs/to-store-columns.d.ts +3 -0
- package/dist/constants.d.ts +1 -0
- package/dist/create-sqlite-claim-store.d.ts +5 -0
- package/dist/index.d.ts +1 -1
- package/dist/migrations/create-tables-migration.d.ts +2 -0
- package/dist/migrations/rename-relations-migration.d.ts +2 -0
- package/dist/migrations/run-store-migrations.d.ts +3 -0
- package/dist/migrations/v1-tables-migration.d.ts +2 -0
- package/dist/reads/build-sqlite-reader.d.ts +4 -0
- package/dist/reads/is-latest-of-session.d.ts +3 -0
- package/dist/reads/read-item-views.d.ts +5 -0
- 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/test-utils/create-test-claim-store.d.ts +2 -0
- package/dist/types.d.ts +172 -0
- package/dist/with-busy-retry.d.ts +1 -0
- package/dist/with-transaction.d.ts +3 -0
- package/dist/writes/apply-sqlite-change.d.ts +4 -0
- package/dist/writes/create-sqlite-tx.d.ts +4 -0
- package/dist/writes/take-write-lock.d.ts +3 -0
- package/package.json +15 -1
- package/src/codecs/parse-json-text.ts +5 -0
- package/src/codecs/parse-store-row.ts +36 -0
- package/src/codecs/table-codecs.ts +148 -0
- package/src/codecs/to-column-name.ts +3 -0
- package/src/codecs/to-store-columns.ts +44 -0
- package/src/constants.ts +1 -0
- package/src/create-sqlite-claim-store.ts +114 -0
- package/src/index.ts +1 -1
- package/src/migrations/create-tables-migration.ts +146 -0
- 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/reads/read-item-views.ts +92 -0
- 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 +23 -0
- package/src/types.ts +173 -0
- package/src/with-busy-retry.ts +32 -0
- package/src/with-transaction.ts +29 -0
- package/src/writes/apply-sqlite-change.ts +223 -0
- package/src/writes/create-sqlite-tx.ts +229 -0
- package/src/writes/take-write-lock.ts +8 -0
- package/dist/package-name.d.ts +0 -1
- package/src/package-name.ts +0 -1
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
import { formatCanonicalJson } from '@boxxy/content-hash';
|
|
2
|
+
import { collectSeatSync, toJSONValue } from '@boxxy/model';
|
|
3
|
+
import type {
|
|
4
|
+
FactId,
|
|
5
|
+
Instant,
|
|
6
|
+
Limit,
|
|
7
|
+
PullRequestRef,
|
|
8
|
+
RunnerId,
|
|
9
|
+
StoreChange,
|
|
10
|
+
StoreReader,
|
|
11
|
+
StoreTx,
|
|
12
|
+
} from '@boxxy/model';
|
|
13
|
+
import type { Kysely } from 'kysely';
|
|
14
|
+
import { buildSQLiteReader } from '../reads/build-sqlite-reader.ts';
|
|
15
|
+
import type { StoreDatabase } from '../types.ts';
|
|
16
|
+
import { applySQLiteChange } from './apply-sqlite-change.ts';
|
|
17
|
+
|
|
18
|
+
// A call started beside a change would run its statements inside the change's savepoint, and a
|
|
19
|
+
// refusal would roll them back after the call resolved, so every call waits for the one before it.
|
|
20
|
+
export function createSQLiteTx(db: Kysely<StoreDatabase>): StoreTx {
|
|
21
|
+
const inTurn = makeTurnRunner();
|
|
22
|
+
const reader = buildSQLiteReader(db);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
readSnapshot: inTurn(reader.readSnapshot),
|
|
26
|
+
readItem: inTurn(reader.readItem),
|
|
27
|
+
readItemDetail: inTurn(reader.readItemDetail),
|
|
28
|
+
listItems: inTurn(reader.listItems),
|
|
29
|
+
listOpenIssues: inTurn(reader.listOpenIssues),
|
|
30
|
+
readLastLeaseGen: inTurn(reader.readLastLeaseGen),
|
|
31
|
+
readAttempt: inTurn(reader.readAttempt),
|
|
32
|
+
listEvidence: inTurn(reader.listEvidence),
|
|
33
|
+
listItemAttempts: inTurn(reader.listItemAttempts),
|
|
34
|
+
listLiveAttempts: inTurn(reader.listLiveAttempts),
|
|
35
|
+
listStrayAttempts: inTurn(reader.listStrayAttempts),
|
|
36
|
+
listSucceededAttemptsWithoutPullRequest: inTurn(reader.listSucceededAttemptsWithoutPullRequest),
|
|
37
|
+
findAttemptBySession: inTurn(reader.findAttemptBySession),
|
|
38
|
+
findAttemptByAgentSession: inTurn(reader.findAttemptByAgentSession),
|
|
39
|
+
findSessionByCaller: inTurn(reader.findSessionByCaller),
|
|
40
|
+
readPullRequestRow: inTurn(reader.readPullRequestRow),
|
|
41
|
+
listWatchedPullRequests: inTurn(reader.listWatchedPullRequests),
|
|
42
|
+
listLimits: inTurn(reader.listLimits),
|
|
43
|
+
listLimitSeats: inTurn(reader.listLimitSeats),
|
|
44
|
+
listItemNotes: inTurn(reader.listItemNotes),
|
|
45
|
+
listUnsentNotices: inTurn(reader.listUnsentNotices),
|
|
46
|
+
listKeptSessions: inTurn(reader.listKeptSessions),
|
|
47
|
+
listSessionsToForget: inTurn(reader.listSessionsToForget),
|
|
48
|
+
readRunner: inTurn(reader.readRunner),
|
|
49
|
+
readUnsentFacts: inTurn(reader.readUnsentFacts),
|
|
50
|
+
readOutboxCounts: inTurn(reader.readOutboxCounts),
|
|
51
|
+
readItemBehind: inTurn(reader.readItemBehind),
|
|
52
|
+
applyChange: inTurn((change: StoreChange) => applySQLiteChange(db, change)),
|
|
53
|
+
markFactsSent: inTurn((ids: readonly FactId[], at: Instant) => writeFactsSent(db, ids, at)),
|
|
54
|
+
parkFacts: inTurn((ids: readonly FactId[], reason: string, at: Instant) =>
|
|
55
|
+
writeFactsParked(db, ids, reason, at),
|
|
56
|
+
),
|
|
57
|
+
unparkFacts: inTurn(() => removeFactParks(db)),
|
|
58
|
+
writeHeartbeat: inTurn((runner: RunnerId, at: Instant) => writeRunnerBeat(db, runner, at)),
|
|
59
|
+
syncLimits: inTurn((limits: readonly Limit[], at: Instant) =>
|
|
60
|
+
writeLimits(db, reader, limits, at),
|
|
61
|
+
),
|
|
62
|
+
markPullRequestChecked: inTurn((ref: PullRequestRef, at: Instant) =>
|
|
63
|
+
writePullRequestChecked(db, ref, at),
|
|
64
|
+
),
|
|
65
|
+
markNoticeTried: inTurn((id: number, at: Instant, sent: boolean) =>
|
|
66
|
+
writeNoticeTry(db, id, at, sent),
|
|
67
|
+
),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function makeTurnRunner() {
|
|
72
|
+
let turn: Promise<unknown> = Promise.resolve();
|
|
73
|
+
|
|
74
|
+
return <Args extends unknown[], Result>(run: (...args: Args) => Promise<Result>) =>
|
|
75
|
+
async (...args: Args): Promise<Result> => {
|
|
76
|
+
const previous = turn;
|
|
77
|
+
const next = Promise.withResolvers<void>();
|
|
78
|
+
|
|
79
|
+
turn = next.promise;
|
|
80
|
+
|
|
81
|
+
await previous;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
return await run(...args);
|
|
85
|
+
} finally {
|
|
86
|
+
next.resolve();
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function writeFactsSent(
|
|
92
|
+
db: Kysely<StoreDatabase>,
|
|
93
|
+
ids: readonly FactId[],
|
|
94
|
+
at: Instant,
|
|
95
|
+
): Promise<void> {
|
|
96
|
+
if (ids.length === 0) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
await db
|
|
101
|
+
.updateTable('outbox')
|
|
102
|
+
.set({ sent_at: at })
|
|
103
|
+
.where('fact_id', 'in', ids)
|
|
104
|
+
.where('sent_at', 'is', null)
|
|
105
|
+
.where('parked_at', 'is', null)
|
|
106
|
+
.execute();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function writeFactsParked(
|
|
110
|
+
db: Kysely<StoreDatabase>,
|
|
111
|
+
ids: readonly FactId[],
|
|
112
|
+
reason: string,
|
|
113
|
+
at: Instant,
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
if (ids.length === 0) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
await db
|
|
120
|
+
.updateTable('outbox')
|
|
121
|
+
.set({ parked_at: at, parked_reason: reason })
|
|
122
|
+
.where('fact_id', 'in', ids)
|
|
123
|
+
.where('sent_at', 'is', null)
|
|
124
|
+
.execute();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async function removeFactParks(db: Kysely<StoreDatabase>): Promise<number> {
|
|
128
|
+
const result = await db
|
|
129
|
+
.updateTable('outbox')
|
|
130
|
+
.set({ parked_at: null, parked_reason: null })
|
|
131
|
+
.where('sent_at', 'is', null)
|
|
132
|
+
.where('parked_at', 'is not', null)
|
|
133
|
+
.executeTakeFirst();
|
|
134
|
+
|
|
135
|
+
return Number(result.numUpdatedRows);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function writeRunnerBeat(
|
|
139
|
+
db: Kysely<StoreDatabase>,
|
|
140
|
+
runner: RunnerId,
|
|
141
|
+
at: Instant,
|
|
142
|
+
): Promise<void> {
|
|
143
|
+
await db
|
|
144
|
+
.insertInto('runners')
|
|
145
|
+
.values({ id: runner, heartbeat_at: at, paused: 0 })
|
|
146
|
+
.onConflict((conflict) => conflict.column('id').doUpdateSet({ heartbeat_at: at }))
|
|
147
|
+
.execute();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function writeLimits(
|
|
151
|
+
db: Kysely<StoreDatabase>,
|
|
152
|
+
reader: StoreReader,
|
|
153
|
+
limits: readonly Limit[],
|
|
154
|
+
at: Instant,
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
const sync = collectSeatSync({
|
|
157
|
+
limits,
|
|
158
|
+
held: await reader.listLimitSeats(),
|
|
159
|
+
attempts: await reader.listLiveAttempts(),
|
|
160
|
+
at,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await db.deleteFrom('limits').execute();
|
|
164
|
+
|
|
165
|
+
for (const limit of limits) {
|
|
166
|
+
await db
|
|
167
|
+
.insertInto('limits')
|
|
168
|
+
.values({
|
|
169
|
+
name: limit.name,
|
|
170
|
+
max: limit.max,
|
|
171
|
+
counts: formatCanonicalJson(toJSONValue(limit.counts)),
|
|
172
|
+
})
|
|
173
|
+
.execute();
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
for (const seat of sync.release) {
|
|
177
|
+
await db
|
|
178
|
+
.updateTable('limit_seats')
|
|
179
|
+
.set({ released_at: at })
|
|
180
|
+
.where('limit', '=', seat.limit)
|
|
181
|
+
.where('attempt', '=', seat.attempt)
|
|
182
|
+
.where('hold_gen', '=', seat.holdGen)
|
|
183
|
+
.where('released_at', 'is', null)
|
|
184
|
+
.execute();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const seat of sync.take) {
|
|
188
|
+
await db
|
|
189
|
+
.insertInto('limit_seats')
|
|
190
|
+
.values({
|
|
191
|
+
limit: seat.limit,
|
|
192
|
+
attempt: seat.attempt,
|
|
193
|
+
hold_gen: seat.holdGen,
|
|
194
|
+
over_cap: seat.overCap ? 1 : 0,
|
|
195
|
+
taken_at: seat.takenAt,
|
|
196
|
+
released_at: seat.releasedAt,
|
|
197
|
+
})
|
|
198
|
+
.execute();
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function writePullRequestChecked(
|
|
203
|
+
db: Kysely<StoreDatabase>,
|
|
204
|
+
ref: PullRequestRef,
|
|
205
|
+
at: Instant,
|
|
206
|
+
): Promise<void> {
|
|
207
|
+
await db
|
|
208
|
+
.updateTable('pull_requests')
|
|
209
|
+
.set({ checked_at: at })
|
|
210
|
+
.where('repo', '=', ref.repo)
|
|
211
|
+
.where('number', '=', ref.number)
|
|
212
|
+
.execute();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
async function writeNoticeTry(
|
|
216
|
+
db: Kysely<StoreDatabase>,
|
|
217
|
+
id: number,
|
|
218
|
+
at: Instant,
|
|
219
|
+
sent: boolean,
|
|
220
|
+
): Promise<void> {
|
|
221
|
+
await db
|
|
222
|
+
.updateTable('notices')
|
|
223
|
+
.set((builder) => ({
|
|
224
|
+
tries: builder('tries', '+', 1),
|
|
225
|
+
...(sent ? { sent_at: at } : {}),
|
|
226
|
+
}))
|
|
227
|
+
.where('id', '=', id)
|
|
228
|
+
.execute();
|
|
229
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { sql } from 'kysely';
|
|
2
|
+
import type { Kysely } from 'kysely';
|
|
3
|
+
import type { StoreDatabase } from '../types.ts';
|
|
4
|
+
import { withBusyRetry } from '../with-busy-retry.ts';
|
|
5
|
+
|
|
6
|
+
export async function takeWriteLock(db: Kysely<StoreDatabase>, waitLimitMs: number): Promise<void> {
|
|
7
|
+
await withBusyRetry(waitLimitMs, () => sql`BEGIN IMMEDIATE`.execute(db));
|
|
8
|
+
}
|
package/dist/package-name.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const packageName = "@boxxy/store-sqlite";
|
package/src/package-name.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const packageName = '@boxxy/store-sqlite';
|