Files
tech-log-frontend/src/adapters/storage/opfs/opfs-policy.ts
T
DongHyeonkaandClaude Opus 5 4bff9ca151 chore: sync the frontend template from 4dc033c to 8157ad4
The product was materialized from the template at `4dc033c` and has stayed
on it through 43 template commits, so it was missing all three rounds of
adapter remediation — including files it never had, such as the shared
`abortable-operation` primitive and the `exact-snapshot` decoder that
later fixes are written against. Taking only the newest round was not
possible for that reason: the delta is coherent only as a whole.

The product had not touched `src/adapters` at all since materialization,
so the 140-file delta applied with a three-way merge and no conflicts.
`package.json` was the single overlap and merged cleanly: the product owns
`name`, the template contributed `check:adapter-inventory`,
`check:remediation-ledger` and the image-resolve-signal type fixture.
All 24 product-owned files — README, index.html, CI workflow, i18n
catalog, home page, generated schemas, evidence scripts, component and
visual snapshots — are byte-identical to `main`.

`template.lock.json` now pins the synced revision and tree.

Verified in this repository, not inherited from the template: six type
projects, lint, nine gates (adapter inventory, remediation ledger,
registries, diagnostics, realtime boundaries, architecture, browser
file/storage boundaries, optional recipes, documentation), the production
build, and 2,054 of 2,073 tests. The 19 failures are all in
`tests/unit/ci-artifact-contract.test.ts` and are the same pre-existing
sandbox RLIMIT, EMFILE, umask and `/tmp` permission behaviour the template
records; four suites that failed once under parallel load pass in
isolation.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 12:04:58 +09:00

241 lines
8.0 KiB
TypeScript

import {
assertValidStoragePolicy,
isValidByteLength,
type BrowserDataFailureCode,
type BrowserDataOperation,
type BrowserStoragePolicy,
} from "../../../application/ports/browser-file-storage/shared.ts";
import type { OpfsStorageScope } from "../../../application/ports/browser-file-storage/opfs-ports.ts";
export type OpfsRuntimePolicy = Readonly<{
rootDirectoryName: string;
mutationLockName: string;
chunkSizeBytes: number;
maxObjectBytes: number;
maxChunkCount: number;
rpcTimeoutMs: number;
reconciliationBudgetMs: number;
reconciliationBatchSize: number;
orphanGracePeriodMs: number;
orphanGcBatchSize: number;
maxCancellationTombstones: number;
allowAsyncWritableChunkFallback: boolean;
isObjectIdAllowed: (objectId: string) => boolean;
isMediaTypeAllowed: (mediaType: string) => boolean;
}>;
export type OpfsSafeObservation = Readonly<{
operation: BrowserDataOperation;
/**
* NS-04. `DEGRADED` is a committed effect whose bookkeeping is unsettled:
* the payload is durable but a reconciler still owns the transaction.
*/
outcome: "STARTED" | "SUCCEEDED" | "FAILED" | "DEGRADED";
failureCode?: BrowserDataFailureCode;
byteBucket?: "0" | "1B_1MiB" | "1MiB_16MiB" | "16MiB_256MiB" | "GT_256MiB";
transactionBucket?: "0" | "1_10" | "11_100" | "GT_100";
}>;
export type OpfsSafeObserver = (observation: OpfsSafeObservation) => void;
const SAFE_SEGMENT = /^[a-z0-9][a-z0-9._-]{0,63}$/u;
const OPAQUE_OBJECT_ID = /^[A-Za-z0-9_-]{8,128}$/u;
const MEDIA_TYPE = /^[a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+(?:\s*;.*)?$/iu;
export const DEFAULT_OPFS_RUNTIME_POLICY: OpfsRuntimePolicy = Object.freeze({
rootDirectoryName: "ca-frontend-opfs-v1",
mutationLockName: "ca-frontend-opfs-v1:mutation",
chunkSizeBytes: 4 * 1024 * 1024,
maxObjectBytes: 2 * 1024 * 1024 * 1024,
maxChunkCount: 512,
rpcTimeoutMs: 60_000,
reconciliationBudgetMs: 5_000,
reconciliationBatchSize: 100,
orphanGracePeriodMs: 24 * 60 * 60 * 1_000,
orphanGcBatchSize: 100,
maxCancellationTombstones: 1_024,
allowAsyncWritableChunkFallback: true,
isObjectIdAllowed: (objectId) => OPAQUE_OBJECT_ID.test(objectId),
isMediaTypeAllowed: (mediaType) => MEDIA_TYPE.test(mediaType),
});
export function resolveOpfsRuntimePolicy(
policy: Partial<OpfsRuntimePolicy> = {},
): OpfsRuntimePolicy {
const resolved: OpfsRuntimePolicy = Object.freeze({
...DEFAULT_OPFS_RUNTIME_POLICY,
...policy,
});
assertOpfsRuntimePolicy(resolved);
return resolved;
}
export function assertOpfsRuntimePolicy(policy: OpfsRuntimePolicy): void {
if (
!SAFE_SEGMENT.test(policy.rootDirectoryName) ||
policy.mutationLockName.length === 0 ||
!Number.isSafeInteger(policy.chunkSizeBytes) ||
policy.chunkSizeBytes < 64 * 1024 ||
policy.chunkSizeBytes > 64 * 1024 * 1024 ||
!isValidByteLength(policy.maxObjectBytes) ||
policy.maxObjectBytes < policy.chunkSizeBytes ||
!Number.isSafeInteger(policy.maxChunkCount) ||
policy.maxChunkCount < 1 ||
policy.maxObjectBytes > policy.chunkSizeBytes * policy.maxChunkCount ||
!Number.isSafeInteger(policy.rpcTimeoutMs) ||
policy.rpcTimeoutMs < 1_000 ||
!Number.isSafeInteger(policy.reconciliationBudgetMs) ||
policy.reconciliationBudgetMs < 1 ||
policy.reconciliationBudgetMs > 60_000 ||
!Number.isSafeInteger(policy.reconciliationBatchSize) ||
policy.reconciliationBatchSize < 1 ||
policy.reconciliationBatchSize > 1_000 ||
!Number.isSafeInteger(policy.orphanGracePeriodMs) ||
policy.orphanGracePeriodMs < 60_000 ||
!Number.isSafeInteger(policy.orphanGcBatchSize) ||
policy.orphanGcBatchSize < 1 ||
policy.orphanGcBatchSize > 1_000 ||
!Number.isSafeInteger(policy.maxCancellationTombstones) ||
policy.maxCancellationTombstones < 16 ||
policy.maxCancellationTombstones > 10_000 ||
typeof policy.isObjectIdAllowed !== "function" ||
typeof policy.isMediaTypeAllowed !== "function"
) {
throw new TypeError("OPFS runtime policy is invalid.");
}
}
export function validateObjectWriteInput(
input: Readonly<{
scope: OpfsStorageScope;
objectId: string;
expectedGeneration: number | null;
mediaType: string;
byteLength: number | null;
storagePolicy: Parameters<typeof assertValidStoragePolicy>[0];
}>,
policy: OpfsRuntimePolicy,
): boolean {
try {
assertValidStoragePolicy(input.storagePolicy);
} catch {
return false;
}
return (
isValidOpfsStorageScope(input.scope) &&
input.scope.namespace === input.storagePolicy.namespace &&
policy.isObjectIdAllowed(input.objectId) &&
policy.isMediaTypeAllowed(input.mediaType) &&
(input.expectedGeneration === null ||
(Number.isSafeInteger(input.expectedGeneration) &&
input.expectedGeneration > 0)) &&
input.byteLength !== null &&
isValidByteLength(input.byteLength) &&
input.byteLength <= policy.maxObjectBytes &&
Math.ceil(input.byteLength / policy.chunkSizeBytes) <=
policy.maxChunkCount
);
}
export function isValidOpfsStorageScope(
scope: OpfsStorageScope,
): boolean {
return (
scope.namespace.length > 0 &&
scope.namespace.length <= 64 &&
OPAQUE_OBJECT_ID.test(scope.authorityToken) &&
OPAQUE_OBJECT_ID.test(scope.namespaceToken) &&
OPAQUE_OBJECT_ID.test(scope.partitionToken)
);
}
/**
* Captures the registry binding at composition time. Callers may own mutable
* config objects, so no OPFS operation is allowed to retain those references.
*/
export function snapshotOpfsStorageScope(
input: OpfsStorageScope,
): OpfsStorageScope {
try {
const snapshot: OpfsStorageScope = Object.freeze({
namespace: input.namespace,
authorityToken: input.authorityToken,
namespaceToken: input.namespaceToken,
partitionToken: input.partitionToken,
});
if (!isValidOpfsStorageScope(snapshot)) throw new TypeError();
return snapshot;
} catch {
throw new TypeError("OPFS storage scope is invalid.");
}
}
/**
* Deep enough for the closed BrowserStoragePolicy contract: retention is the
* only nested value. Fields are copied explicitly so later caller mutation or
* extension properties cannot alter the bound policy fingerprint.
*/
export function snapshotOpfsStoragePolicy(
input: BrowserStoragePolicy,
): BrowserStoragePolicy {
try {
const retention: BrowserStoragePolicy["retention"] =
input.retention.kind === "TTL"
? Object.freeze({
kind: "TTL",
maxAgeMs: input.retention.maxAgeMs,
})
: Object.freeze({ kind: input.retention.kind });
const snapshot: BrowserStoragePolicy = Object.freeze({
owner: input.owner,
namespace: input.namespace,
classification: input.classification,
authority: input.authority,
accountScope: input.accountScope,
retention,
softBudgetBytes: input.softBudgetBytes,
hardBudgetBytes: input.hardBudgetBytes,
evictionPriority: input.evictionPriority,
logoutAction: input.logoutAction,
accountDeletionAction: input.accountDeletionAction,
pressureAction: input.pressureAction,
unavailableFallback: input.unavailableFallback,
});
assertValidStoragePolicy(snapshot);
return snapshot;
} catch {
throw new TypeError("OPFS storage policy is invalid.");
}
}
export function byteBucket(
byteLength: number,
): NonNullable<OpfsSafeObservation["byteBucket"]> {
if (byteLength === 0) return "0";
if (byteLength <= 1024 * 1024) return "1B_1MiB";
if (byteLength <= 16 * 1024 * 1024) return "1MiB_16MiB";
if (byteLength <= 256 * 1024 * 1024) return "16MiB_256MiB";
return "GT_256MiB";
}
export function transactionBucket(
count: number,
): NonNullable<OpfsSafeObservation["transactionBucket"]> {
if (count === 0) return "0";
if (count <= 10) return "1_10";
if (count <= 100) return "11_100";
return "GT_100";
}
export function observeOpfsSafely(
observer: OpfsSafeObserver | undefined,
observation: OpfsSafeObservation,
): void {
try {
observer?.(Object.freeze({ ...observation }));
} catch {
// Persistence behavior never depends on observability.
}
}