@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/index.ts
ADDED
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { createBackendPlugin, coreServices } from "@checkstack/backend-api";
|
|
3
|
+
import { internalSecretsRef } from "@checkstack/secrets-backend";
|
|
4
|
+
import {
|
|
5
|
+
createRegistryTokenStore,
|
|
6
|
+
migrateRegistryTokenToPlatform,
|
|
7
|
+
type RegistryTokenStore,
|
|
8
|
+
} from "./registry-token";
|
|
9
|
+
import {
|
|
10
|
+
pluginMetadata,
|
|
11
|
+
scriptPackagesAccessRules,
|
|
12
|
+
scriptPackagesContract,
|
|
13
|
+
type BlobGcSummary,
|
|
14
|
+
} from "@checkstack/script-packages-common";
|
|
15
|
+
import type { PluginMetadata } from "@checkstack/common";
|
|
16
|
+
import { extractErrorMessage } from "@checkstack/common";
|
|
17
|
+
import { blobStoreExtensionPoint, type BlobStore } from "./blob-store";
|
|
18
|
+
import {
|
|
19
|
+
createBlobStoreRegistry,
|
|
20
|
+
type BlobStoreRegistry,
|
|
21
|
+
} from "./blob-store-registry";
|
|
22
|
+
import { resolveScriptPackagesDir, storePaths } from "./data-dir";
|
|
23
|
+
import {
|
|
24
|
+
createPackageStore,
|
|
25
|
+
createRegistryConfigStore,
|
|
26
|
+
createSizeCapStore,
|
|
27
|
+
createStorageConfigStore,
|
|
28
|
+
createBlobIndexStore,
|
|
29
|
+
createBlobGcStateStore,
|
|
30
|
+
createLockfileHistoryStore,
|
|
31
|
+
} from "./stores";
|
|
32
|
+
import { createBlobGcTrigger } from "./blob-gc-runner";
|
|
33
|
+
import {
|
|
34
|
+
createInstallStateStore,
|
|
35
|
+
createInstallerLock,
|
|
36
|
+
} from "./install-state-store";
|
|
37
|
+
import { runInstallNow } from "./install-controller";
|
|
38
|
+
import {
|
|
39
|
+
runStorageMigration,
|
|
40
|
+
resumeCrashedMigration,
|
|
41
|
+
} from "./storage-migration";
|
|
42
|
+
import { createCentralResolver } from "./resolver";
|
|
43
|
+
import { resolveRegistryRequestConfig } from "./registry-request-config";
|
|
44
|
+
import { createReconcileFsDeps } from "./reconcile-fs";
|
|
45
|
+
import { reconcileToHash } from "./reconciler";
|
|
46
|
+
import { scriptPackagesChangedHook } from "./hooks";
|
|
47
|
+
import { createScriptPackagesRouter } from "./router";
|
|
48
|
+
import { createTypeClosureHttpHandler } from "./type-acquisition-route";
|
|
49
|
+
import { TYPE_ACQUISITION_PATH_PREFIX } from "@checkstack/script-packages-common";
|
|
50
|
+
import * as schema from "./schema";
|
|
51
|
+
|
|
52
|
+
interface EnvStash {
|
|
53
|
+
blobStores: BlobStoreRegistry;
|
|
54
|
+
/**
|
|
55
|
+
* Set in `afterPluginsReady` (the only phase where `emitHook` exists) and
|
|
56
|
+
* called by the installer (wired in `init`) after a successful install.
|
|
57
|
+
* Undefined until `afterPluginsReady` runs.
|
|
58
|
+
*/
|
|
59
|
+
emitChanged?: (lockfileHash: string) => Promise<void>;
|
|
60
|
+
/** Registry token store (internal secrets), set in `init`. */
|
|
61
|
+
registryToken?: RegistryTokenStore;
|
|
62
|
+
/**
|
|
63
|
+
* Blob-GC trigger built in `init` (wires stores + the installer lock).
|
|
64
|
+
* Reused by the scheduled recurring job registered in `afterPluginsReady`.
|
|
65
|
+
*/
|
|
66
|
+
triggerBlobGc?: () => Promise<BlobGcSummary>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default createBackendPlugin({
|
|
70
|
+
metadata: pluginMetadata,
|
|
71
|
+
|
|
72
|
+
register(env) {
|
|
73
|
+
const blobStores = createBlobStoreRegistry();
|
|
74
|
+
(env as unknown as EnvStash).blobStores = blobStores;
|
|
75
|
+
|
|
76
|
+
env.registerAccessRules(scriptPackagesAccessRules);
|
|
77
|
+
|
|
78
|
+
env.registerExtensionPoint(blobStoreExtensionPoint, {
|
|
79
|
+
registerBlobStore: (store: BlobStore, _metadata: PluginMetadata) => {
|
|
80
|
+
blobStores.register(store);
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
env.registerInit({
|
|
85
|
+
schema,
|
|
86
|
+
deps: {
|
|
87
|
+
logger: coreServices.logger,
|
|
88
|
+
rpc: coreServices.rpc,
|
|
89
|
+
auth: coreServices.auth,
|
|
90
|
+
advisoryLock: coreServices.advisoryLock,
|
|
91
|
+
queueManager: coreServices.queueManager,
|
|
92
|
+
internalSecrets: internalSecretsRef,
|
|
93
|
+
},
|
|
94
|
+
init: async ({
|
|
95
|
+
logger,
|
|
96
|
+
database,
|
|
97
|
+
rpc,
|
|
98
|
+
auth,
|
|
99
|
+
advisoryLock,
|
|
100
|
+
internalSecrets,
|
|
101
|
+
}) => {
|
|
102
|
+
logger.debug("๐ฆ Initializing Script Packages Backend...");
|
|
103
|
+
|
|
104
|
+
const storeRoot = resolveScriptPackagesDir();
|
|
105
|
+
const packages = createPackageStore(database);
|
|
106
|
+
const registry = createRegistryConfigStore(database);
|
|
107
|
+
const storage = createStorageConfigStore(database);
|
|
108
|
+
const registryToken = createRegistryTokenStore({ internalSecrets });
|
|
109
|
+
(env as unknown as EnvStash).registryToken = registryToken;
|
|
110
|
+
const sizeCap = createSizeCapStore(database);
|
|
111
|
+
const blobIndex = createBlobIndexStore(database);
|
|
112
|
+
const lockfileHistory = createLockfileHistoryStore(database);
|
|
113
|
+
const blobGcState = createBlobGcStateStore(database);
|
|
114
|
+
const installState = createInstallStateStore(database);
|
|
115
|
+
const installerLock = createInstallerLock(advisoryLock);
|
|
116
|
+
|
|
117
|
+
// Whether an install OR storage migration is in flight โ the guard
|
|
118
|
+
// both `triggerInstall` (migration check) and the blob GC use.
|
|
119
|
+
const isBusy = async () => {
|
|
120
|
+
const [cfg, state] = await Promise.all([
|
|
121
|
+
storage.get(),
|
|
122
|
+
installState.load(),
|
|
123
|
+
]);
|
|
124
|
+
return (
|
|
125
|
+
cfg.migrationStatus === "migrating" || state.status === "installing"
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Blob GC trigger: shared by the admin `gcBlobs` RPC and the
|
|
130
|
+
// scheduled recurring job. Holds the installer lock for the pass.
|
|
131
|
+
const triggerBlobGc = createBlobGcTrigger({
|
|
132
|
+
installerLock,
|
|
133
|
+
blobStores,
|
|
134
|
+
loadCurrent: async () => {
|
|
135
|
+
const state = await installState.load();
|
|
136
|
+
return {
|
|
137
|
+
lockfileHash: state.lockfileHash,
|
|
138
|
+
manifest: state.manifest,
|
|
139
|
+
};
|
|
140
|
+
},
|
|
141
|
+
recentHistory: (limit) => lockfileHistory.recent(limit),
|
|
142
|
+
pruneHistory: (keep) => lockfileHistory.pruneOlderThan(keep),
|
|
143
|
+
listBlobs: () => blobIndex.listWithMeta(),
|
|
144
|
+
removeBlobRow: (integrity) => blobIndex.remove(integrity),
|
|
145
|
+
isBusy,
|
|
146
|
+
recordRun: (r) => blobGcState.recordRun(r),
|
|
147
|
+
logger,
|
|
148
|
+
});
|
|
149
|
+
(env as unknown as EnvStash).triggerBlobGc = triggerBlobGc;
|
|
150
|
+
|
|
151
|
+
// Build the install orchestration. The resolver + active blob store
|
|
152
|
+
// are resolved lazily at install time so config/registry changes
|
|
153
|
+
// and store-plugin registration order don't matter.
|
|
154
|
+
const triggerInstall = async () => {
|
|
155
|
+
// Same registry + token resolution the live registry-client RPCs
|
|
156
|
+
// use (shared helper) so the install and autocomplete paths can
|
|
157
|
+
// never drift on how they talk to the registry.
|
|
158
|
+
const reqConfig = await resolveRegistryRequestConfig({
|
|
159
|
+
registry,
|
|
160
|
+
registryToken,
|
|
161
|
+
logger,
|
|
162
|
+
});
|
|
163
|
+
const reg = await registry.get();
|
|
164
|
+
const storageConfig = await storage.get();
|
|
165
|
+
const activeBackend = storageConfig.activeBackend;
|
|
166
|
+
if (!blobStores.has(activeBackend)) {
|
|
167
|
+
return {
|
|
168
|
+
started: false,
|
|
169
|
+
reason: `Active blob store "${activeBackend}" is not registered.`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
const store = blobStores.get(activeBackend);
|
|
173
|
+
const paths = storePaths(storeRoot);
|
|
174
|
+
const resolver = createCentralResolver({
|
|
175
|
+
scratchDir: path.join(paths.root, ".install-scratch"),
|
|
176
|
+
cacheDir: paths.cache,
|
|
177
|
+
registry: {
|
|
178
|
+
registryUrl: reqConfig.registryUrl,
|
|
179
|
+
scopedRegistries: reqConfig.scopedRegistries,
|
|
180
|
+
authToken: reqConfig.authToken,
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
return runInstallNow({
|
|
184
|
+
installState,
|
|
185
|
+
installerLock,
|
|
186
|
+
resolver,
|
|
187
|
+
blobStore: {
|
|
188
|
+
id: store.id,
|
|
189
|
+
has: (i) => store.has(i),
|
|
190
|
+
put: (i) => store.put(i),
|
|
191
|
+
},
|
|
192
|
+
blobIndex,
|
|
193
|
+
loadInstallInputs: async () => ({
|
|
194
|
+
packages: await packages.list(),
|
|
195
|
+
ignoreScripts: reg.ignoreScripts,
|
|
196
|
+
}),
|
|
197
|
+
sizeCap: () => sizeCap.get(),
|
|
198
|
+
isMigrationInFlight: async () => {
|
|
199
|
+
const cfg = await storage.get();
|
|
200
|
+
return cfg.migrationStatus === "migrating";
|
|
201
|
+
},
|
|
202
|
+
recordHistory: ({ lockfileHash, manifest }) =>
|
|
203
|
+
lockfileHistory.record({ lockfileHash, manifest }),
|
|
204
|
+
emitChanged: async ({ lockfileHash }) => {
|
|
205
|
+
await (env as unknown as EnvStash).emitChanged?.(lockfileHash);
|
|
206
|
+
},
|
|
207
|
+
logger,
|
|
208
|
+
});
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
// Kick a storage migration in the background. Mutually exclusive
|
|
212
|
+
// with installs via the installer advisory lock (an install refuses
|
|
213
|
+
// while a migration is in flight via `isMigrationInFlight`; this
|
|
214
|
+
// refuses while an install holds the lock). Returns immediately;
|
|
215
|
+
// progress is polled via `getStorageMigrationState`.
|
|
216
|
+
const triggerMigration = async ({ target }: { target: string }) => {
|
|
217
|
+
const current = await storage.get();
|
|
218
|
+
if (current.migrationStatus === "migrating") {
|
|
219
|
+
return {
|
|
220
|
+
started: false,
|
|
221
|
+
reason: "A storage migration is already in progress.",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (current.activeBackend === target) {
|
|
225
|
+
return {
|
|
226
|
+
started: false,
|
|
227
|
+
reason: `"${target}" is already the active backend.`,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
if (!blobStores.has(target)) {
|
|
231
|
+
return {
|
|
232
|
+
started: false,
|
|
233
|
+
reason: `Target blob store "${target}" is not registered.`,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const lock = await installerLock.tryInstallerLock();
|
|
237
|
+
if (!lock) {
|
|
238
|
+
return {
|
|
239
|
+
started: false,
|
|
240
|
+
reason: "An install is in progress; try again once it completes.",
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
// Run in the background; release the lock when done.
|
|
244
|
+
void runStorageMigration({
|
|
245
|
+
blobIndex,
|
|
246
|
+
storage,
|
|
247
|
+
getStore: (id) => blobStores.get(id),
|
|
248
|
+
activeBackend: current.activeBackend,
|
|
249
|
+
target,
|
|
250
|
+
logger,
|
|
251
|
+
}).finally(() => {
|
|
252
|
+
void lock.release();
|
|
253
|
+
});
|
|
254
|
+
return { started: true };
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const router = createScriptPackagesRouter({
|
|
258
|
+
db: database,
|
|
259
|
+
blobStores,
|
|
260
|
+
logger,
|
|
261
|
+
triggerInstall,
|
|
262
|
+
triggerMigration,
|
|
263
|
+
triggerBlobGc,
|
|
264
|
+
registryToken,
|
|
265
|
+
});
|
|
266
|
+
rpc.registerRouter(router, scriptPackagesContract);
|
|
267
|
+
|
|
268
|
+
// Raw, HTTP-cacheable route for editor lazy ATA (package `.d.ts`
|
|
269
|
+
// closures). Served outside oRPC so the response can carry
|
|
270
|
+
// `Cache-Control` (oRPC procedures here can't set response headers).
|
|
271
|
+
// Mounted at `/api/script-packages/types/:hash/:specifier`.
|
|
272
|
+
rpc.registerHttpHandler(
|
|
273
|
+
createTypeClosureHttpHandler({
|
|
274
|
+
auth,
|
|
275
|
+
getLockfileHash: async () => {
|
|
276
|
+
const state = await installState.load();
|
|
277
|
+
return state.lockfileHash;
|
|
278
|
+
},
|
|
279
|
+
storeRoot,
|
|
280
|
+
logger,
|
|
281
|
+
}),
|
|
282
|
+
TYPE_ACQUISITION_PATH_PREFIX,
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
logger.debug("โ
Script Packages Backend initialized.");
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
afterPluginsReady: async ({
|
|
289
|
+
logger,
|
|
290
|
+
database,
|
|
291
|
+
onHook,
|
|
292
|
+
emitHook,
|
|
293
|
+
advisoryLock,
|
|
294
|
+
queueManager,
|
|
295
|
+
}) => {
|
|
296
|
+
const stash = env as unknown as EnvStash;
|
|
297
|
+
const blobStores = stash.blobStores;
|
|
298
|
+
const storeRoot = resolveScriptPackagesDir();
|
|
299
|
+
const installState = createInstallStateStore(database);
|
|
300
|
+
const installerLock = createInstallerLock(advisoryLock);
|
|
301
|
+
const storage = createStorageConfigStore(database);
|
|
302
|
+
const blobIndex = createBlobIndexStore(database);
|
|
303
|
+
|
|
304
|
+
// One-time, idempotent, parity-verified migration of a legacy
|
|
305
|
+
// inline-ciphertext registry token onto the secrets platform's
|
|
306
|
+
// internal secrets. No-op once migrated (column holds the marker)
|
|
307
|
+
// or when no token is set. Never drops the legacy value until the
|
|
308
|
+
// platform copy reads back identically.
|
|
309
|
+
const registryToken = stash.registryToken;
|
|
310
|
+
if (registryToken) {
|
|
311
|
+
try {
|
|
312
|
+
const registry = createRegistryConfigStore(database);
|
|
313
|
+
const currentRef = await registry.authSecretRef();
|
|
314
|
+
const reg = await registry.get();
|
|
315
|
+
const outcome = await migrateRegistryTokenToPlatform({
|
|
316
|
+
currentRef,
|
|
317
|
+
tokenStore: registryToken,
|
|
318
|
+
rewrite: async (marker) => {
|
|
319
|
+
await registry.set({
|
|
320
|
+
registryUrl: reg.registryUrl,
|
|
321
|
+
scopedRegistries: reg.scopedRegistries,
|
|
322
|
+
ignoreScripts: reg.ignoreScripts,
|
|
323
|
+
authSecretRef: marker,
|
|
324
|
+
});
|
|
325
|
+
},
|
|
326
|
+
});
|
|
327
|
+
if (outcome === "migrated") {
|
|
328
|
+
logger.debug(
|
|
329
|
+
"๐ Migrated script-package registry token onto the secrets platform.",
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
} catch (error) {
|
|
333
|
+
logger.error(
|
|
334
|
+
`Registry-token migration failed: ${extractErrorMessage(error)}`,
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Startup backstop: resume a storage migration that crashed
|
|
340
|
+
// mid-flight. A migration that died leaves `migrationStatus` stuck
|
|
341
|
+
// at "migrating" โ which blocks `installNow` AND makes
|
|
342
|
+
// `triggerMigration` refuse to restart it โ so nothing would ever
|
|
343
|
+
// unwedge it without operator intervention. `runStorageMigration`
|
|
344
|
+
// is idempotent + resumable (it re-derives its work set from the
|
|
345
|
+
// index, skipping blobs already on the target), so we relaunch it
|
|
346
|
+
// toward the recorded target under the installer-election lock so
|
|
347
|
+
// exactly one pod resumes (and an in-flight install on another pod
|
|
348
|
+
// is mutually excluded). Fire-and-forget; progress is polled.
|
|
349
|
+
try {
|
|
350
|
+
await resumeCrashedMigration({
|
|
351
|
+
loadState: async () => {
|
|
352
|
+
const cfg = await storage.get();
|
|
353
|
+
return {
|
|
354
|
+
migrationStatus: cfg.migrationStatus,
|
|
355
|
+
migrationTarget: cfg.migrationTarget,
|
|
356
|
+
activeBackend: cfg.activeBackend,
|
|
357
|
+
};
|
|
358
|
+
},
|
|
359
|
+
tryLock: () => installerLock.tryInstallerLock(),
|
|
360
|
+
runMigration: ({ target, activeBackend }) =>
|
|
361
|
+
runStorageMigration({
|
|
362
|
+
blobIndex,
|
|
363
|
+
storage,
|
|
364
|
+
getStore: (id) => blobStores.get(id),
|
|
365
|
+
activeBackend,
|
|
366
|
+
target,
|
|
367
|
+
logger,
|
|
368
|
+
}),
|
|
369
|
+
logger,
|
|
370
|
+
});
|
|
371
|
+
} catch (error) {
|
|
372
|
+
logger.error(
|
|
373
|
+
`Storage-migration resume check failed: ${extractErrorMessage(error)}`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Let the installer (in init's triggerInstall) emit the hook.
|
|
378
|
+
stash.emitChanged = async (lockfileHash: string) => {
|
|
379
|
+
await emitHook(scriptPackagesChangedHook, { lockfileHash });
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
// Reconcile this instance to a desired hash using the shared blob
|
|
383
|
+
// store (delta pull from whichever backend holds each blob).
|
|
384
|
+
const reconcileLocal = async (input: {
|
|
385
|
+
lockfileHash: string;
|
|
386
|
+
manifest: Awaited<ReturnType<typeof installState.load>>["manifest"];
|
|
387
|
+
}) => {
|
|
388
|
+
const deps = createReconcileFsDeps({
|
|
389
|
+
storeRoot,
|
|
390
|
+
logger,
|
|
391
|
+
fetchBlob: async ({ integrity }) => {
|
|
392
|
+
const cfg = await storage.get();
|
|
393
|
+
const active = cfg.activeBackend;
|
|
394
|
+
const res = await blobStores.readWithFallback({
|
|
395
|
+
integrity,
|
|
396
|
+
activeBackendId: active,
|
|
397
|
+
});
|
|
398
|
+
if (!res) {
|
|
399
|
+
throw new Error(
|
|
400
|
+
`Blob ${integrity} not available in any backend.`,
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
return res.bytes;
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
await reconcileToHash({
|
|
407
|
+
lockfileHash: input.lockfileHash,
|
|
408
|
+
manifest: input.manifest,
|
|
409
|
+
deps,
|
|
410
|
+
});
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// Broadcast subscription: EVERY core instance reconciles on change
|
|
414
|
+
// (the deliberate inverse of installer-election). Best-effort
|
|
415
|
+
// liveness; the startup backstop below guarantees convergence.
|
|
416
|
+
onHook(
|
|
417
|
+
scriptPackagesChangedHook,
|
|
418
|
+
async ({ lockfileHash }) => {
|
|
419
|
+
const state = await installState.load();
|
|
420
|
+
if (state.lockfileHash === lockfileHash) {
|
|
421
|
+
await reconcileLocal({ lockfileHash, manifest: state.manifest });
|
|
422
|
+
}
|
|
423
|
+
},
|
|
424
|
+
{ mode: "broadcast" },
|
|
425
|
+
);
|
|
426
|
+
|
|
427
|
+
// Startup backstop: converge to the durable desired hash regardless
|
|
428
|
+
// of whether this pod ever saw the broadcast. Idempotent (no-op when
|
|
429
|
+
// already at the hash).
|
|
430
|
+
try {
|
|
431
|
+
const state = await installState.load();
|
|
432
|
+
if (state.lockfileHash && state.status === "ready") {
|
|
433
|
+
await reconcileLocal({
|
|
434
|
+
lockfileHash: state.lockfileHash,
|
|
435
|
+
manifest: state.manifest,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
} catch (error) {
|
|
439
|
+
logger.error(
|
|
440
|
+
`Startup script-package reconcile failed: ${extractErrorMessage(error)}`,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Scheduled recurring blob GC: prune unreferenced, past-grace blobs
|
|
445
|
+
// from the active/recorded backends so Postgres/S3 storage is
|
|
446
|
+
// reclaimed. The trigger (built in `init`) holds the installer
|
|
447
|
+
// advisory lock for the pass, so exactly one pod GCs at a time and it
|
|
448
|
+
// is mutually exclusive with installs / migrations. Runs daily; the
|
|
449
|
+
// grace window (default 24h) makes a once-a-day cadence safe.
|
|
450
|
+
const triggerBlobGc = stash.triggerBlobGc;
|
|
451
|
+
if (triggerBlobGc) {
|
|
452
|
+
try {
|
|
453
|
+
const gcQueue = queueManager.getQueue<Record<string, never>>(
|
|
454
|
+
"script-packages-blob-gc",
|
|
455
|
+
);
|
|
456
|
+
await gcQueue.consume(
|
|
457
|
+
async () => {
|
|
458
|
+
const summary = await triggerBlobGc();
|
|
459
|
+
if (summary.ran) {
|
|
460
|
+
logger.debug(
|
|
461
|
+
`Scheduled blob GC: ${summary.deleted} deleted (${summary.bytesReclaimed} bytes), ${summary.keptWithinGrace} kept within grace.`,
|
|
462
|
+
);
|
|
463
|
+
} else {
|
|
464
|
+
logger.debug(
|
|
465
|
+
`Scheduled blob GC skipped: ${summary.reason ?? "unknown"}.`,
|
|
466
|
+
);
|
|
467
|
+
}
|
|
468
|
+
},
|
|
469
|
+
{ consumerGroup: "script-packages-blob-gc-worker", maxRetries: 0 },
|
|
470
|
+
);
|
|
471
|
+
await gcQueue.scheduleRecurring(
|
|
472
|
+
{},
|
|
473
|
+
{
|
|
474
|
+
jobId: "script-packages-blob-gc-daily",
|
|
475
|
+
intervalSeconds: 24 * 60 * 60,
|
|
476
|
+
},
|
|
477
|
+
);
|
|
478
|
+
logger.debug("๐งน Script-packages blob GC scheduled (daily).");
|
|
479
|
+
} catch (error) {
|
|
480
|
+
logger.error(
|
|
481
|
+
`Failed to schedule blob GC: ${extractErrorMessage(error)}`,
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
logger.debug("โ
Script Packages Backend afterPluginsReady complete.");
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
},
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
// โโโ Public surface โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
493
|
+
|
|
494
|
+
export {
|
|
495
|
+
blobStoreExtensionPoint,
|
|
496
|
+
type BlobStore,
|
|
497
|
+
type BlobStoreExtensionPoint,
|
|
498
|
+
} from "./blob-store";
|
|
499
|
+
export {
|
|
500
|
+
createBlobStoreRegistry,
|
|
501
|
+
type BlobStoreRegistry,
|
|
502
|
+
} from "./blob-store-registry";
|
|
503
|
+
export {
|
|
504
|
+
resolveDataDir,
|
|
505
|
+
resolveScriptPackagesDir,
|
|
506
|
+
storePaths,
|
|
507
|
+
} from "./data-dir";
|
|
508
|
+
export {
|
|
509
|
+
buildDependencies,
|
|
510
|
+
buildStorePackageJson,
|
|
511
|
+
computeLockfileHash,
|
|
512
|
+
sortManifest,
|
|
513
|
+
} from "./lockfile";
|
|
514
|
+
export { renderNpmrc, type NpmrcInput } from "./npmrc";
|
|
515
|
+
export {
|
|
516
|
+
resolveRegistryRequestConfig,
|
|
517
|
+
type RegistryRequestConfig,
|
|
518
|
+
} from "./registry-request-config";
|
|
519
|
+
export {
|
|
520
|
+
searchPackages,
|
|
521
|
+
getPackageVersions,
|
|
522
|
+
registryUrlForName,
|
|
523
|
+
RegistryClientError,
|
|
524
|
+
type PackageSearchResult,
|
|
525
|
+
} from "./registry-client";
|
|
526
|
+
export { parseBunLock, splitSpec } from "./parse-bun-lock";
|
|
527
|
+
export {
|
|
528
|
+
createInstallStateStore,
|
|
529
|
+
createInstallerLock,
|
|
530
|
+
type InstallStateStore,
|
|
531
|
+
type InstallerLock,
|
|
532
|
+
} from "./install-state-store";
|
|
533
|
+
export {
|
|
534
|
+
performInstall,
|
|
535
|
+
type BlobIndex,
|
|
536
|
+
type BlobPublisher,
|
|
537
|
+
type InstallResult,
|
|
538
|
+
type Resolver,
|
|
539
|
+
type ResolvedPackage,
|
|
540
|
+
} from "./install-service";
|
|
541
|
+
export { evaluateSizeCap, type SizeCapVerdict } from "./size-cap";
|
|
542
|
+
export { packDir, unpackInto } from "./cache-archive";
|
|
543
|
+
export { blobSha256, verifyBlobSha256 } from "./blob-hash";
|
|
544
|
+
export { computeMissingBlobs } from "./reconcile-diff";
|
|
545
|
+
export { atomicSymlinkSwap, readCurrentTarget } from "./atomic-symlink";
|
|
546
|
+
export {
|
|
547
|
+
reconcileToHash,
|
|
548
|
+
type ReconcileDeps,
|
|
549
|
+
type ReconcileResult,
|
|
550
|
+
} from "./reconciler";
|
|
551
|
+
export { scriptPackagesChangedHook } from "./hooks";
|
|
552
|
+
export {
|
|
553
|
+
createCentralResolver,
|
|
554
|
+
type CentralResolverOptions,
|
|
555
|
+
} from "./resolver";
|
|
556
|
+
export { createReconcileFsDeps } from "./reconcile-fs";
|
|
557
|
+
export { findCacheEntry, type CacheEntryLocation } from "./cache-layout";
|
|
558
|
+
export {
|
|
559
|
+
resolvePackageTypeClosure,
|
|
560
|
+
typesPackageDirName,
|
|
561
|
+
extractReferences,
|
|
562
|
+
} from "./package-types";
|
|
563
|
+
export { createTypeClosureHttpHandler } from "./type-acquisition-route";
|
|
564
|
+
export {
|
|
565
|
+
resolveResolutionRoot,
|
|
566
|
+
resolveResolutionRootForHost,
|
|
567
|
+
resolveResolutionRootFromStore,
|
|
568
|
+
type ResolutionRootStatus,
|
|
569
|
+
} from "./resolution-root";
|
|
570
|
+
export {
|
|
571
|
+
runInstallNow,
|
|
572
|
+
type InstallControllerDeps,
|
|
573
|
+
type InstallOutcome,
|
|
574
|
+
} from "./install-controller";
|
|
575
|
+
export {
|
|
576
|
+
runStorageMigration,
|
|
577
|
+
resumeCrashedMigration,
|
|
578
|
+
type StorageMigrationDeps,
|
|
579
|
+
type StorageMigrationResult,
|
|
580
|
+
type ResumeCrashedMigrationDeps,
|
|
581
|
+
type ResumeCrashedMigrationResult,
|
|
582
|
+
type MigrationStateSnapshot,
|
|
583
|
+
} from "./storage-migration";
|
|
584
|
+
export { runBlobGc, type BlobGcDeps, type GcBlob } from "./blob-gc";
|
|
585
|
+
export {
|
|
586
|
+
createBlobGcTrigger,
|
|
587
|
+
type BlobGcRunnerDeps,
|
|
588
|
+
} from "./blob-gc-runner";
|
|
589
|
+
export { sweepTreeGc, type TreeGcResult } from "./tree-gc";
|
|
590
|
+
export {
|
|
591
|
+
createLockfileHistoryStore,
|
|
592
|
+
createBlobGcStateStore,
|
|
593
|
+
} from "./stores";
|
|
594
|
+
export * as schema from "./schema";
|