@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.
Files changed (54) hide show
  1. package/README.md +24 -3
  2. package/dist/codecs/parse-json-text.d.ts +1 -0
  3. package/dist/codecs/parse-store-row.d.ts +2 -0
  4. package/dist/codecs/table-codecs.d.ts +10 -0
  5. package/dist/codecs/to-column-name.d.ts +1 -0
  6. package/dist/codecs/to-store-columns.d.ts +3 -0
  7. package/dist/constants.d.ts +1 -0
  8. package/dist/create-sqlite-claim-store.d.ts +5 -0
  9. package/dist/index.d.ts +1 -1
  10. package/dist/migrations/create-tables-migration.d.ts +2 -0
  11. package/dist/migrations/rename-relations-migration.d.ts +2 -0
  12. package/dist/migrations/run-store-migrations.d.ts +3 -0
  13. package/dist/migrations/v1-tables-migration.d.ts +2 -0
  14. package/dist/reads/build-sqlite-reader.d.ts +4 -0
  15. package/dist/reads/is-latest-of-session.d.ts +3 -0
  16. package/dist/reads/read-item-views.d.ts +5 -0
  17. package/dist/reads/read-limit-fills.d.ts +4 -0
  18. package/dist/reads/read-wakes.d.ts +4 -0
  19. package/dist/test-utils/create-claim-bench.d.ts +8 -0
  20. package/dist/test-utils/create-test-claim-store.d.ts +2 -0
  21. package/dist/types.d.ts +172 -0
  22. package/dist/with-busy-retry.d.ts +1 -0
  23. package/dist/with-transaction.d.ts +3 -0
  24. package/dist/writes/apply-sqlite-change.d.ts +4 -0
  25. package/dist/writes/create-sqlite-tx.d.ts +4 -0
  26. package/dist/writes/take-write-lock.d.ts +3 -0
  27. package/package.json +15 -1
  28. package/src/codecs/parse-json-text.ts +5 -0
  29. package/src/codecs/parse-store-row.ts +36 -0
  30. package/src/codecs/table-codecs.ts +148 -0
  31. package/src/codecs/to-column-name.ts +3 -0
  32. package/src/codecs/to-store-columns.ts +44 -0
  33. package/src/constants.ts +1 -0
  34. package/src/create-sqlite-claim-store.ts +114 -0
  35. package/src/index.ts +1 -1
  36. package/src/migrations/create-tables-migration.ts +146 -0
  37. package/src/migrations/rename-relations-migration.ts +17 -0
  38. package/src/migrations/run-store-migrations.ts +42 -0
  39. package/src/migrations/v1-tables-migration.ts +184 -0
  40. package/src/reads/build-sqlite-reader.ts +484 -0
  41. package/src/reads/is-latest-of-session.ts +21 -0
  42. package/src/reads/read-item-views.ts +92 -0
  43. package/src/reads/read-limit-fills.ts +32 -0
  44. package/src/reads/read-wakes.ts +34 -0
  45. package/src/test-utils/create-claim-bench.ts +66 -0
  46. package/src/test-utils/create-test-claim-store.ts +23 -0
  47. package/src/types.ts +173 -0
  48. package/src/with-busy-retry.ts +32 -0
  49. package/src/with-transaction.ts +29 -0
  50. package/src/writes/apply-sqlite-change.ts +223 -0
  51. package/src/writes/create-sqlite-tx.ts +229 -0
  52. package/src/writes/take-write-lock.ts +8 -0
  53. package/dist/package-name.d.ts +0 -1
  54. 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
+ }
@@ -1 +0,0 @@
1
- export declare const packageName = "@boxxy/store-sqlite";
@@ -1 +0,0 @@
1
- export const packageName = '@boxxy/store-sqlite';