UNPKG

zephyr-agent

Version:
407 lines (406 loc) • 18.5 kB
import "node:module"; import { createHash, randomUUID } from "node:crypto"; import { readFile, rm } from "node:fs/promises"; import { zeBuildAssets } from "../transformers/ze-build-assets.mjs"; import { writeFileAtomically } from "./atomic-file.mjs"; import { PARTIAL_ASSET_LOCK_STALE_MS, StorageKeys, ZE_PATH, ZE_STORAGE_PATH } from "./storage-keys.mjs"; import { getStorageLockPath, withStorageLock } from "./storage-lock.mjs"; import * as __rspack_external_node_path_806ed179 from "node:path"; const BUFFER_ENCODING_KEY = '__zephyr_buffer_base64__'; const STORE_VERSION_KEY = '__zephyr_partial_store_version__'; const STORE_VERSION = 2; const CLAIM_STALE_MS = 1800000; const GROUP_STALE_MS = 86400000; const LEGACY_SCOPE = { invocationId: '__zephyr_legacy_unscoped__', generation: 0 }; function isRecord(value) { return 'object' == typeof value && null !== value && !Array.isArray(value); } function decodeBuffer(value) { if (Buffer.isBuffer(value) || 'string' == typeof value) return value; if (isRecord(value) && 'string' == typeof value[BUFFER_ENCODING_KEY]) return Buffer.from(value[BUFFER_ENCODING_KEY], 'base64'); if (isRecord(value) && 'Buffer' === value['type'] && Array.isArray(value['data']) && value['data'].every((byte)=>Number.isInteger(byte) && byte >= 0 && byte <= 255)) return Buffer.from(value['data']); throw new TypeError('Persisted partial asset contains an invalid buffer'); } function serializePartialAssetMaps(partialAssetMaps) { return Object.fromEntries(Object.entries(partialAssetMaps).map(([partialKey, assetsMap])=>[ partialKey, Object.fromEntries(Object.entries(assetsMap).map(([hash, asset])=>[ hash, { ...asset, buffer: Buffer.isBuffer(asset.buffer) ? { [BUFFER_ENCODING_KEY]: asset.buffer.toString('base64') } : asset.buffer } ])) ])); } function deserializePartialAssetMaps(value) { if (null == value) return; if (!isRecord(value)) throw new TypeError('Persisted partial asset maps must be an object'); return Object.fromEntries(Object.entries(value).map(([partialKey, rawAssetsMap])=>{ if (!isRecord(rawAssetsMap)) throw new TypeError(`Persisted partial "${partialKey}" must be an object`); return [ partialKey, Object.fromEntries(Object.entries(rawAssetsMap).map(([hash, rawAsset])=>{ if (!isRecord(rawAsset)) throw new TypeError(`Persisted partial asset "${partialKey}:${hash}" must be an object`); const assetPath = rawAsset['path']; if ('string' != typeof assetPath) throw new TypeError(`Persisted partial asset "${partialKey}:${hash}" has no valid path`); const slashPath = assetPath.replace(/\\/g, '/'); const pathSegments = slashPath.split('/').filter((segment)=>segment && '.' !== segment); if (!slashPath || slashPath.includes('\0') || slashPath.startsWith('/') || /^[A-Za-z]:/.test(slashPath) || 0 === pathSegments.length || pathSegments.includes('..')) throw new TypeError(`Persisted partial asset "${partialKey}:${hash}" escapes the snapshot root`); const buffer = decodeBuffer(rawAsset['buffer']); const rebuilt = zeBuildAssets({ filepath: assetPath, content: buffer }); if (rawAsset['hash'] !== hash || rebuilt.hash !== hash || rawAsset['size'] !== rebuilt.size) throw new TypeError(`Persisted partial asset "${partialKey}:${hash}" failed its content integrity check`); const normalizedPath = pathSegments.join('/'); const normalizedAsset = normalizedPath === assetPath ? rebuilt : zeBuildAssets({ filepath: normalizedPath, content: buffer }); return [ normalizedAsset.hash, { ...rawAsset, path: normalizedAsset.path, hash: normalizedAsset.hash, size: normalizedAsset.size, extname: normalizedAsset.extname, buffer } ]; })) ]; })); } function assertScope(scope) { if (!scope.invocationId.trim()) throw new TypeError('Partial asset invocationId must not be empty'); if (!Number.isSafeInteger(scope.generation) || scope.generation < 0) throw new TypeError('Partial asset generation must be a non-negative safe integer'); } function getScopeKey(scope) { assertScope(scope); return JSON.stringify([ scope.invocationId, scope.generation ]); } function emptyStore() { return { [STORE_VERSION_KEY]: STORE_VERSION, nextRevision: 1, groups: {}, claims: {} }; } function parseScope(value) { if (!isRecord(value) || 'string' != typeof value['invocationId'] || 'number' != typeof value['generation']) throw new TypeError('Persisted partial group has an invalid scope'); const scope = { invocationId: value['invocationId'], generation: value['generation'] }; assertScope(scope); return scope; } function readStore(value) { if (null == value) return emptyStore(); if (isRecord(value) && value[STORE_VERSION_KEY] === STORE_VERSION) { if (!Number.isSafeInteger(value['nextRevision']) || value['nextRevision'] < 1 || !isRecord(value['groups']) || !isRecord(value['claims'])) throw new TypeError('Persisted partial asset store is malformed'); const store = value; for (const [scopeKey, group] of Object.entries(store.groups)){ if (!isRecord(group) || !isRecord(group.entries) || 'number' != typeof group.updatedAt) throw new TypeError(`Persisted partial group "${scopeKey}" is malformed`); const scope = parseScope(group.scope); if (getScopeKey(scope) !== scopeKey) throw new TypeError(`Persisted partial group "${scopeKey}" has a mismatched scope`); for (const [partialKey, entry] of Object.entries(group.entries)){ if (!isRecord(entry) || !Number.isSafeInteger(entry.revision) || entry.revision < 1 || !isRecord(entry.assets)) throw new TypeError(`Persisted partial contribution "${scopeKey}:${partialKey}" is malformed`); deserializePartialAssetMaps({ [partialKey]: entry.assets }); } } for (const [claimId, claim] of Object.entries(store.claims))if (!isRecord(claim) || 'string' != typeof claim.scopeKey || !isRecord(claim.revisions) || 'number' != typeof claim.createdAt || !Object.values(claim.revisions).every((revision)=>Number.isSafeInteger(revision) && revision >= 1)) throw new TypeError(`Persisted partial claim "${claimId}" is malformed`); return store; } const legacyMaps = deserializePartialAssetMaps(value); const store = emptyStore(); if (legacyMaps && Object.keys(legacyMaps).length > 0) { const scopeKey = getScopeKey(LEGACY_SCOPE); store.groups[scopeKey] = { scope: { ...LEGACY_SCOPE }, entries: {}, updatedAt: Date.now() }; for (const [partialKey, assetsMap] of Object.entries(legacyMaps))store.groups[scopeKey].entries[partialKey] = { revision: store.nextRevision++, assets: serializePartialAssetMaps({ [partialKey]: assetsMap })[partialKey] }; } return store; } function readVersionedStore(value) { if (!isRecord(value) || value[STORE_VERSION_KEY] !== STORE_VERSION) throw new TypeError('Persisted partial asset store has an unsupported schema'); return readStore(value); } function pruneStaleStore(store, now = Date.now()) { for (const [claimId, claim] of Object.entries(store.claims))if (now - claim.createdAt >= CLAIM_STALE_MS) delete store.claims[claimId]; const claimedScopes = new Set(Object.values(store.claims).map((claim)=>claim.scopeKey)); for (const [scopeKey, group] of Object.entries(store.groups))if (!claimedScopes.has(scopeKey) && now - group.updatedAt >= GROUP_STALE_MS) delete store.groups[scopeKey]; } function deserializeGroup(group) { return Object.fromEntries(Object.entries(group.entries).map(([partialKey, entry])=>{ const assetsMap = deserializePartialAssetMaps({ [partialKey]: entry.assets }); return [ partialKey, assetsMap?.[partialKey] ?? {} ]; })); } function get_key(application_uid) { return [ StorageKeys.ze_app_partial_asset_map, application_uid ].join('.'); } function get_application_digest(application_uid) { return createHash('sha256').update(application_uid).digest('hex'); } function getPartialAssetStorePath(application_uid) { return __rspack_external_node_path_806ed179.join(ZE_PATH, 'partial-assets', `${get_application_digest(application_uid)}.json`); } function getLegacyPartialAssetStorePath(application_uid) { const legacyKeyDigest = createHash('sha256').update(get_key(application_uid)).digest('hex'); return __rspack_external_node_path_806ed179.join(ZE_STORAGE_PATH, legacyKeyDigest); } function getPartialAssetLockName(application_uid) { return `partial-assets-${get_application_digest(application_uid)}`; } function getPartialAssetLockPath(application_uid) { return getStorageLockPath(getPartialAssetLockName(application_uid)); } function withPartialAssetsLock(application_uid, action) { return withStorageLock(getPartialAssetLockName(application_uid), action, { retries: { retries: 8, factor: 1.5, minTimeout: 25, maxTimeout: 500 }, staleMs: PARTIAL_ASSET_LOCK_STALE_MS, whenUnavailable: 'throw' }); } async function readJsonFileStrict(filePath) { let content; try { content = await readFile(filePath, 'utf8'); } catch (error) { if (error?.code === 'ENOENT') return; throw error; } try { return JSON.parse(content); } catch (error) { throw Object.assign(new TypeError(`Partial asset store "${filePath}" contains invalid JSON`), { cause: error }); } } async function readLegacyStoreValue(application_uid) { const legacyPath = getLegacyPartialAssetStorePath(application_uid); const datum = await readJsonFileStrict(legacyPath); if (void 0 === datum) return { found: false }; if (!isRecord(datum) || datum['key'] !== get_key(application_uid)) throw new TypeError(`Legacy partial asset store "${legacyPath}" is malformed`); if ('number' == typeof datum['ttl'] && Number.isFinite(datum['ttl']) && datum['ttl'] < Date.now()) { await rm(legacyPath, { force: true }); return { found: false }; } if (!Object.prototype.hasOwnProperty.call(datum, 'value')) throw new TypeError(`Legacy partial asset store "${legacyPath}" has no value`); return { found: true, value: datum['value'] }; } async function writeStore(application_uid, store) { await writeFileAtomically(getPartialAssetStorePath(application_uid), JSON.stringify(store)); } async function readApplicationStore(application_uid) { const storePath = getPartialAssetStorePath(application_uid); const persisted = await readJsonFileStrict(storePath); if (void 0 !== persisted) return readVersionedStore(persisted); const legacy = await readLegacyStoreValue(application_uid); if (!legacy.found) return emptyStore(); const migrated = readStore(legacy.value); await writeStore(application_uid, migrated); await rm(getLegacyPartialAssetStorePath(application_uid), { force: true }); return migrated; } async function savePartialAssetMap(application_uid, partial_key, assetMap, scope = LEGACY_SCOPE) { assertScope(scope); await withPartialAssetsLock(application_uid, async ()=>{ const store = await readApplicationStore(application_uid); pruneStaleStore(store); const scopeKey = getScopeKey(scope); const group = store.groups[scopeKey] ??= { scope: { ...scope }, entries: {}, updatedAt: Date.now() }; group.updatedAt = Date.now(); group.entries[partial_key] = { revision: store.nextRevision++, assets: serializePartialAssetMaps({ [partial_key]: assetMap })[partial_key] }; await writeStore(application_uid, store); }); } async function getPartialAssetMap(application_uid, scope = LEGACY_SCOPE) { assertScope(scope); return withPartialAssetsLock(application_uid, async ()=>{ const store = await readApplicationStore(application_uid); const group = store.groups[getScopeKey(scope)]; return group ? deserializeGroup(group) : void 0; }); } async function claimPartialAssetMapBatch(application_uid, scopes) { if (0 === scopes.length) throw new TypeError('At least one partial asset scope must be claimed'); const scopeKeys = scopes.map((scope)=>{ assertScope(scope); return getScopeKey(scope); }); if (new Set(scopeKeys).size !== scopeKeys.length) throw new TypeError('Partial asset scopes must be unique within a claim batch'); return withPartialAssetsLock(application_uid, async ()=>{ const store = await readApplicationStore(application_uid); pruneStaleStore(store); const candidates = scopes.map((scope, index)=>{ const scopeKey = scopeKeys[index]; const group = store.groups[scopeKey]; if (!group || 0 === Object.keys(group.entries).length) return; const revisions = Object.fromEntries(Object.entries(group.entries).map(([partialKey, entry])=>[ partialKey, entry.revision ])); const isAlreadyClaimed = Object.values(store.claims).some((claim)=>claim.scopeKey === scopeKey && Object.entries(revisions).some(([partialKey, revision])=>claim.revisions[partialKey] === revision)); return isAlreadyClaimed ? void 0 : { scope, scopeKey, group, revisions }; }); if (candidates.some((candidate)=>!candidate)) return; const now = Date.now(); const claims = candidates.map((candidate)=>{ if (!candidate) throw new TypeError('Partial claim candidate was unavailable'); const claimId = randomUUID(); store.claims[claimId] = { scopeKey: candidate.scopeKey, revisions: candidate.revisions, createdAt: now }; return { claimId, scope: { ...candidate.scope }, partialAssetMaps: deserializeGroup(candidate.group) }; }); await writeStore(application_uid, store); return { claims: Object.freeze(claims) }; }); } async function claimPartialAssetMap(application_uid, scope) { const batch = await claimPartialAssetMapBatch(application_uid, [ scope ]); return batch?.claims[0]; } async function commitPartialAssetMapClaimBatch(application_uid, claimIds) { const uniqueClaimIds = [ ...new Set(claimIds) ]; if (0 === uniqueClaimIds.length) return; await withPartialAssetsLock(application_uid, async ()=>{ const store = await readApplicationStore(application_uid); let changed = false; for (const claimId of uniqueClaimIds){ const claim = store.claims[claimId]; if (!claim) continue; const group = store.groups[claim.scopeKey]; if (group) { for (const [partialKey, revision] of Object.entries(claim.revisions))if (group.entries[partialKey]?.revision === revision) delete group.entries[partialKey]; if (0 === Object.keys(group.entries).length) delete store.groups[claim.scopeKey]; } delete store.claims[claimId]; changed = true; } if (changed) await writeStore(application_uid, store); }); } async function commitPartialAssetMapClaim(application_uid, claimId) { await commitPartialAssetMapClaimBatch(application_uid, [ claimId ]); } async function rollbackPartialAssetMapClaimBatch(application_uid, claimIds) { const uniqueClaimIds = [ ...new Set(claimIds) ]; if (0 === uniqueClaimIds.length) return; await withPartialAssetsLock(application_uid, async ()=>{ const store = await readApplicationStore(application_uid); let changed = false; for (const claimId of uniqueClaimIds)if (store.claims[claimId]) { delete store.claims[claimId]; changed = true; } if (changed) await writeStore(application_uid, store); }); } async function rollbackPartialAssetMapClaim(application_uid, claimId) { await rollbackPartialAssetMapClaimBatch(application_uid, [ claimId ]); } async function takePartialAssetMap(application_uid) { const claim = await claimPartialAssetMap(application_uid, LEGACY_SCOPE); if (!claim) return; await commitPartialAssetMapClaim(application_uid, claim.claimId); return claim.partialAssetMaps; } async function removePartialAssetMap(application_uid) { await withPartialAssetsLock(application_uid, async ()=>{ await rm(getLegacyPartialAssetStorePath(application_uid), { force: true }); await rm(getPartialAssetStorePath(application_uid), { force: true }); }); } export { claimPartialAssetMap, claimPartialAssetMapBatch, commitPartialAssetMapClaim, commitPartialAssetMapClaimBatch, deserializePartialAssetMaps, getLegacyPartialAssetStorePath, getPartialAssetLockPath, getPartialAssetMap, getPartialAssetStorePath, removePartialAssetMap, rollbackPartialAssetMapClaim, rollbackPartialAssetMapClaimBatch, savePartialAssetMap, serializePartialAssetMaps, takePartialAssetMap }; //# sourceMappingURL=partial-assets-map.mjs.map