blob: d00c109af26f6814e82eed4fbd5a0c33f2c00900 [file]
import {
assertStorageRootLease,
runWithStorageRootLease,
StorageRootAuthorityError,
type StorageRootLease,
} from './root-authority.js';
import {
invalidMemoryDocument,
isRevision,
memoryBundleIoFailed,
type CommitMemoryBundleInput,
MemoryBundleBackupRevisionConflictError,
MemoryBundleBackupNotFoundError,
type MemoryBackupSnapshot,
type MemoryBundleMutationResult,
MemoryBundleRevisionConflictError,
type MemoryBundleSnapshot,
MemoryBundleStoreError,
type RestoreMemoryBackupInput,
} from './memory-bundle-model.js';
import {
commitMemoryBundle,
readMemoryBackups,
readMemoryBundle,
recoverMemoryBundle,
restoreMemoryBackup,
} from './memory-bundle-io.js';
import { SerializedOperationLane } from './serialized-operation-lane.js';
export {
MEMORY_DOCUMENT_MAX_BYTES,
MemoryBundleBackupRevisionConflictError,
MemoryBundleBackupNotFoundError,
MemoryBundleRevisionConflictError,
MemoryBundleStoreError,
} from './memory-bundle-model.js';
export type {
CommitMemoryBundleInput,
MemoryBackupKind,
MemoryBackupSnapshot,
MemoryBundleMutationResult,
MemoryBundleSnapshot,
MemoryBundleStoreErrorCode,
MemoryDocumentName,
MemoryRevision,
MemoryDocumentSnapshot,
RestoreMemoryBackupInput,
} from './memory-bundle-model.js';
const readerBrand: unique symbol = Symbol('InteractiveMemoryBundleStoreReader');
const writerBrand: unique symbol = Symbol('InteractiveMemoryBundleStoreWriter');
export interface InteractiveMemoryBundleStoreReader {
readonly kind: 'interactive';
readonly access: 'read';
readonly [readerBrand]: true;
read(): Promise<MemoryBundleSnapshot>;
listBackups(): Promise<readonly MemoryBackupSnapshot[]>;
}
export interface InteractiveMemoryBundleStoreWriter {
readonly kind: 'interactive';
readonly access: 'write';
readonly [writerBrand]: true;
read(): Promise<MemoryBundleSnapshot>;
listBackups(): Promise<readonly MemoryBackupSnapshot[]>;
commit(input: CommitMemoryBundleInput): Promise<MemoryBundleMutationResult>;
restoreBackup(input: RestoreMemoryBackupInput): Promise<MemoryBundleMutationResult>;
}
const readers = new WeakSet<object>();
const writers = new WeakSet<object>();
const writerByLease = new WeakMap<object, InteractiveMemoryBundleStoreWriter>();
const writerOpeningByLease = new WeakMap<object, Promise<InteractiveMemoryBundleStoreWriter>>();
export function authenticateInteractiveMemoryBundleStoreReader(
store: InteractiveMemoryBundleStoreReader,
): InteractiveMemoryBundleStoreReader {
if (!readers.has(store)) throw invalidFacade('read');
return store;
}
export function authenticateInteractiveMemoryBundleStoreWriter(
store: InteractiveMemoryBundleStoreWriter,
): InteractiveMemoryBundleStoreWriter {
if (!writers.has(store)) throw invalidFacade('write');
return store;
}
export async function openInteractiveMemoryBundleStoreForRead(
lease: StorageRootLease<'interactive', 'read'>,
): Promise<InteractiveMemoryBundleStoreReader> {
await assertStorageRootLease(lease, 'interactive', 'read');
const facade = Object.freeze({
kind: 'interactive' as const,
access: 'read' as const,
[readerBrand]: true as const,
read: () =>
runMemoryStoreOperation(() =>
runWithStorageRootLease(lease, 'interactive', 'read', (root) => readMemoryBundle(root)),
),
listBackups: () =>
runMemoryStoreOperation(() =>
runWithStorageRootLease(lease, 'interactive', 'read', (root) => readMemoryBackups(root)),
),
});
readers.add(facade);
return facade;
}
export async function openInteractiveMemoryBundleStoreForWrite(
lease: StorageRootLease<'interactive', 'write'>,
): Promise<InteractiveMemoryBundleStoreWriter> {
await assertStorageRootLease(lease, 'interactive', 'write');
const existing = writerByLease.get(lease);
if (existing) return existing;
const opening = writerOpeningByLease.get(lease);
if (opening) return opening;
const coordinator = new MemoryBundleCoordinator(<T>(operation: (root: string) => Promise<T>) =>
runWithStorageRootLease(lease, 'interactive', 'write', operation),
);
const pending = Promise.resolve().then(async () => {
await coordinator.recoverForWrite();
await assertStorageRootLease(lease, 'interactive', 'write');
const recoveredExisting = writerByLease.get(lease);
if (recoveredExisting) return recoveredExisting;
const facade = Object.freeze({
kind: 'interactive' as const,
access: 'write' as const,
[writerBrand]: true as const,
read: () => coordinator.read(),
listBackups: () => coordinator.listBackups(),
commit: (input: CommitMemoryBundleInput) => coordinator.commit(admitCommitInput(input)),
restoreBackup: (input: RestoreMemoryBackupInput) =>
coordinator.restoreBackup(admitRestoreInput(input)),
});
writers.add(facade);
writerByLease.set(lease, facade);
return facade;
});
writerOpeningByLease.set(lease, pending);
try {
return await pending;
} finally {
if (writerOpeningByLease.get(lease) === pending) {
writerOpeningByLease.delete(lease);
}
}
}
type RootExecutor = <T>(operation: (root: string) => Promise<T>) => Promise<T>;
class MemoryBundleCoordinator {
private readonly lane: SerializedOperationLane<string>;
constructor(execute: RootExecutor) {
this.lane = new SerializedOperationLane(execute);
}
recoverForWrite(): Promise<void> {
return this.inLane((root) => recoverMemoryBundle(root));
}
read(): Promise<MemoryBundleSnapshot> {
return this.inLane((root) => readMemoryBundle(root));
}
listBackups(): Promise<readonly MemoryBackupSnapshot[]> {
return this.inLane((root) => readMemoryBackups(root));
}
commit(input: CommitMemoryBundleInput): Promise<MemoryBundleMutationResult> {
return this.inLane((root) => commitMemoryBundle(root, input));
}
restoreBackup(input: RestoreMemoryBackupInput): Promise<MemoryBundleMutationResult> {
return this.inLane((root) => restoreMemoryBackup(root, input));
}
private inLane<T>(operation: (root: string) => Promise<T>): Promise<T> {
return this.lane.run(operation).catch(rethrowMemoryStoreError);
}
}
function admitCommitInput(input: CommitMemoryBundleInput): CommitMemoryBundleInput {
if (!isRevision(input.expectedRevision)) {
throw invalidMemoryDocument('Expected Memory bundle revision must be SHA-256');
}
return {
expectedRevision: input.expectedRevision,
memory: Uint8Array.from(input.memory),
pending: input.pending === null ? null : Uint8Array.from(input.pending),
...(input.backup === undefined ? {} : { backup: requireCommitBackupKind(input.backup) }),
};
}
function admitRestoreInput(input: RestoreMemoryBackupInput): RestoreMemoryBackupInput {
if (!isRevision(input.expectedRevision)) {
throw invalidMemoryDocument('Expected Memory bundle revision must be SHA-256');
}
if (!isRevision(input.expectedBackupRevision)) {
throw invalidMemoryDocument('Expected Memory backup revision must be SHA-256');
}
if (input.kind !== 'save' && input.kind !== 'reset' && input.kind !== 'restore') {
throw invalidMemoryDocument('Memory backup kind is invalid');
}
return {
expectedRevision: input.expectedRevision,
expectedBackupRevision: input.expectedBackupRevision,
kind: input.kind,
};
}
function requireCommitBackupKind(input: unknown): 'save' | 'reset' {
if (input !== 'save' && input !== 'reset') {
throw invalidMemoryDocument('Memory commit backup kind is invalid');
}
return input;
}
function runMemoryStoreOperation<T>(operation: () => Promise<T>): Promise<T> {
return operation().catch(rethrowMemoryStoreError);
}
function rethrowMemoryStoreError(error: unknown): never {
if (
error instanceof MemoryBundleStoreError ||
error instanceof MemoryBundleRevisionConflictError ||
error instanceof MemoryBundleBackupRevisionConflictError ||
error instanceof MemoryBundleBackupNotFoundError ||
error instanceof StorageRootAuthorityError
) {
throw error;
}
if (isNodeError(error)) {
throw memoryBundleIoFailed('Memory bundle I/O failed', error);
}
throw error;
}
function isNodeError(error: unknown): error is NodeJS.ErrnoException {
return (
typeof error === 'object' &&
error !== null &&
'code' in error &&
typeof (error as NodeJS.ErrnoException).code === 'string'
);
}
function invalidFacade(access: 'read' | 'write'): StorageRootAuthorityError {
return new StorageRootAuthorityError(
'invalid_lease',
`Expected authentic interactive ${access} Memory bundle store`,
);
}