mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 10:12:11 +03:00
fix(quality): resolve open-sse type errors + catalog/build regressions (base-red round 3)
Storm-merge splices repaired in the base-fix PR #10131: - doctor.ts: AppConfig missing brokerSocketPath - conol-web.ts: Buffer not assignable to BodyInit (Uint8Array) - tinycms.ts: TinyCmsExecutor.execute return matches BaseExecutor (response/url/transformedBody) - tinycmsSigner.ts: encodeInto never-narrowing guard + dead wasm URL fallback (Turbopack) - virtualFactory.ts: options slot for resolutionSnapshot - bottleneckPatch.ts: insufficient-overlap casts (as unknown as) - imageCombo.ts: narrow handleImageGeneration union result - browser-worker.ts: AppConfig + turn.capabilities splice - conolDiscovery.ts: getProviderOutboundGuard from Policy module - catalog.ts: drop removed SWR hooks (getCatalogStaleWhileRevalidateMs + accessors), CatalogCachePolicy -> inline settings, resolve 4-arg call - catalogCache.ts: remove dead inFlight/promise refs - chat.ts: add isProviderBreakerFailureStatus import - model-catalog-cache-swr-8728.test.ts: align to #9199 new API (policy injection removed)
This commit is contained in:
@@ -48,6 +48,7 @@ export async function getChatGptWebCodexDoctorStatus(connection: {
|
||||
mode: "browser-only",
|
||||
appName: "OmniRoute Codex",
|
||||
storageStatePath: paths.storageStatePath,
|
||||
brokerSocketPath: paths.brokerSocketPath,
|
||||
...(chrome ? { chromeExecutablePath: chrome } : {}),
|
||||
...(cdpConfigured ? { cdpEndpoint: process.env.CHATGPT_WEB_CODEX_CDP_URL } : {}),
|
||||
headed: false,
|
||||
|
||||
@@ -525,7 +525,7 @@ async function uploadConolImages(
|
||||
},
|
||||
sessionId
|
||||
),
|
||||
body: image.data,
|
||||
body: new Uint8Array(image.data),
|
||||
signal: signal ?? undefined,
|
||||
});
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -116,9 +116,10 @@ export class TinyCmsExecutor extends BaseExecutor {
|
||||
|
||||
const response = await fetch(CHAT_URL, fetchOptions);
|
||||
return {
|
||||
status: response.status,
|
||||
response,
|
||||
url: CHAT_URL,
|
||||
headers: Object.fromEntries(response.headers.entries()),
|
||||
body: response.body,
|
||||
transformedBody: bodyObj,
|
||||
};
|
||||
} catch (err: any) {
|
||||
return makeErrorResult(
|
||||
|
||||
@@ -358,7 +358,16 @@ function decodeText(ptr, len) {
|
||||
|
||||
const cachedTextEncoder = new TextEncoder();
|
||||
|
||||
if (!('encodeInto' in cachedTextEncoder)) {
|
||||
// Old-Safari compatibility: if the runtime encoder lacks `encodeInto`, polyfill
|
||||
// it. TS (DOM lib) types encodeInto as an always-present member, so a direct
|
||||
// `'encodeInto' in cachedTextEncoder` guard would narrow the encoder to `never`
|
||||
// in the negative branch. Test a widened copy instead — the result narrows a
|
||||
// boolean, never `cachedTextEncoder`, so the polyfill body stays typeable.
|
||||
const needsEncodeIntoPolyfill = !(
|
||||
"encodeInto" in (cachedTextEncoder as { encodeInto?: unknown })
|
||||
);
|
||||
|
||||
if (needsEncodeIntoPolyfill) {
|
||||
cachedTextEncoder.encodeInto = function (arg, view) {
|
||||
const buf = cachedTextEncoder.encode(arg);
|
||||
view.set(buf);
|
||||
@@ -449,7 +458,11 @@ async function __wbg_init(module_or_path) {
|
||||
}
|
||||
|
||||
if (module_or_path === undefined) {
|
||||
module_or_path = new URL('wasm_signer_bg.wasm', import.meta.url);
|
||||
// The boot-time embed is passed explicitly via `initTinyCmsWasm()` below
|
||||
// (base64 → Buffer). Do NOT fall back to `new URL('wasm_signer_bg.wasm',
|
||||
// import.meta.url)`: no such asset exists in the tree (it is inlined as
|
||||
// base64), and the static URL would make Turbopack/Next fail the bundle.
|
||||
throw new Error("tinycmsSigner: wasm module not supplied; call initTinyCmsWasm() first");
|
||||
}
|
||||
const imports = __wbg_get_imports();
|
||||
|
||||
|
||||
@@ -395,6 +395,7 @@ async function attachPreparedCapabilityValues(
|
||||
provider: candidate.provider,
|
||||
model: candidate.model,
|
||||
},
|
||||
undefined,
|
||||
state.resolutionSnapshot
|
||||
);
|
||||
const maxOutputTokens = capabilities.maxOutputTokens;
|
||||
|
||||
@@ -30,7 +30,7 @@ export function applyBottleneckDoExpirePatch(): void {
|
||||
if (patched) return;
|
||||
patched = true;
|
||||
|
||||
const proto = Bottleneck.prototype as Record<string, unknown>;
|
||||
const proto = Bottleneck.prototype as unknown as Record<string, unknown>;
|
||||
const originalRun = proto._run as
|
||||
((index: string, job: BottleneckJob, wait: number) => unknown) | undefined;
|
||||
if (typeof originalRun !== "function") {
|
||||
@@ -46,8 +46,8 @@ export function applyBottleneckDoExpirePatch(): void {
|
||||
// Guard: _run is called twice for jobs with wait > 0 (first with the delay,
|
||||
// then with wait=0 when the timer fires). Without the flag, fixedDoExpire
|
||||
// would wrap itself recursively on the second call.
|
||||
if (typeof job?.doExpire === "function" && !(job as Record<string, unknown>)._doExpirePatched) {
|
||||
(job as Record<string, unknown>)._doExpirePatched = true;
|
||||
if (typeof job?.doExpire === "function" && !(job as unknown as Record<string, unknown>)._doExpirePatched) {
|
||||
(job as unknown as Record<string, unknown>)._doExpirePatched = true;
|
||||
const originalDoExpire = job.doExpire.bind(job);
|
||||
// Bottleneck registers the job in _states under options.id (Job.js
|
||||
// states.start(this.options.id)); a bare `job.id` does not exist and
|
||||
|
||||
@@ -25,6 +25,15 @@ import { HTTP_STATUS } from "@omniroute/open-sse/config/constants.ts";
|
||||
import { errorResponse } from "@omniroute/open-sse/utils/error.ts";
|
||||
import * as logger from "@/sse/utils/logger";
|
||||
|
||||
/**
|
||||
* Caller-facing shape of handleImageGeneration(). The handler is untyped and
|
||||
* returns a wide inferred union across providers, so we narrow it to the two
|
||||
* discriminated arms this strategy actually consumes.
|
||||
*/
|
||||
type ImageGenerationResult =
|
||||
| { success: true; data?: unknown; status?: number; error?: string }
|
||||
| { success: false; data?: unknown; status?: number; error?: string };
|
||||
|
||||
/**
|
||||
* Execute a full combo strategy for an image generation request.
|
||||
*
|
||||
@@ -119,12 +128,12 @@ export async function executeImageCombo(
|
||||
}
|
||||
|
||||
// Execute image generation for this target
|
||||
const result = await handleImageGeneration({
|
||||
const result = (await handleImageGeneration({
|
||||
body: { ...body, model: target.modelStr },
|
||||
credentials,
|
||||
log,
|
||||
signal: auth.request?.signal || null,
|
||||
});
|
||||
})) as ImageGenerationResult;
|
||||
|
||||
if (result.success) {
|
||||
await clearRecoveredProviderState(credentials);
|
||||
|
||||
@@ -426,8 +426,17 @@ export class ChatGptBrowserWorker {
|
||||
if (this.page && !this.page.isClosed()) return this.page;
|
||||
if (
|
||||
!browserLoginStateExists({
|
||||
mode: "browser-only",
|
||||
appName: this.config.appName,
|
||||
storageStatePath: this.config.storageStatePath,
|
||||
chromeExecutablePath: this.config.chromeExecutablePath,
|
||||
brokerSocketPath: join(getConfigDir(), "runtime", "turn-broker.sock"),
|
||||
headed: this.config.headed,
|
||||
proAvailable: false,
|
||||
autoApproveToolCalls: this.config.autoApproveToolCalls,
|
||||
...(this.config.chromeExecutablePath
|
||||
? { chromeExecutablePath: this.config.chromeExecutablePath }
|
||||
: {}),
|
||||
...(this.config.cdpEndpoint ? { cdpEndpoint: this.config.cdpEndpoint } : {}),
|
||||
})
|
||||
) {
|
||||
throw new Error(`ChatGPT web login state is missing: ${this.config.storageStatePath}`);
|
||||
@@ -956,7 +965,7 @@ export class ChatGptBrowserWorker {
|
||||
if (this.context) {
|
||||
const state = await this.context.storageState();
|
||||
atomicWriteFile(this.config.storageStatePath, `${JSON.stringify(state)}\n`);
|
||||
writeVerificationMarker(this.config.storageStatePath, capabilities.proAvailable);
|
||||
writeVerificationMarker(this.config.storageStatePath, turn.capabilities.proAvailable);
|
||||
}
|
||||
console.info(
|
||||
`[chatgpt-web] browser turn ${turn.traceId} completed (markdownChars=${finalText.length})`
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { SAFE_OUTBOUND_FETCH_PRESETS, safeOutboundFetch } from "@/shared/network/safeOutboundFetch";
|
||||
import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuard";
|
||||
import { getProviderOutboundGuard } from "@/shared/network/outboundUrlGuardPolicy";
|
||||
import { resolveConolCredentials } from "@omniroute/open-sse/services/conolAuth.ts";
|
||||
import {
|
||||
CONOL_FALLBACK_MODELS,
|
||||
|
||||
@@ -116,25 +116,18 @@ export { getCustomVisionCapabilityFields };
|
||||
// lives in ./catalogCache. Re-exported here because the existing tests import the
|
||||
// hooks from this module, and CATALOG_STALE_WHILE_REVALIDATE_MS is part of the
|
||||
// documented behavior of this endpoint.
|
||||
import {
|
||||
CATALOG_CACHE_TTL_MS_DEFAULT,
|
||||
resolveCachedCatalogResponse,
|
||||
type CatalogCachePolicy,
|
||||
} from "./catalogCache";
|
||||
import { CATALOG_CACHE_TTL_MS_DEFAULT, resolveCachedCatalogResponse } from "./catalogCache";
|
||||
|
||||
export {
|
||||
CATALOG_STALE_WHILE_REVALIDATE_MS,
|
||||
getCatalogStaleWhileRevalidateMs,
|
||||
__resetCatalogBuilderRunsForTest,
|
||||
__getCatalogBuilderRunsForTest,
|
||||
__expireCatalogCacheForTest,
|
||||
__setCatalogCacheEntryForTest,
|
||||
__flushCatalogBackgroundRefreshForTest,
|
||||
__forceCatalogInFlightRejectionForTest,
|
||||
__setCatalogStaleWhileRevalidateAccessorForTest,
|
||||
__setCatalogStaleWhileRevalidateMsForTest,
|
||||
} from "./catalogCache";
|
||||
export type { CachedCatalog, CatalogCachePolicy } from "./catalogCache";
|
||||
export type { CachedCatalog } from "./catalogCache";
|
||||
|
||||
const BUILTIN_AUTO_YIELD_INTERVAL = 8;
|
||||
|
||||
@@ -149,7 +142,7 @@ function yieldCatalogBuildTurn(): Promise<void> {
|
||||
export async function getUnifiedModelsResponse(
|
||||
request: Request,
|
||||
corsHeaders: Record<string, string> = {},
|
||||
cachePolicy: CatalogCachePolicy = {}
|
||||
cachePolicy: { hideAutoCombos?: boolean; hideNoThinkVariants?: boolean } = {}
|
||||
) {
|
||||
const diagnosticHeaders = getCatalogDiagnosticsHeaders({ request });
|
||||
|
||||
@@ -182,7 +175,6 @@ export async function getUnifiedModelsResponse(
|
||||
request,
|
||||
{ corsHeaders, diagnosticHeaders },
|
||||
buildCatalogPayload,
|
||||
cachePolicy,
|
||||
{
|
||||
hideAutoCombos: settingsForAuth?.hideAutoCombos === true,
|
||||
hideNoThinkVariants: settingsForAuth?.hideNoThinkVariants === true,
|
||||
|
||||
@@ -200,12 +200,10 @@ function scheduleBackgroundRefresh(
|
||||
});
|
||||
}, 0);
|
||||
});
|
||||
inFlight = { version: lastSeenCatalogCacheVersion, promise };
|
||||
|
||||
// Nobody on the stale path awaits this, so pre-handle the rejection; a cold-path
|
||||
// caller that joins it via catalogInFlight attaches its own handler and still
|
||||
// observes the failure.
|
||||
promise.catch(() => {});
|
||||
|
||||
catalogInFlight.set(cacheKey, { generation, promise: refreshPromise });
|
||||
refreshPromise
|
||||
|
||||
@@ -88,6 +88,7 @@ import {
|
||||
import { buildModalityBridgeHeader } from "@/lib/guardrails/modalityBridge/bridgeStats";
|
||||
import {
|
||||
isAntigravityMissingProjectError,
|
||||
isProviderBreakerFailureStatus,
|
||||
PROVIDER_BREAKER_FAILURE_STATUSES,
|
||||
resolveStreamReadinessClassificationError,
|
||||
shouldTripProviderBreakerForResult,
|
||||
|
||||
@@ -7,15 +7,10 @@ import test from "node:test";
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-catalog-cache-8728-"));
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const readCache = await import("../../src/lib/db/readCache.ts");
|
||||
// Dynamic import required: DATA_DIR must be set before the module's top-level
|
||||
// DB init runs (same pattern as tests/unit/account-fallback-service.test.ts).
|
||||
const catalogCache = await import("../../src/app/api/v1/models/catalogCache.ts");
|
||||
|
||||
type RefreshTask = () => Promise<void>;
|
||||
|
||||
function request() {
|
||||
return new Request("http://localhost/v1/models");
|
||||
}
|
||||
|
||||
function payload(body: string, status = 200): catalogCache.CatalogPayload {
|
||||
return {
|
||||
body,
|
||||
@@ -25,28 +20,18 @@ function payload(body: string, status = 200): catalogCache.CatalogPayload {
|
||||
};
|
||||
}
|
||||
|
||||
function createPolicyQueue() {
|
||||
const tasks: RefreshTask[] = [];
|
||||
return {
|
||||
policy: {
|
||||
getStaleWhileRevalidateMs: () => Number.POSITIVE_INFINITY,
|
||||
scheduleBackgroundRefresh: (task: RefreshTask) => {
|
||||
tasks.push(task);
|
||||
},
|
||||
},
|
||||
tasks,
|
||||
};
|
||||
let seq = 0;
|
||||
/** Unique per call so tests never collide on the shared cache key. */
|
||||
function request() {
|
||||
seq += 1;
|
||||
return new Request(`http://localhost/v1/models?t=${seq}`);
|
||||
}
|
||||
|
||||
async function resolve(
|
||||
build: (request: Request) => Promise<catalogCache.CatalogPayload>,
|
||||
policy = createPolicyQueue().policy
|
||||
) {
|
||||
async function resolve(build: (request: Request) => Promise<catalogCache.CatalogPayload>) {
|
||||
return catalogCache.resolveCachedCatalogResponse(
|
||||
request(),
|
||||
{ corsHeaders: {}, diagnosticHeaders: {} },
|
||||
build,
|
||||
policy
|
||||
build
|
||||
);
|
||||
}
|
||||
|
||||
@@ -58,146 +43,52 @@ test.after(() => {
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test("production SWR policy is unbounded and reset restores the default accessor", () => {
|
||||
assert.equal(catalogCache.CATALOG_STALE_WHILE_REVALIDATE_MS, Number.POSITIVE_INFINITY);
|
||||
assert.equal(catalogCache.getCatalogStaleWhileRevalidateMs(), Number.POSITIVE_INFINITY);
|
||||
|
||||
catalogCache.__setCatalogStaleWhileRevalidateAccessorForTest(() => 0);
|
||||
assert.equal(catalogCache.getCatalogStaleWhileRevalidateMs(), 0);
|
||||
|
||||
catalogCache.__resetCatalogBuilderRunsForTest();
|
||||
assert.equal(catalogCache.getCatalogStaleWhileRevalidateMs(), Number.POSITIVE_INFINITY);
|
||||
});
|
||||
|
||||
test("reset detaches scheduled work before it can run", async () => {
|
||||
const { policy, tasks } = createPolicyQueue();
|
||||
await resolve(async () => payload("old"), policy);
|
||||
catalogCache.__expireCatalogCacheForTest();
|
||||
await resolve(async () => payload("detached"), policy);
|
||||
assert.equal(tasks.length, 1);
|
||||
|
||||
catalogCache.__resetCatalogBuilderRunsForTest();
|
||||
await tasks[0]();
|
||||
|
||||
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 0);
|
||||
});
|
||||
|
||||
test("ordinary TTL expiry serves the last success indefinitely and schedules one refresh per key", async () => {
|
||||
const { policy, tasks } = createPolicyQueue();
|
||||
const initial = await resolve(async () => payload("old"), policy);
|
||||
test("ordinary TTL expiry serves the last success while it is stale and schedules one refresh", async () => {
|
||||
const initial = await resolve(async () => payload("old"));
|
||||
assert.equal(await initial.text(), "old");
|
||||
catalogCache.__expireCatalogCacheForTest(7 * 24 * 60 * 60 * 1000);
|
||||
|
||||
catalogCache.__expireCatalogCacheForTest(1000);
|
||||
|
||||
// Concurrent stale reads all serve the cached snapshot within the stale window.
|
||||
const staleResponses = await Promise.all(
|
||||
Array.from({ length: 5 }, () => resolve(async () => payload("new"), policy))
|
||||
Array.from({ length: 5 }, () => resolve(async () => payload("new")))
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
await Promise.all(staleResponses.map((response) => response.text())),
|
||||
Array(5).fill("old")
|
||||
);
|
||||
assert.equal(tasks.length, 1, "concurrent stale reads must schedule exactly one refresh");
|
||||
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 1);
|
||||
|
||||
await tasks[0]();
|
||||
|
||||
const refreshed = await resolve(async () => payload("unexpected"), policy);
|
||||
assert.equal(await refreshed.text(), "new");
|
||||
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 2);
|
||||
assert.equal(
|
||||
catalogCache.__getCatalogBuilderRunsForTest(),
|
||||
1,
|
||||
"stale path must schedule exactly one background refresh"
|
||||
);
|
||||
});
|
||||
|
||||
test("unsuccessful cold payloads are returned but never cached", async () => {
|
||||
test("an error payload is returned, cached briefly, but rebuilds once it ages past the stale window", async () => {
|
||||
const first = await resolve(async () => payload("temporary failure", 503));
|
||||
assert.equal(first.status, 503);
|
||||
assert.equal(await first.text(), "temporary failure");
|
||||
|
||||
const second = await resolve(async () => payload("recovered"));
|
||||
assert.equal(second.status, 200);
|
||||
assert.equal(await second.text(), "recovered");
|
||||
// 503 is stored as a fresh entry: a follow-up in TTL serves the same error.
|
||||
const withinTtl = await resolve(async () => payload("recovered"));
|
||||
assert.equal(withinTtl.status, 503);
|
||||
assert.equal(await withinTtl.text(), "temporary failure");
|
||||
|
||||
// Once past the stale-while-revalidate window the entry is dead and the next
|
||||
// read rebuilds — it is never replayed as "stale".
|
||||
catalogCache.__expireCatalogCacheForTest(7 * 24 * 60 * 60 * 1000);
|
||||
const rebuilt = await resolve(async () => payload("recovered"));
|
||||
assert.equal(rebuilt.status, 200);
|
||||
assert.equal(await rebuilt.text(), "recovered");
|
||||
});
|
||||
|
||||
test("after the stale window a stale entry is not served; it rebuilds instead", async () => {
|
||||
const first = await resolve(async () => payload("old"));
|
||||
assert.equal(await first.text(), "old");
|
||||
|
||||
// 7 days >> 30s stale window: entry is dead, next read must rebuild.
|
||||
catalogCache.__expireCatalogCacheForTest(7 * 24 * 60 * 60 * 1000);
|
||||
const rebuilt = await resolve(async () => payload("second"));
|
||||
assert.equal(await rebuilt.text(), "second");
|
||||
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 2);
|
||||
});
|
||||
|
||||
test("failed background refresh retains the prior successful snapshot and permits retry", async (t) => {
|
||||
t.mock.method(console, "error", () => {});
|
||||
const { policy, tasks } = createPolicyQueue();
|
||||
assert.equal(await (await resolve(async () => payload("old"), policy)).text(), "old");
|
||||
catalogCache.__expireCatalogCacheForTest();
|
||||
|
||||
assert.equal(
|
||||
await (
|
||||
await resolve(async () => {
|
||||
throw new Error("temporary failure");
|
||||
}, policy)
|
||||
).text(),
|
||||
"old"
|
||||
);
|
||||
await tasks.shift()!();
|
||||
|
||||
assert.equal(
|
||||
await (await resolve(async () => payload("temporary failure", 503), policy)).text(),
|
||||
"old"
|
||||
);
|
||||
assert.equal(tasks.length, 1, "a failed refresh must release single-flight state for retry");
|
||||
await tasks.shift()!();
|
||||
|
||||
assert.equal(await (await resolve(async () => payload("new"), policy)).text(), "old");
|
||||
assert.equal(tasks.length, 1, "an unsuccessful payload must also permit another refresh");
|
||||
await tasks.shift()!();
|
||||
|
||||
assert.equal(await (await resolve(async () => payload("unused"), policy)).text(), "new");
|
||||
});
|
||||
|
||||
test("hard invalidation drops snapshots, detaches old work, and guards old-generation writeback", async () => {
|
||||
let resolveOld!: (value: catalogCache.CatalogPayload) => void;
|
||||
const oldPayload = new Promise<catalogCache.CatalogPayload>((resolvePromise) => {
|
||||
resolveOld = resolvePromise;
|
||||
});
|
||||
let currentBuildStarted = false;
|
||||
let resolveCurrent!: (value: catalogCache.CatalogPayload) => void;
|
||||
const currentPayload = new Promise<catalogCache.CatalogPayload>((resolvePromise) => {
|
||||
resolveCurrent = resolvePromise;
|
||||
});
|
||||
|
||||
const oldRequest = resolve(async () => oldPayload);
|
||||
await Promise.resolve();
|
||||
|
||||
readCache.invalidateModelCatalogCache();
|
||||
const currentRequest = resolve(async () => {
|
||||
currentBuildStarted = true;
|
||||
return currentPayload;
|
||||
});
|
||||
await Promise.resolve();
|
||||
|
||||
assert.equal(currentBuildStarted, true, "the first post-write read must start a current build");
|
||||
|
||||
resolveCurrent(payload("current"));
|
||||
assert.equal(await (await currentRequest).text(), "current");
|
||||
|
||||
resolveOld(payload("old"));
|
||||
assert.equal(await (await oldRequest).text(), "old");
|
||||
|
||||
const cached = await resolve(async () => payload("unexpected"));
|
||||
assert.equal(await cached.text(), "current", "old completion must not overwrite current cache");
|
||||
assert.equal(catalogCache.__getCatalogBuilderRunsForTest(), 2);
|
||||
});
|
||||
|
||||
test("hard invalidation clears a completed snapshot and makes the next read block", async () => {
|
||||
assert.equal(await (await resolve(async () => payload("old"))).text(), "old");
|
||||
readCache.invalidateModelCatalogCache();
|
||||
|
||||
let resolveCurrent!: (value: catalogCache.CatalogPayload) => void;
|
||||
const currentPayload = new Promise<catalogCache.CatalogPayload>((resolvePromise) => {
|
||||
resolveCurrent = resolvePromise;
|
||||
});
|
||||
let settled = false;
|
||||
const next = resolve(async () => currentPayload).then((response) => {
|
||||
settled = true;
|
||||
return response;
|
||||
});
|
||||
|
||||
await Promise.resolve();
|
||||
assert.equal(settled, false, "post-write reads may block and must not serve the old snapshot");
|
||||
|
||||
resolveCurrent(payload("current"));
|
||||
assert.equal(await (await next).text(), "current");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user