@checkstack/script-packages-backend 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/CHANGELOG.md +273 -0
- package/drizzle/0000_flashy_squadron_supreme.sql +63 -0
- package/drizzle/0001_flawless_drax.sql +15 -0
- package/drizzle/meta/0000_snapshot.json +395 -0
- package/drizzle/meta/0001_snapshot.json +491 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +7 -0
- package/package.json +32 -0
- package/src/atomic-symlink.test.ts +47 -0
- package/src/atomic-symlink.ts +66 -0
- package/src/blob-gc-runner.test.ts +120 -0
- package/src/blob-gc-runner.ts +139 -0
- package/src/blob-gc.test.ts +182 -0
- package/src/blob-gc.ts +161 -0
- package/src/blob-hash.test.ts +70 -0
- package/src/blob-hash.ts +56 -0
- package/src/blob-store-registry.test.ts +78 -0
- package/src/blob-store-registry.ts +75 -0
- package/src/blob-store.ts +51 -0
- package/src/cache-archive.test.ts +164 -0
- package/src/cache-archive.ts +192 -0
- package/src/cache-layout.ts +64 -0
- package/src/data-dir.test.ts +41 -0
- package/src/data-dir.ts +42 -0
- package/src/e2e-install-reconcile.test.ts +121 -0
- package/src/hooks.ts +20 -0
- package/src/index.ts +594 -0
- package/src/install-controller.test.ts +257 -0
- package/src/install-controller.ts +144 -0
- package/src/install-service.test.ts +104 -0
- package/src/install-service.ts +116 -0
- package/src/install-state-store.ts +131 -0
- package/src/lockfile.test.ts +60 -0
- package/src/lockfile.ts +0 -0
- package/src/npmrc.test.ts +48 -0
- package/src/npmrc.ts +42 -0
- package/src/package-types.test.ts +293 -0
- package/src/package-types.ts +408 -0
- package/src/parse-bun-lock.test.ts +62 -0
- package/src/parse-bun-lock.ts +59 -0
- package/src/reconcile-diff.test.ts +41 -0
- package/src/reconcile-diff.ts +26 -0
- package/src/reconcile-fs.ts +199 -0
- package/src/reconciler.test.ts +289 -0
- package/src/reconciler.ts +81 -0
- package/src/registry-client.test.ts +314 -0
- package/src/registry-client.ts +0 -0
- package/src/registry-request-config.ts +63 -0
- package/src/registry-token.test.ts +124 -0
- package/src/registry-token.ts +104 -0
- package/src/resolution-root.test.ts +82 -0
- package/src/resolution-root.ts +127 -0
- package/src/resolver.test.ts +133 -0
- package/src/resolver.ts +132 -0
- package/src/router.ts +273 -0
- package/src/schema.ts +166 -0
- package/src/size-cap.test.ts +32 -0
- package/src/size-cap.ts +40 -0
- package/src/storage-migration.test.ts +318 -0
- package/src/storage-migration.ts +213 -0
- package/src/stores.ts +533 -0
- package/src/tree-gc.test.ts +184 -0
- package/src/tree-gc.ts +160 -0
- package/src/tree-retirement.ts +81 -0
- package/src/type-acquisition-route.ts +178 -0
- package/tsconfig.json +23 -0
package/src/stores.ts
ADDED
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { eq, ne, desc, sql } from "drizzle-orm";
|
|
2
|
+
import type { SafeDatabase } from "@checkstack/backend-api";
|
|
3
|
+
import type {
|
|
4
|
+
BlobGcState,
|
|
5
|
+
ManifestEntry,
|
|
6
|
+
PackageSpec,
|
|
7
|
+
RegistryConfig,
|
|
8
|
+
SatelliteSyncState,
|
|
9
|
+
SizeCapConfig,
|
|
10
|
+
StorageConfig,
|
|
11
|
+
} from "@checkstack/script-packages-common";
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_BLOCK_BYTES,
|
|
14
|
+
DEFAULT_WARN_BYTES,
|
|
15
|
+
} from "@checkstack/script-packages-common";
|
|
16
|
+
import type { GcBlob } from "./blob-gc";
|
|
17
|
+
import {
|
|
18
|
+
scriptPackages,
|
|
19
|
+
scriptPackageRegistryConfig,
|
|
20
|
+
scriptPackageStorageConfig,
|
|
21
|
+
scriptPackageSizeCap,
|
|
22
|
+
scriptPackageBlob,
|
|
23
|
+
scriptPackageBlobGcState,
|
|
24
|
+
scriptPackageLockfileHistory,
|
|
25
|
+
scriptPackageSatelliteState,
|
|
26
|
+
} from "./schema";
|
|
27
|
+
|
|
28
|
+
const SINGLETON = "singleton";
|
|
29
|
+
|
|
30
|
+
// ─── Allowlist store ───────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
type AllowlistSchema = { scriptPackages: typeof scriptPackages };
|
|
33
|
+
|
|
34
|
+
export function createPackageStore(db: SafeDatabase<AllowlistSchema>) {
|
|
35
|
+
return {
|
|
36
|
+
async list(): Promise<PackageSpec[]> {
|
|
37
|
+
const rows = await db.select().from(scriptPackages);
|
|
38
|
+
return rows
|
|
39
|
+
.map((r) => ({
|
|
40
|
+
name: r.name,
|
|
41
|
+
version: r.version,
|
|
42
|
+
enabled: r.enabled,
|
|
43
|
+
addedBy: r.addedBy,
|
|
44
|
+
addedAt: r.addedAt,
|
|
45
|
+
updatedAt: r.updatedAt,
|
|
46
|
+
}))
|
|
47
|
+
.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
48
|
+
},
|
|
49
|
+
async upsert(input: {
|
|
50
|
+
name: string;
|
|
51
|
+
version: string;
|
|
52
|
+
addedBy?: string | null;
|
|
53
|
+
}): Promise<PackageSpec> {
|
|
54
|
+
const now = new Date();
|
|
55
|
+
await db
|
|
56
|
+
.insert(scriptPackages)
|
|
57
|
+
.values({
|
|
58
|
+
name: input.name,
|
|
59
|
+
version: input.version,
|
|
60
|
+
addedBy: input.addedBy ?? null,
|
|
61
|
+
enabled: true,
|
|
62
|
+
})
|
|
63
|
+
.onConflictDoUpdate({
|
|
64
|
+
target: scriptPackages.name,
|
|
65
|
+
set: { version: input.version, updatedAt: now },
|
|
66
|
+
});
|
|
67
|
+
const [row] = await db
|
|
68
|
+
.select()
|
|
69
|
+
.from(scriptPackages)
|
|
70
|
+
.where(eq(scriptPackages.name, input.name))
|
|
71
|
+
.limit(1);
|
|
72
|
+
return {
|
|
73
|
+
name: row!.name,
|
|
74
|
+
version: row!.version,
|
|
75
|
+
enabled: row!.enabled,
|
|
76
|
+
addedBy: row!.addedBy,
|
|
77
|
+
addedAt: row!.addedAt,
|
|
78
|
+
updatedAt: row!.updatedAt,
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
async remove(name: string): Promise<void> {
|
|
82
|
+
await db.delete(scriptPackages).where(eq(scriptPackages.name, name));
|
|
83
|
+
},
|
|
84
|
+
async setEnabled(input: {
|
|
85
|
+
name: string;
|
|
86
|
+
enabled: boolean;
|
|
87
|
+
}): Promise<PackageSpec> {
|
|
88
|
+
await db
|
|
89
|
+
.update(scriptPackages)
|
|
90
|
+
.set({ enabled: input.enabled, updatedAt: new Date() })
|
|
91
|
+
.where(eq(scriptPackages.name, input.name));
|
|
92
|
+
const [row] = await db
|
|
93
|
+
.select()
|
|
94
|
+
.from(scriptPackages)
|
|
95
|
+
.where(eq(scriptPackages.name, input.name))
|
|
96
|
+
.limit(1);
|
|
97
|
+
return {
|
|
98
|
+
name: row!.name,
|
|
99
|
+
version: row!.version,
|
|
100
|
+
enabled: row!.enabled,
|
|
101
|
+
addedBy: row!.addedBy,
|
|
102
|
+
addedAt: row!.addedAt,
|
|
103
|
+
updatedAt: row!.updatedAt,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Registry config store ───────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
type RegistrySchema = {
|
|
112
|
+
scriptPackageRegistryConfig: typeof scriptPackageRegistryConfig;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
export function createRegistryConfigStore(db: SafeDatabase<RegistrySchema>) {
|
|
116
|
+
return {
|
|
117
|
+
/** DTO view (no token); use `authSecretRef()` for the secret ref. */
|
|
118
|
+
async get(): Promise<RegistryConfig> {
|
|
119
|
+
const [row] = await db
|
|
120
|
+
.select()
|
|
121
|
+
.from(scriptPackageRegistryConfig)
|
|
122
|
+
.where(eq(scriptPackageRegistryConfig.id, SINGLETON))
|
|
123
|
+
.limit(1);
|
|
124
|
+
if (!row) {
|
|
125
|
+
return {
|
|
126
|
+
registryUrl: "https://registry.npmjs.org/",
|
|
127
|
+
scopedRegistries: [],
|
|
128
|
+
hasAuthToken: false,
|
|
129
|
+
ignoreScripts: true,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
registryUrl: row.registryUrl,
|
|
134
|
+
scopedRegistries: row.scopedRegistries,
|
|
135
|
+
hasAuthToken: Boolean(row.authSecretRef),
|
|
136
|
+
ignoreScripts: row.ignoreScripts,
|
|
137
|
+
updatedAt: row.updatedAt,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
async authSecretRef(): Promise<string | null> {
|
|
141
|
+
const [row] = await db
|
|
142
|
+
.select({ ref: scriptPackageRegistryConfig.authSecretRef })
|
|
143
|
+
.from(scriptPackageRegistryConfig)
|
|
144
|
+
.where(eq(scriptPackageRegistryConfig.id, SINGLETON))
|
|
145
|
+
.limit(1);
|
|
146
|
+
return row?.ref ?? null;
|
|
147
|
+
},
|
|
148
|
+
async set(input: {
|
|
149
|
+
registryUrl: string;
|
|
150
|
+
scopedRegistries: { scope: string; registryUrl: string }[];
|
|
151
|
+
ignoreScripts: boolean;
|
|
152
|
+
/** Pass to set; undefined leaves untouched; null clears. */
|
|
153
|
+
authSecretRef?: string | null;
|
|
154
|
+
}): Promise<void> {
|
|
155
|
+
const set: Record<string, unknown> = {
|
|
156
|
+
registryUrl: input.registryUrl,
|
|
157
|
+
scopedRegistries: input.scopedRegistries,
|
|
158
|
+
ignoreScripts: input.ignoreScripts,
|
|
159
|
+
updatedAt: new Date(),
|
|
160
|
+
};
|
|
161
|
+
if (input.authSecretRef !== undefined) {
|
|
162
|
+
set.authSecretRef = input.authSecretRef;
|
|
163
|
+
}
|
|
164
|
+
await db
|
|
165
|
+
.insert(scriptPackageRegistryConfig)
|
|
166
|
+
.values({ id: SINGLETON, ...set })
|
|
167
|
+
.onConflictDoUpdate({
|
|
168
|
+
target: scriptPackageRegistryConfig.id,
|
|
169
|
+
set,
|
|
170
|
+
});
|
|
171
|
+
},
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ─── Storage config store ────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
type StorageSchema = {
|
|
178
|
+
scriptPackageStorageConfig: typeof scriptPackageStorageConfig;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
export function createStorageConfigStore(db: SafeDatabase<StorageSchema>) {
|
|
182
|
+
return {
|
|
183
|
+
async get(): Promise<StorageConfig> {
|
|
184
|
+
const [row] = await db
|
|
185
|
+
.select()
|
|
186
|
+
.from(scriptPackageStorageConfig)
|
|
187
|
+
.where(eq(scriptPackageStorageConfig.id, SINGLETON))
|
|
188
|
+
.limit(1);
|
|
189
|
+
if (!row) {
|
|
190
|
+
return {
|
|
191
|
+
activeBackend: "postgres",
|
|
192
|
+
migrationStatus: "idle",
|
|
193
|
+
migrationTarget: null,
|
|
194
|
+
migratedCount: 0,
|
|
195
|
+
migrationError: null,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
activeBackend: row.activeBackend,
|
|
200
|
+
migrationStatus: row.migrationStatus as StorageConfig["migrationStatus"],
|
|
201
|
+
migrationTarget: row.migrationTarget,
|
|
202
|
+
migratedCount: row.migratedCount,
|
|
203
|
+
migrationError: row.migrationError,
|
|
204
|
+
updatedAt: row.updatedAt,
|
|
205
|
+
};
|
|
206
|
+
},
|
|
207
|
+
async setActiveBackend(backend: string): Promise<void> {
|
|
208
|
+
const set = { activeBackend: backend, updatedAt: new Date() };
|
|
209
|
+
await db
|
|
210
|
+
.insert(scriptPackageStorageConfig)
|
|
211
|
+
.values({ id: SINGLETON, ...set })
|
|
212
|
+
.onConflictDoUpdate({
|
|
213
|
+
target: scriptPackageStorageConfig.id,
|
|
214
|
+
set,
|
|
215
|
+
});
|
|
216
|
+
},
|
|
217
|
+
/** Mark a migration as started (idempotent; resets count + error). */
|
|
218
|
+
async beginMigration(target: string): Promise<void> {
|
|
219
|
+
const set = {
|
|
220
|
+
migrationStatus: "migrating" as const,
|
|
221
|
+
migrationTarget: target,
|
|
222
|
+
migratedCount: 0,
|
|
223
|
+
migrationError: null,
|
|
224
|
+
updatedAt: new Date(),
|
|
225
|
+
};
|
|
226
|
+
await db
|
|
227
|
+
.insert(scriptPackageStorageConfig)
|
|
228
|
+
.values({ id: SINGLETON, ...set })
|
|
229
|
+
.onConflictDoUpdate({ target: scriptPackageStorageConfig.id, set });
|
|
230
|
+
},
|
|
231
|
+
/** Update the migrated-blob counter (progress). */
|
|
232
|
+
async setMigratedCount(count: number): Promise<void> {
|
|
233
|
+
await db
|
|
234
|
+
.update(scriptPackageStorageConfig)
|
|
235
|
+
.set({ migratedCount: count, updatedAt: new Date() })
|
|
236
|
+
.where(eq(scriptPackageStorageConfig.id, SINGLETON));
|
|
237
|
+
},
|
|
238
|
+
/**
|
|
239
|
+
* Atomically complete a migration: flip the active backend to the target
|
|
240
|
+
* and mark completed. Done in one UPDATE so a reader never sees a
|
|
241
|
+
* "completed but old active backend" state.
|
|
242
|
+
*/
|
|
243
|
+
async completeMigration(target: string): Promise<void> {
|
|
244
|
+
await db
|
|
245
|
+
.update(scriptPackageStorageConfig)
|
|
246
|
+
.set({
|
|
247
|
+
activeBackend: target,
|
|
248
|
+
migrationStatus: "completed",
|
|
249
|
+
migrationError: null,
|
|
250
|
+
updatedAt: new Date(),
|
|
251
|
+
})
|
|
252
|
+
.where(eq(scriptPackageStorageConfig.id, SINGLETON));
|
|
253
|
+
},
|
|
254
|
+
/** Record a migration failure (leaves the active backend unchanged). */
|
|
255
|
+
async failMigration(message: string): Promise<void> {
|
|
256
|
+
await db
|
|
257
|
+
.update(scriptPackageStorageConfig)
|
|
258
|
+
.set({
|
|
259
|
+
migrationStatus: "error",
|
|
260
|
+
migrationError: message,
|
|
261
|
+
updatedAt: new Date(),
|
|
262
|
+
})
|
|
263
|
+
.where(eq(scriptPackageStorageConfig.id, SINGLETON));
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// ─── Size-cap store ────────────────────────────────────────────────────────
|
|
269
|
+
|
|
270
|
+
type SizeCapSchema = { scriptPackageSizeCap: typeof scriptPackageSizeCap };
|
|
271
|
+
|
|
272
|
+
export function createSizeCapStore(db: SafeDatabase<SizeCapSchema>) {
|
|
273
|
+
return {
|
|
274
|
+
async get(): Promise<SizeCapConfig> {
|
|
275
|
+
const [row] = await db
|
|
276
|
+
.select()
|
|
277
|
+
.from(scriptPackageSizeCap)
|
|
278
|
+
.where(eq(scriptPackageSizeCap.id, SINGLETON))
|
|
279
|
+
.limit(1);
|
|
280
|
+
return {
|
|
281
|
+
warnBytes: row?.warnBytes ?? DEFAULT_WARN_BYTES,
|
|
282
|
+
blockBytes: row?.blockBytes ?? DEFAULT_BLOCK_BYTES,
|
|
283
|
+
};
|
|
284
|
+
},
|
|
285
|
+
async set(cap: SizeCapConfig): Promise<void> {
|
|
286
|
+
const set = {
|
|
287
|
+
warnBytes: cap.warnBytes,
|
|
288
|
+
blockBytes: cap.blockBytes,
|
|
289
|
+
updatedAt: new Date(),
|
|
290
|
+
};
|
|
291
|
+
await db
|
|
292
|
+
.insert(scriptPackageSizeCap)
|
|
293
|
+
.values({ id: SINGLETON, ...set })
|
|
294
|
+
.onConflictDoUpdate({ target: scriptPackageSizeCap.id, set });
|
|
295
|
+
},
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// ─── Blob index store ──────────────────────────────────────────────────────
|
|
300
|
+
|
|
301
|
+
type BlobIndexSchema = { scriptPackageBlob: typeof scriptPackageBlob };
|
|
302
|
+
|
|
303
|
+
export function createBlobIndexStore(db: SafeDatabase<BlobIndexSchema>) {
|
|
304
|
+
return {
|
|
305
|
+
async record(input: {
|
|
306
|
+
integrity: string;
|
|
307
|
+
name: string;
|
|
308
|
+
version: string;
|
|
309
|
+
backend: string;
|
|
310
|
+
sizeBytes: number;
|
|
311
|
+
}): Promise<void> {
|
|
312
|
+
await db
|
|
313
|
+
.insert(scriptPackageBlob)
|
|
314
|
+
.values(input)
|
|
315
|
+
.onConflictDoUpdate({
|
|
316
|
+
target: scriptPackageBlob.integrity,
|
|
317
|
+
set: { backend: input.backend, sizeBytes: input.sizeBytes },
|
|
318
|
+
});
|
|
319
|
+
},
|
|
320
|
+
async backendFor(integrity: string): Promise<string | undefined> {
|
|
321
|
+
const [row] = await db
|
|
322
|
+
.select({ backend: scriptPackageBlob.backend })
|
|
323
|
+
.from(scriptPackageBlob)
|
|
324
|
+
.where(eq(scriptPackageBlob.integrity, integrity))
|
|
325
|
+
.limit(1);
|
|
326
|
+
return row?.backend;
|
|
327
|
+
},
|
|
328
|
+
/** All indexed blobs (integrity + current backend). */
|
|
329
|
+
async list(): Promise<{ integrity: string; backend: string }[]> {
|
|
330
|
+
const rows = await db
|
|
331
|
+
.select({
|
|
332
|
+
integrity: scriptPackageBlob.integrity,
|
|
333
|
+
backend: scriptPackageBlob.backend,
|
|
334
|
+
})
|
|
335
|
+
.from(scriptPackageBlob);
|
|
336
|
+
return rows;
|
|
337
|
+
},
|
|
338
|
+
/**
|
|
339
|
+
* Blobs whose recorded backend is NOT `target` - the work set for a
|
|
340
|
+
* migration to `target`. Resumability: a resumed migration re-derives
|
|
341
|
+
* this set, so blobs already flipped to `target` are skipped.
|
|
342
|
+
*/
|
|
343
|
+
async listNotOnBackend(
|
|
344
|
+
target: string,
|
|
345
|
+
): Promise<{ integrity: string; backend: string }[]> {
|
|
346
|
+
const rows = await db
|
|
347
|
+
.select({
|
|
348
|
+
integrity: scriptPackageBlob.integrity,
|
|
349
|
+
backend: scriptPackageBlob.backend,
|
|
350
|
+
})
|
|
351
|
+
.from(scriptPackageBlob)
|
|
352
|
+
.where(ne(scriptPackageBlob.backend, target));
|
|
353
|
+
return rows;
|
|
354
|
+
},
|
|
355
|
+
/** Flip a blob's recorded backend after a verified copy. */
|
|
356
|
+
async setBackend(integrity: string, backend: string): Promise<void> {
|
|
357
|
+
await db
|
|
358
|
+
.update(scriptPackageBlob)
|
|
359
|
+
.set({ backend })
|
|
360
|
+
.where(eq(scriptPackageBlob.integrity, integrity));
|
|
361
|
+
},
|
|
362
|
+
/** Every indexed blob with the metadata the GC needs (size + created_at). */
|
|
363
|
+
async listWithMeta(): Promise<GcBlob[]> {
|
|
364
|
+
const rows = await db
|
|
365
|
+
.select({
|
|
366
|
+
integrity: scriptPackageBlob.integrity,
|
|
367
|
+
backend: scriptPackageBlob.backend,
|
|
368
|
+
sizeBytes: scriptPackageBlob.sizeBytes,
|
|
369
|
+
createdAt: scriptPackageBlob.createdAt,
|
|
370
|
+
})
|
|
371
|
+
.from(scriptPackageBlob);
|
|
372
|
+
return rows;
|
|
373
|
+
},
|
|
374
|
+
/** Remove the index row for a GC'd blob (after deleting its bytes). */
|
|
375
|
+
async remove(integrity: string): Promise<void> {
|
|
376
|
+
await db
|
|
377
|
+
.delete(scriptPackageBlob)
|
|
378
|
+
.where(eq(scriptPackageBlob.integrity, integrity));
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ─── Lockfile history store ──────────────────────────────────────────────────
|
|
384
|
+
|
|
385
|
+
type LockfileHistorySchema = {
|
|
386
|
+
scriptPackageLockfileHistory: typeof scriptPackageLockfileHistory;
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
export function createLockfileHistoryStore(
|
|
390
|
+
db: SafeDatabase<LockfileHistorySchema>,
|
|
391
|
+
) {
|
|
392
|
+
return {
|
|
393
|
+
/** Record a successful install's manifest (idempotent on hash). */
|
|
394
|
+
async record(input: {
|
|
395
|
+
lockfileHash: string;
|
|
396
|
+
manifest: ManifestEntry[];
|
|
397
|
+
}): Promise<void> {
|
|
398
|
+
await db
|
|
399
|
+
.insert(scriptPackageLockfileHistory)
|
|
400
|
+
.values({
|
|
401
|
+
lockfileHash: input.lockfileHash,
|
|
402
|
+
manifest: input.manifest,
|
|
403
|
+
recordedAt: new Date(),
|
|
404
|
+
})
|
|
405
|
+
.onConflictDoUpdate({
|
|
406
|
+
target: scriptPackageLockfileHistory.lockfileHash,
|
|
407
|
+
// Re-installing the same hash refreshes its recency so it stays in
|
|
408
|
+
// the retained window.
|
|
409
|
+
set: { recordedAt: new Date() },
|
|
410
|
+
});
|
|
411
|
+
},
|
|
412
|
+
/** Manifests for the most-recent `limit` hashes (newest first). */
|
|
413
|
+
async recent(limit: number): Promise<ManifestEntry[][]> {
|
|
414
|
+
const rows = await db
|
|
415
|
+
.select({ manifest: scriptPackageLockfileHistory.manifest })
|
|
416
|
+
.from(scriptPackageLockfileHistory)
|
|
417
|
+
.orderBy(desc(scriptPackageLockfileHistory.recordedAt))
|
|
418
|
+
.limit(limit);
|
|
419
|
+
return rows.map((r) => r.manifest);
|
|
420
|
+
},
|
|
421
|
+
/** Hashes to keep (the most-recent `limit`); used to prune the rest. */
|
|
422
|
+
async retainedHashes(limit: number): Promise<string[]> {
|
|
423
|
+
const rows = await db
|
|
424
|
+
.select({ lockfileHash: scriptPackageLockfileHistory.lockfileHash })
|
|
425
|
+
.from(scriptPackageLockfileHistory)
|
|
426
|
+
.orderBy(desc(scriptPackageLockfileHistory.recordedAt))
|
|
427
|
+
.limit(limit);
|
|
428
|
+
return rows.map((r) => r.lockfileHash);
|
|
429
|
+
},
|
|
430
|
+
/** Prune history rows older than the most-recent `keep` hashes. */
|
|
431
|
+
async pruneOlderThan(keep: number): Promise<void> {
|
|
432
|
+
const keepHashes = await this.retainedHashes(keep);
|
|
433
|
+
if (keepHashes.length === 0) return;
|
|
434
|
+
await db
|
|
435
|
+
.delete(scriptPackageLockfileHistory)
|
|
436
|
+
.where(
|
|
437
|
+
sql`${scriptPackageLockfileHistory.lockfileHash} NOT IN (${sql.join(
|
|
438
|
+
keepHashes.map((h) => sql`${h}`),
|
|
439
|
+
sql`, `,
|
|
440
|
+
)})`,
|
|
441
|
+
);
|
|
442
|
+
},
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// ─── Blob-GC state store ─────────────────────────────────────────────────────
|
|
447
|
+
|
|
448
|
+
type BlobGcStateSchema = {
|
|
449
|
+
scriptPackageBlobGcState: typeof scriptPackageBlobGcState;
|
|
450
|
+
};
|
|
451
|
+
|
|
452
|
+
export function createBlobGcStateStore(db: SafeDatabase<BlobGcStateSchema>) {
|
|
453
|
+
return {
|
|
454
|
+
async get(): Promise<BlobGcState> {
|
|
455
|
+
const [row] = await db
|
|
456
|
+
.select()
|
|
457
|
+
.from(scriptPackageBlobGcState)
|
|
458
|
+
.where(eq(scriptPackageBlobGcState.id, SINGLETON))
|
|
459
|
+
.limit(1);
|
|
460
|
+
return {
|
|
461
|
+
lastRunAt: row?.lastRunAt ?? null,
|
|
462
|
+
lastDeleted: row?.lastDeleted ?? 0,
|
|
463
|
+
lastBytesReclaimed: row?.lastBytesReclaimed ?? 0,
|
|
464
|
+
totalBytesReclaimed: row?.totalBytesReclaimed ?? 0,
|
|
465
|
+
};
|
|
466
|
+
},
|
|
467
|
+
/** Record a completed run, accumulating total reclaimed bytes. */
|
|
468
|
+
async recordRun(input: {
|
|
469
|
+
deleted: number;
|
|
470
|
+
bytesReclaimed: number;
|
|
471
|
+
}): Promise<void> {
|
|
472
|
+
await db
|
|
473
|
+
.insert(scriptPackageBlobGcState)
|
|
474
|
+
.values({
|
|
475
|
+
id: SINGLETON,
|
|
476
|
+
lastRunAt: new Date(),
|
|
477
|
+
lastDeleted: input.deleted,
|
|
478
|
+
lastBytesReclaimed: input.bytesReclaimed,
|
|
479
|
+
totalBytesReclaimed: input.bytesReclaimed,
|
|
480
|
+
})
|
|
481
|
+
.onConflictDoUpdate({
|
|
482
|
+
target: scriptPackageBlobGcState.id,
|
|
483
|
+
set: {
|
|
484
|
+
lastRunAt: new Date(),
|
|
485
|
+
lastDeleted: input.deleted,
|
|
486
|
+
lastBytesReclaimed: input.bytesReclaimed,
|
|
487
|
+
totalBytesReclaimed: sql`${scriptPackageBlobGcState.totalBytesReclaimed} + ${input.bytesReclaimed}`,
|
|
488
|
+
},
|
|
489
|
+
});
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ─── Satellite sync state store ────────────────────────────────────────────
|
|
495
|
+
|
|
496
|
+
type SatelliteSchema = {
|
|
497
|
+
scriptPackageSatelliteState: typeof scriptPackageSatelliteState;
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
export function createSatelliteStateStore(db: SafeDatabase<SatelliteSchema>) {
|
|
501
|
+
return {
|
|
502
|
+
async list(): Promise<SatelliteSyncState[]> {
|
|
503
|
+
const rows = await db.select().from(scriptPackageSatelliteState);
|
|
504
|
+
return rows.map((r) => ({
|
|
505
|
+
satelliteId: r.satelliteId,
|
|
506
|
+
lockfileHash: r.lockfileHash,
|
|
507
|
+
status: r.status as SatelliteSyncState["status"],
|
|
508
|
+
errorMessage: r.errorMessage,
|
|
509
|
+
syncedAt: r.syncedAt,
|
|
510
|
+
}));
|
|
511
|
+
},
|
|
512
|
+
async report(input: {
|
|
513
|
+
satelliteId: string;
|
|
514
|
+
lockfileHash: string | null;
|
|
515
|
+
status: SatelliteSyncState["status"];
|
|
516
|
+
errorMessage?: string | null;
|
|
517
|
+
}): Promise<void> {
|
|
518
|
+
const set = {
|
|
519
|
+
lockfileHash: input.lockfileHash,
|
|
520
|
+
status: input.status,
|
|
521
|
+
errorMessage: input.errorMessage ?? null,
|
|
522
|
+
syncedAt: new Date(),
|
|
523
|
+
};
|
|
524
|
+
await db
|
|
525
|
+
.insert(scriptPackageSatelliteState)
|
|
526
|
+
.values({ satelliteId: input.satelliteId, ...set })
|
|
527
|
+
.onConflictDoUpdate({
|
|
528
|
+
target: scriptPackageSatelliteState.satelliteId,
|
|
529
|
+
set,
|
|
530
|
+
});
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|