169 lines
4.3 KiB
TypeScript
169 lines
4.3 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
isRealtimeResult,
|
|
realtimeFailure,
|
|
realtimeSuccess,
|
|
snapshotRealtimeResult,
|
|
} from "../../../src/adapters/realtime/result.ts";
|
|
|
|
function isString(value: unknown): value is string {
|
|
return typeof value === "string";
|
|
}
|
|
|
|
describe("realtime result boundary", () => {
|
|
it("uses an immutable one-shot snapshot instead of rereading a mutable result", () => {
|
|
const source = { ok: true, value: "captured" };
|
|
const captured = snapshotRealtimeResult(source, (value): value is string => {
|
|
source.value = "changed-during-validation";
|
|
return value === "captured";
|
|
});
|
|
source.value = "changed-after-validation";
|
|
|
|
expect(captured).toEqual(
|
|
realtimeSuccess("captured"),
|
|
);
|
|
expect(Object.isFrozen(captured)).toBe(true);
|
|
expect(isRealtimeResult(source, isString)).toBe(false);
|
|
});
|
|
|
|
it("canonicalizes failure fields before the source can change", () => {
|
|
const source = {
|
|
ok: false,
|
|
error: {
|
|
kind: "FORBIDDEN",
|
|
operation: "RECOVER",
|
|
retryable: false,
|
|
},
|
|
};
|
|
const captured = snapshotRealtimeResult(
|
|
source,
|
|
(_value): _value is never => false,
|
|
);
|
|
source.error.kind = "PROVIDER_UNAVAILABLE";
|
|
source.error.operation = "CONNECT";
|
|
source.error.retryable = true;
|
|
|
|
expect(captured).toEqual(
|
|
realtimeFailure("FORBIDDEN", "RECOVER", false),
|
|
);
|
|
expect(
|
|
captured && !captured.ok
|
|
? Object.isFrozen(captured.error)
|
|
: false,
|
|
).toBe(true);
|
|
});
|
|
|
|
it("rejects accessors without invoking them", () => {
|
|
let outerReads = 0;
|
|
const outerAccessor = Object.defineProperties(
|
|
{},
|
|
{
|
|
ok: {
|
|
enumerable: true,
|
|
get() {
|
|
outerReads += 1;
|
|
return true;
|
|
},
|
|
},
|
|
value: {
|
|
enumerable: true,
|
|
value: "safe",
|
|
},
|
|
},
|
|
);
|
|
let nestedReads = 0;
|
|
const nestedAccessor = Object.defineProperties(
|
|
{},
|
|
{
|
|
kind: {
|
|
enumerable: true,
|
|
get() {
|
|
nestedReads += 1;
|
|
return "FORBIDDEN";
|
|
},
|
|
},
|
|
operation: {
|
|
enumerable: true,
|
|
value: "RECOVER",
|
|
},
|
|
retryable: {
|
|
enumerable: true,
|
|
value: false,
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(
|
|
snapshotRealtimeResult(outerAccessor, isString),
|
|
).toBeNull();
|
|
expect(
|
|
snapshotRealtimeResult(
|
|
{ ok: false, error: nestedAccessor },
|
|
(_value): _value is never => false,
|
|
),
|
|
).toBeNull();
|
|
expect(outerReads).toBe(0);
|
|
expect(nestedReads).toBe(0);
|
|
});
|
|
|
|
it("consumes proxy fields only from one descriptor snapshot", () => {
|
|
const propertyReads: PropertyKey[] = [];
|
|
const descriptorReads = new Map<PropertyKey, number>();
|
|
const source = new Proxy(
|
|
{ ok: true, value: "descriptor-value" },
|
|
{
|
|
get(_target, key) {
|
|
propertyReads.push(key);
|
|
return key === "value" ? "get-trap-value" : false;
|
|
},
|
|
getOwnPropertyDescriptor(target, key) {
|
|
descriptorReads.set(
|
|
key,
|
|
(descriptorReads.get(key) ?? 0) + 1,
|
|
);
|
|
return Reflect.getOwnPropertyDescriptor(target, key);
|
|
},
|
|
},
|
|
);
|
|
|
|
expect(snapshotRealtimeResult(source, isString)).toEqual(
|
|
realtimeSuccess("descriptor-value"),
|
|
);
|
|
expect(propertyReads).toEqual([]);
|
|
expect(descriptorReads).toEqual(
|
|
new Map<PropertyKey, number>([
|
|
["ok", 1],
|
|
["value", 1],
|
|
]),
|
|
);
|
|
});
|
|
|
|
it("fails closed when a proxy is revoked", () => {
|
|
const revocable = Proxy.revocable(
|
|
{ ok: true, value: "safe" },
|
|
{},
|
|
);
|
|
revocable.revoke();
|
|
|
|
expect(
|
|
snapshotRealtimeResult(revocable.proxy, isString),
|
|
).toBeNull();
|
|
expect(isRealtimeResult(revocable.proxy, isString)).toBe(false);
|
|
});
|
|
|
|
it.each([
|
|
{ ok: true, value: "safe", extra: true },
|
|
Object.assign(
|
|
Object.create({ inherited: true }) as Record<string, unknown>,
|
|
{ ok: true, value: "safe" },
|
|
),
|
|
Object.assign(
|
|
{ ok: true, value: "safe" },
|
|
{ [Symbol("hidden")]: true },
|
|
),
|
|
])("rejects extra, inherited and symbol-key shapes", (source) => {
|
|
expect(snapshotRealtimeResult(source, isString)).toBeNull();
|
|
});
|
|
});
|