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>
182 lines
5.7 KiB
TypeScript
182 lines
5.7 KiB
TypeScript
import type {
|
|
BrowserRpcKind,
|
|
BrowserRpcOperationV3,
|
|
BrowserRpcProtocol,
|
|
BrowserRpcProviderProfile,
|
|
BrowserRpcRuntimeBindingIdentity,
|
|
BrowserRpcTransportFailureCode,
|
|
} from "../../contracts/browser-rpc.ts";
|
|
|
|
export type BrowserRpcTransportFailure = Readonly<{
|
|
code: BrowserRpcTransportFailureCode;
|
|
retryAfterMs?: number;
|
|
}>;
|
|
|
|
export type BrowserRpcTransportCall = Readonly<{
|
|
operation: BrowserRpcOperationV3;
|
|
profile: BrowserRpcProviderProfile;
|
|
request: unknown;
|
|
encodedRequestBytes: number;
|
|
attempt: number;
|
|
timeoutMs: number;
|
|
signal: AbortSignal;
|
|
idempotencyKey?: string;
|
|
}>;
|
|
|
|
export type BrowserRpcUnaryTransportResult =
|
|
| Readonly<{
|
|
ok: true;
|
|
message: unknown;
|
|
encodedBytes: number;
|
|
}>
|
|
| Readonly<{
|
|
ok: false;
|
|
failure: BrowserRpcTransportFailure;
|
|
}>;
|
|
|
|
export type BrowserRpcStreamFrame =
|
|
| Readonly<{
|
|
kind: "MESSAGE";
|
|
message: unknown;
|
|
encodedBytes: number;
|
|
}>
|
|
| Readonly<{
|
|
kind: "TERMINAL";
|
|
ok: true;
|
|
}>
|
|
| Readonly<{
|
|
kind: "TERMINAL";
|
|
ok: false;
|
|
failure: BrowserRpcTransportFailure;
|
|
}>;
|
|
|
|
/**
|
|
* RPC-RR-01. A server stream is a physical resource, not just a sequence.
|
|
*
|
|
* A bare `AsyncIterable` gives the runtime no way to cancel the underlying
|
|
* stream or to learn when it actually closed: `iterator.return()` is a request
|
|
* a non-cooperative implementation may ignore. The runtime could then time out,
|
|
* report the call finished, and admit a second stream for the same operation
|
|
* while the first was still running against the server.
|
|
*
|
|
* The lease separates the three concerns the runtime needs:
|
|
*
|
|
* - `frames` is the sequence,
|
|
* - `cancel(reason)` is a synchronous request to stop the physical stream,
|
|
* - `waitClosed()` settles only once that stream is really closed,
|
|
* - `streamId` names the physical stream so two leases are never confused.
|
|
*/
|
|
export type BrowserRpcServerStreamLease = Readonly<{
|
|
streamId: string;
|
|
frames: AsyncIterable<BrowserRpcStreamFrame>;
|
|
cancel(reason: string): void;
|
|
waitClosed(): Promise<void>;
|
|
}>;
|
|
|
|
export type BrowserRpcTransport = BrowserRpcRuntimeBindingIdentity &
|
|
Readonly<{
|
|
invokeUnary?(
|
|
call: BrowserRpcTransportCall,
|
|
): Promise<BrowserRpcUnaryTransportResult>;
|
|
openServerStream?(
|
|
call: BrowserRpcTransportCall,
|
|
): BrowserRpcServerStreamLease;
|
|
}>;
|
|
|
|
const STREAM_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u;
|
|
|
|
/**
|
|
* RPC-RR-01. Decodes a lease from own data descriptors before the runtime
|
|
* registers it, so an accessor cannot hand the registry one object and the
|
|
* cancellation path another.
|
|
*/
|
|
export function decodeServerStreamLease(
|
|
value: unknown,
|
|
): BrowserRpcServerStreamLease | null {
|
|
if (value === null || typeof value !== "object") return null;
|
|
let streamId: unknown;
|
|
let frames: unknown;
|
|
let cancel: unknown;
|
|
let waitClosed: unknown;
|
|
try {
|
|
if (Object.getOwnPropertySymbols(value).length > 0) return null;
|
|
const names = Object.getOwnPropertyNames(value).sort();
|
|
const expected = ["cancel", "frames", "streamId", "waitClosed"];
|
|
if (
|
|
names.length !== expected.length ||
|
|
names.some((name, index) => name !== expected[index])
|
|
) {
|
|
return null;
|
|
}
|
|
for (const name of names) {
|
|
const descriptor = Object.getOwnPropertyDescriptor(value, name);
|
|
if (!descriptor || !("value" in descriptor)) return null;
|
|
}
|
|
streamId = Object.getOwnPropertyDescriptor(value, "streamId")?.value;
|
|
frames = Object.getOwnPropertyDescriptor(value, "frames")?.value;
|
|
cancel = Object.getOwnPropertyDescriptor(value, "cancel")?.value;
|
|
waitClosed = Object.getOwnPropertyDescriptor(value, "waitClosed")?.value;
|
|
} catch {
|
|
return null;
|
|
}
|
|
// RPC-02. The async-iterator lookup is a read of foreign state like any
|
|
// other, so it happens inside the decoder's own boundary. Performing it after
|
|
// the `try` let a throwing `Symbol.asyncIterator` getter escape this
|
|
// function as a native `TypeError`, breaking the decoder's totality.
|
|
let openFrames: unknown;
|
|
try {
|
|
if (
|
|
typeof streamId !== "string" ||
|
|
!STREAM_ID.test(streamId) ||
|
|
frames === null ||
|
|
typeof frames !== "object" ||
|
|
typeof cancel !== "function" ||
|
|
typeof waitClosed !== "function"
|
|
) {
|
|
return null;
|
|
}
|
|
openFrames = (frames as AsyncIterable<unknown>)[Symbol.asyncIterator];
|
|
if (typeof openFrames !== "function") return null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
const iterate = (openFrames as () => AsyncIterator<BrowserRpcStreamFrame>)
|
|
.bind(frames);
|
|
return Object.freeze({
|
|
streamId,
|
|
frames: Object.freeze({
|
|
[Symbol.asyncIterator]: iterate,
|
|
}) as AsyncIterable<BrowserRpcStreamFrame>,
|
|
cancel: (cancel as (reason: string) => void).bind(value),
|
|
waitClosed: (waitClosed as () => Promise<void>).bind(value),
|
|
});
|
|
}
|
|
|
|
export function defineBrowserRpcTransport(
|
|
transport: BrowserRpcTransport,
|
|
): BrowserRpcTransport {
|
|
if (
|
|
!transport.runtimeProfileId ||
|
|
!transport.providerId ||
|
|
!isProtocol(transport.protocol) ||
|
|
!isRpcKind(transport.rpcKind) ||
|
|
(transport.rpcKind === "UNARY" &&
|
|
(typeof transport.invokeUnary !== "function" ||
|
|
transport.openServerStream !== undefined)) ||
|
|
(transport.rpcKind === "SERVER_STREAM" &&
|
|
(typeof transport.openServerStream !== "function" ||
|
|
transport.invokeUnary !== undefined))
|
|
) {
|
|
throw new TypeError("Browser RPC transport is invalid.");
|
|
}
|
|
return Object.freeze({ ...transport });
|
|
}
|
|
|
|
function isProtocol(value: string): value is BrowserRpcProtocol {
|
|
return value === "CONNECT_HTTP" || value === "GRPC_WEB";
|
|
}
|
|
|
|
function isRpcKind(value: string): value is BrowserRpcKind {
|
|
return value === "UNARY" || value === "SERVER_STREAM";
|
|
}
|