@openchambery/web 2.1.9-beta.1 → 2.1.9-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AgentAvatar-CJQwq8UM.js → AgentAvatar-qLxaEaDh.js} +1 -1
- package/dist/assets/{AssistantView-j9aI8OWI.js → AssistantView-3Lz_SADb.js} +3 -3
- package/dist/assets/{DiagramView-EsIuVPrU.js → DiagramView-BZP5Aj4S.js} +1 -1
- package/dist/assets/{FadeInOnReveal-BNVExwoL.js → FadeInOnReveal-CX8Bkq-l.js} +1 -1
- package/dist/assets/{Icon-B4xKSuz3.js → Icon-BCFeHDWx.js} +1 -1
- package/dist/assets/{ImageLoadingPlaceholder-xB8WeKks.js → ImageLoadingPlaceholder-DEpMJXzV.js} +1 -1
- package/dist/assets/{LoadingImage-CoT3GWhg.js → LoadingImage-BAT9D9wN.js} +1 -1
- package/dist/assets/{MarkdownRenderer-TmfHeWHu.js → MarkdownRenderer-DpB0hJ_E.js} +2 -2
- package/dist/assets/{MarkdownRendererImpl-vkv1QLTd.js → MarkdownRendererImpl-Hg1bbRlM.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-BbnksqWU.js → MarkstreamRendererImpl-BfUZU3WC.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-CtGncmBY.js → MobileDetailNavigation-DqGKTFzT.js} +1 -1
- package/dist/assets/{MobileShareBridge-pYQFFYmm.js → MobileShareBridge-DHHw00TT.js} +2 -2
- package/dist/assets/{MobileTabPageHeader-D7NAfatx.js → MobileTabPageHeader-jFeAz_Z2.js} +1 -1
- package/dist/assets/{MultiRunWindow-gZk-f5lA.js → MultiRunWindow-C92A1Qkc.js} +1 -1
- package/dist/assets/{OnboardingScreen-BiLurnX5.js → OnboardingScreen-CqcZEptL.js} +1 -1
- package/dist/assets/{OverlayScrollbar-DIsrS93m.js → OverlayScrollbar-BY5HaNrb.js} +1 -1
- package/dist/assets/{SettingsView-D7Ykm-CM.js → SettingsView-C2-Tug3M.js} +1 -1
- package/dist/assets/{SettingsWindow-CyPZw4xx.js → SettingsWindow-D_j-lLiY.js} +1 -1
- package/dist/assets/{TerminalView-D-X0mGO4.js → TerminalView-Veaa8xms.js} +1 -1
- package/dist/assets/{ToolOutputDialog-C4vQBg3z.js → ToolOutputDialog-CI32t1nx.js} +1 -1
- package/dist/assets/{appThemeRegistry-DUH4Fprj.js → appThemeRegistry-BhVqv80m.js} +1 -1
- package/dist/assets/{assistant-attachment-cache-Bzewqov4.js → assistant-attachment-cache-B2UAUHUZ.js} +1 -1
- package/dist/assets/{button-BupmKVUs.js → button-CWSs9Baz.js} +1 -1
- package/dist/assets/{collapsible-BAPWS6G-.js → collapsible-ngtoKADo.js} +1 -1
- package/dist/assets/{desktopHosts-BZ7iYE-l.js → desktopHosts-C0e7Nes2.js} +1 -1
- package/dist/assets/{desktopRelayRestore-S9wEiY2U.js → desktopRelayRestore-DckkHKsi.js} +1 -1
- package/dist/assets/{diagram-0a7vH45N.js → diagram-DmlTCkmV.js} +1 -1
- package/dist/assets/{dialog-BYLD4Cm3.js → dialog-BMa16qrs.js} +1 -1
- package/dist/assets/{dropdown-menu.styles-HOWSubFF.js → dropdown-menu.styles-BNwZmBRp.js} +1 -1
- package/dist/assets/{fileMentionTouchSelection-BcqQPEDT.js → fileMentionTouchSelection-ytHOa8vD.js} +1 -1
- package/dist/assets/{fileQueries-DZR-mK8h.js → fileQueries-DVpZC5ha.js} +1 -1
- package/dist/assets/{gitApi-BtJfaFQy.js → gitApi-DZSpp5_u.js} +1 -1
- package/dist/assets/{image-loading-CsxDGgek.js → image-loading-Bj11k4gM.js} +1 -1
- package/dist/assets/{input-DgrLdIys.js → input-CALwaZEB.js} +1 -1
- package/dist/assets/{insertionBoundaries-z2K9a6oY.js → insertionBoundaries-B3F2vgJn.js} +1 -1
- package/dist/assets/{main-DAHeBbJJ.js → main-HsqXFi70.js} +2 -2
- package/dist/assets/{main-BDCqUxWf.js → main-mdIF-V3Y.js} +4 -4
- package/dist/assets/{markdownHeightCache-Dyctmwqv.js → markdownHeightCache-DXM_5jTe.js} +1 -1
- package/dist/assets/{mcpQueries-BSwb38Pz.js → mcpQueries-BAU9FYFF.js} +1 -1
- package/dist/assets/miniChat-B7C-IBFC.js +2 -0
- package/dist/assets/mobile-CqWR9PBo.js +2 -0
- package/dist/assets/{multirun-DldZm-Aq.js → multirun-D3iS9HjC.js} +1 -1
- package/dist/assets/{persistence-B4Ni3Gmv.js → persistence-BKszKaoQ.js} +1 -1
- package/dist/assets/{platform-BQPsr8Zl.js → platform-CgwxwfA4.js} +1 -1
- package/dist/assets/{projectMeta-zESSNFVw.js → projectMeta-DlHKE_8C.js} +1 -1
- package/dist/assets/{relay-image-stream-qI2NTXbG.js → relay-image-stream-SV0lH6aG.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-t1IsnPfc.js → renderElectronMiniChatApp-CVT0WyUz.js} +2 -2
- package/dist/assets/{renderMobileApp-BrC28EkU.js → renderMobileApp-hUJNFMKe.js} +4 -4
- package/dist/assets/runtime-auth-BpCQ-dPr.js +1 -0
- package/dist/assets/{runtime-fetch-QCWYlAx0.js → runtime-fetch-WgH0pROs.js} +1 -1
- package/dist/assets/{runtime-switch-3ad3yzM7.js → runtime-switch-CMiMO5Yj.js} +1 -1
- package/dist/assets/{runtimeConfig-5NAbSrPI.js → runtimeConfig-DAxmpV5p.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-CxreCHBU.js → runtimeEndpointReset-BCf2ORY3.js} +2 -2
- package/dist/assets/{scheduledTaskNavigation-C1PrFEhV.js → scheduledTaskNavigation-DQcLqQ52.js} +1 -1
- package/dist/assets/{sessionChangeSummary-CUMpXSiU.js → sessionChangeSummary-BYoAdclB.js} +1 -1
- package/dist/assets/{sessionLookup-DD8YnthC.js → sessionLookup-DxWtYNX-.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-DogWaIrG.js → sortable-tabs-strip-CscHgCaq.js} +1 -1
- package/dist/assets/{speechSettingsQueries-PsDvs5QG.js → speechSettingsQueries-CBrLvvue.js} +2 -2
- package/dist/assets/{terminalApi-kRMttlcP.js → terminalApi-rxsMWL0o.js} +1 -1
- package/dist/assets/{textarea-Boj9axpc.js → textarea-ClV5sMS-.js} +1 -1
- package/dist/assets/{tooltip-Dbgnq-Mh.js → tooltip-CV6dvLBp.js} +1 -1
- package/dist/assets/{url-Bd61gNgl.js → url-nBOUbtV_.js} +1 -1
- package/dist/assets/{useAppFontEffects-Ba6H7bLU.js → useAppFontEffects-BvJWL9Tq.js} +5 -5
- package/dist/assets/{useAssistantUIStore-Dlvb7P0q.js → useAssistantUIStore-OyPm_4M_.js} +1 -1
- package/dist/assets/{useDesktopWindowControlsLayout-CbeUwtDM.js → useDesktopWindowControlsLayout-6B2M3hOM.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-wtPL_zdo.js → useEffectiveDirectory-fANUMttq.js} +1 -1
- package/dist/assets/{useFontPreferences-lAw0Cbza.js → useFontPreferences-DqEIKMEW.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-DXpdkzw_.js → useMobileNavigationStore-DRpR2Oyd.js} +11 -11
- package/dist/assets/{useUIStore-CDMI_Yzu.js → useUIStore-CEqU9Ujv.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-BXDSKlPX.js → useWorkerHighlightedLines-D1io7Q4t.js} +1 -1
- package/dist/index.html +11 -11
- package/dist/mini-chat.html +11 -11
- package/dist/mobile.html +11 -11
- package/package.json +1 -1
- package/server/lib/git/DOCUMENTATION.md +1 -1
- package/server/lib/git/branches.test.js +39 -0
- package/server/lib/git/service.js +3 -43
- package/server/lib/relay/DOCUMENTATION.md +20 -0
- package/server/lib/relay/relay-server.e2e.test.ts +35 -0
- package/server/lib/relay/tunnel-host.js +14 -4
- package/server/lib/relay/tunnel-host.test.js +37 -1
- package/server/lib/session-index/DOCUMENTATION.md +17 -0
- package/server/lib/session-index/sync-runtime.js +34 -14
- package/server/lib/session-index/sync-runtime.test.js +121 -0
- package/dist/assets/miniChat-DEIx3C4R.js +0 -2
- package/dist/assets/mobile-DmNNI21w.js +0 -2
- package/dist/assets/runtime-auth-C8w0nlKy.js +0 -1
|
@@ -75,6 +75,26 @@ See also `packages/web/server/lib/opencode/DOCUMENTATION.md` (“Catalog / OpenC
|
|
|
75
75
|
4. **Handshake.** Over that connection pair, client and host run the E2EE handshake and derive a shared encrypted channel the relay cannot read.
|
|
76
76
|
5. **Traffic.** All normal app traffic is multiplexed and encrypted through that channel. On the host, decrypted requests are dispatched to the local server over loopback; responses stream back encrypted. Reconnects re-establish a fresh channel and the app's existing retry machinery recovers.
|
|
77
77
|
|
|
78
|
+
## Negotiated JSON response compression
|
|
79
|
+
|
|
80
|
+
Each HTTP request may advertise `acceptEncoding: 'gzip'` in its tunnel JSON head
|
|
81
|
+
when the client supports `DecompressionStream`. The Host may then set
|
|
82
|
+
`encoding: 'gzip'` on the response head and stream gzip bytes as ordinary HttpBody
|
|
83
|
+
frames **before encryption**. The client decompresses before exposing its Fetch
|
|
84
|
+
Response; HTTP headers describe the decoded body, with no compressed content-length
|
|
85
|
+
or content-encoding leaked to callers. Wire framing, batching and crypto counters
|
|
86
|
+
are unchanged. Old Hosts ignore the opt-in; old clients (including native Android
|
|
87
|
+
notifications) omit it and receive uncompressed bodies.
|
|
88
|
+
|
|
89
|
+
Only application/json and application/*+json bodies qualify. Known decoded bodies
|
|
90
|
+
under 1 KiB, HEAD/no-body responses, SSE and binary assets are not compressed.
|
|
91
|
+
Unknown-length JSON is streamed rather than buffered in full; loopback fetch's
|
|
92
|
+
automatic decompression is accounted for before checking length. Cancellation and
|
|
93
|
+
errors propagate through the stream pipeline. Host tests cover negotiation and
|
|
94
|
+
legacy/SSE/binary exclusions; client tests cover malformed gzip and cancellation;
|
|
95
|
+
the authenticated compiled-Relay integration test checks decoded snapshot fidelity
|
|
96
|
+
and encrypted wire-byte reduction on 4,000 session summaries.
|
|
97
|
+
|
|
78
98
|
## Candidate refresh (staying off the relay when direct works)
|
|
79
99
|
|
|
80
100
|
Pairing-payload transport candidates are a snapshot: when DHCP hands the host
|
|
@@ -36,6 +36,9 @@ import { requestSpeechSession } from '../../../../ui/src/lib/voice/direct-sessio
|
|
|
36
36
|
|
|
37
37
|
const timeoutMs = 10_000;
|
|
38
38
|
const previewPng = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aD1sAAAAASUVORK5CYII=', 'base64');
|
|
39
|
+
const initializationSnapshot = { sessions: Array.from({ length: 4000 }, (_, id) => ({
|
|
40
|
+
id: `session-${id}`, title: `Session ${id}`, directory: '/repo/project', time: { created: 1000, updated: 2000 },
|
|
41
|
+
})) };
|
|
39
42
|
const wait = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
40
43
|
|
|
41
44
|
const eventually = async (predicate: () => boolean | Promise<boolean>, message: string, timeout = timeoutMs) => {
|
|
@@ -215,6 +218,10 @@ const startOrigin = async () => {
|
|
|
215
218
|
});
|
|
216
219
|
return;
|
|
217
220
|
}
|
|
221
|
+
if (pathname === '/api/e2e/initialization' && req.method === 'GET') {
|
|
222
|
+
await uiAuth.requireAuth(req, res, () => res.json(initializationSnapshot));
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
218
225
|
if (pathname === '/api/agent' && req.method === 'GET') {
|
|
219
226
|
await uiAuth.requireAuth(req, res, () => {
|
|
220
227
|
const directory = new URL(req.url ?? '/', 'http://origin').searchParams.get('location[directory]');
|
|
@@ -401,10 +408,32 @@ describe('private relay compiled-process boundary', () => {
|
|
|
401
408
|
origin = await startOrigin();
|
|
402
409
|
const identity = await buildIdentity();
|
|
403
410
|
host = startRelayHost({ relayUrl: relay.url, identity, getLocalPort: () => origin.port, onStatus: () => {}, logger: { warn: () => {}, info: () => {} } });
|
|
411
|
+
let receivedBytes = 0;
|
|
404
412
|
client = createRelayTunnelClient({
|
|
405
413
|
relayUrl: relay.url, serverId: identity.serverId, hostEncPubJwk: identity.hostEncPubJwk,
|
|
406
414
|
helloRetryMs: 50, helloTimeoutMs: 2_000, pingIntervalMs: 500, pingTimeoutMs: 250,
|
|
407
415
|
reconnectBaseDelayMs: 50, reconnectMaxDelayMs: 250, batchWindowMs: 5,
|
|
416
|
+
createWireSocket: (url) => {
|
|
417
|
+
const socket = new globalThis.WebSocket(url);
|
|
418
|
+
socket.binaryType = 'arraybuffer';
|
|
419
|
+
const wire = {
|
|
420
|
+
get readyState() { return socket.readyState; },
|
|
421
|
+
send: (data: string | ArrayBuffer | Uint8Array) => socket.send(data as string | ArrayBuffer),
|
|
422
|
+
close: (code?: number, reason?: string) => socket.close(code, reason),
|
|
423
|
+
onopen: null as (() => void) | null,
|
|
424
|
+
onmessage: null as ((event: { data: unknown }) => void) | null,
|
|
425
|
+
onclose: null as ((event: { code: number; reason: string }) => void) | null,
|
|
426
|
+
onerror: null as (() => void) | null,
|
|
427
|
+
};
|
|
428
|
+
socket.onopen = () => wire.onopen?.();
|
|
429
|
+
socket.onmessage = (event) => {
|
|
430
|
+
receivedBytes += typeof event.data === 'string' ? Buffer.byteLength(event.data) : event.data.byteLength;
|
|
431
|
+
wire.onmessage?.({ data: event.data });
|
|
432
|
+
};
|
|
433
|
+
socket.onclose = (event) => wire.onclose?.({ code: event.code, reason: event.reason });
|
|
434
|
+
socket.onerror = () => wire.onerror?.();
|
|
435
|
+
return wire;
|
|
436
|
+
},
|
|
408
437
|
});
|
|
409
438
|
|
|
410
439
|
await eventually(() => host!.getStatus().state === 'connected' && client!.getStatus().state === 'connected', 'host and TypeScript client did not connect');
|
|
@@ -415,6 +444,12 @@ describe('private relay compiled-process boundary', () => {
|
|
|
415
444
|
expect(httpResponse.status).toBe(200);
|
|
416
445
|
expect(await httpResponse.json()).toEqual({ body: 'stream-body', hasRelayConnection: true });
|
|
417
446
|
|
|
447
|
+
const beforeSnapshot = receivedBytes;
|
|
448
|
+
const compressed = await client.fetch('/api/e2e/initialization', { headers: { authorization: 'Bearer client-token' } });
|
|
449
|
+
expect(compressed.status).toBe(200);
|
|
450
|
+
expect(await compressed.json()).toEqual(initializationSnapshot);
|
|
451
|
+
expect(receivedBytes - beforeSnapshot).toBeLessThan(Buffer.byteLength(JSON.stringify(initializationSnapshot)) / 4);
|
|
452
|
+
|
|
418
453
|
const sseResponse = await client.fetch('/api/e2e/sse', { headers: { authorization: 'Bearer client-token' } });
|
|
419
454
|
const reader = sseResponse.body!.getReader();
|
|
420
455
|
const chunks: Uint8Array[] = [];
|
|
@@ -258,11 +258,21 @@ export const createTunnelHost = ({
|
|
|
258
258
|
if (STRIPPED_RESPONSE_HEADERS.has(name)) continue;
|
|
259
259
|
responseHeaders[name] = value;
|
|
260
260
|
}
|
|
261
|
-
await sendJson(TunnelFrameType.HttpResponse, streamId, { status: response.status, headers: responseHeaders });
|
|
262
|
-
|
|
263
261
|
try {
|
|
264
|
-
|
|
265
|
-
|
|
262
|
+
const contentType = (response.headers.get('content-type') ?? '').split(';', 1)[0].trim().toLowerCase();
|
|
263
|
+
const contentLength = response.headers.has('content-encoding') ? null : response.headers.get('content-length');
|
|
264
|
+
// Compress finite JSON responses before encryption, never SSE or binary assets.
|
|
265
|
+
// Old clients omit acceptEncoding and old hosts ignore it, preserving v1 framing.
|
|
266
|
+
const gzip = request.acceptEncoding === 'gzip' && typeof CompressionStream === 'function'
|
|
267
|
+
&& method !== 'HEAD' && response.body !== null
|
|
268
|
+
&& (contentType === 'application/json' || /^application\/[^/]+\+json$/.test(contentType))
|
|
269
|
+
&& (contentLength === null || Number(contentLength) >= 1024);
|
|
270
|
+
const body = gzip ? response.body.pipeThrough(new CompressionStream('gzip')) : response.body;
|
|
271
|
+
await sendJson(TunnelFrameType.HttpResponse, streamId, {
|
|
272
|
+
status: response.status, headers: responseHeaders, ...(gzip ? { encoding: 'gzip' } : {}),
|
|
273
|
+
});
|
|
274
|
+
if (body) {
|
|
275
|
+
for await (const chunk of body) {
|
|
266
276
|
if (closed || stream.abort.signal.aborted) return;
|
|
267
277
|
const bytes = chunk instanceof Uint8Array ? chunk : new Uint8Array(chunk);
|
|
268
278
|
for (const piece of chunkPayload(bytes, MAX_TUNNEL_PAYLOAD_BYTES)) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// corrupted payload. This test must stay red until send serialization is fixed.
|
|
8
8
|
// Production send path is intentionally not changed here.
|
|
9
9
|
|
|
10
|
-
import { describe, expect, it } from '
|
|
10
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
11
11
|
|
|
12
12
|
import {
|
|
13
13
|
MAX_TUNNEL_PAYLOAD_BYTES,
|
|
@@ -68,6 +68,42 @@ const waitFor = async (predicate, { timeoutMs = 2000, intervalMs = 5 } = {}) =>
|
|
|
68
68
|
throw new Error('waitFor timed out');
|
|
69
69
|
};
|
|
70
70
|
|
|
71
|
+
describe('negotiated JSON response compression', () => {
|
|
72
|
+
it.each([
|
|
73
|
+
{ acceptEncoding: 'gzip', contentType: 'application/json', compressed: true },
|
|
74
|
+
{ acceptEncoding: 'gzip', contentType: 'application/problem+json', compressed: true },
|
|
75
|
+
{ acceptEncoding: undefined, contentType: 'application/json', compressed: false },
|
|
76
|
+
{ acceptEncoding: 'gzip', contentType: 'text/event-stream', compressed: false },
|
|
77
|
+
{ acceptEncoding: 'gzip', contentType: 'image/png', compressed: false },
|
|
78
|
+
{ acceptEncoding: 'gzip', contentType: 'application/json', small: true, compressed: false },
|
|
79
|
+
])('preserves body and legacy/SSE behavior for $contentType / $acceptEncoding / small=$small', async ({ acceptEncoding, contentType, small, compressed }) => {
|
|
80
|
+
const text = JSON.stringify({ rows: Array.from({ length: small ? 1 : 4000 }, (_, id) => ({ id, title: 'Session summary', directory: '/repo/project' })) });
|
|
81
|
+
const raw = textEncoder.encode(text);
|
|
82
|
+
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(raw, {
|
|
83
|
+
headers: { 'content-type': contentType, 'content-length': String(raw.byteLength) },
|
|
84
|
+
}));
|
|
85
|
+
const frames = [];
|
|
86
|
+
const host = createTunnelHost({ connectionId: 'compression-test', getLocalPort: () => 9,
|
|
87
|
+
sendFrame: (frame) => { frames.push(decodeTunnelFrame(frame)); }, getBufferedAmount: () => 0 });
|
|
88
|
+
try {
|
|
89
|
+
await host.handleFrame(encodeTunnelFrame(TunnelFrameType.HttpRequest, 1, encodeJsonPayload({
|
|
90
|
+
method: 'GET', path: '/api/test', query: '', headers: {}, ...(acceptEncoding ? { acceptEncoding } : {}),
|
|
91
|
+
})));
|
|
92
|
+
await waitFor(() => frames.some((frame) => frame.frameType === TunnelFrameType.StreamEnd));
|
|
93
|
+
const head = JSON.parse(textDecoder.decode(frames.find((frame) => frame.frameType === TunnelFrameType.HttpResponse).payload));
|
|
94
|
+
expect(head.encoding).toBe(compressed ? 'gzip' : undefined);
|
|
95
|
+
expect(head.headers['content-length']).toBeUndefined();
|
|
96
|
+
const body = new Blob(frames.filter((frame) => frame.frameType === TunnelFrameType.HttpBody).map((frame) => frame.payload));
|
|
97
|
+
if (compressed) {
|
|
98
|
+
expect(body.size).toBeLessThan(raw.byteLength / 4);
|
|
99
|
+
expect(await new Response(body.stream().pipeThrough(new DecompressionStream('gzip'))).text()).toBe(text);
|
|
100
|
+
} else {
|
|
101
|
+
expect(await body.text()).toBe(text);
|
|
102
|
+
}
|
|
103
|
+
} finally { host.close(); fetchMock.mockRestore(); }
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
71
107
|
describe('isTunnelWsPathAllowed', () => {
|
|
72
108
|
it('allows exact terminal/dictation/event sockets and preview proxy prefix', () => {
|
|
73
109
|
expect(isTunnelWsPathAllowed('/api/terminal/ws')).toBe(true);
|
|
@@ -121,6 +121,23 @@ observer active after startup refresh work becomes idle, and successful
|
|
|
121
121
|
event-driven index writes with a semantic snapshot change publish a new
|
|
122
122
|
revision tip immediately.
|
|
123
123
|
|
|
124
|
+
Background directory and child-enrichment progress share a fixed 500ms trailing
|
|
125
|
+
tip window: the first pending change arms the injected timer, later changes do
|
|
126
|
+
not reset it, and delivery reads the latest revision and sync flags. New-batch
|
|
127
|
+
enqueue, idle (`active=false` and `enriching=false`), `publishChange()` (including
|
|
128
|
+
pin/rename and event-driven writes), and stop bypass the window and cancel its
|
|
129
|
+
pending delivery. These immediate edges are exceptions to the 500ms rate bound.
|
|
130
|
+
Stop publishes idle with interrupted directories marked failed; late worker
|
|
131
|
+
cleanup cannot emit another revision or leave a pending tip timer. Internal
|
|
132
|
+
revision increments and `waitForChange` delivery remain immediate, including the
|
|
133
|
+
final state. Full snapshots are materialized for waiting readers, not for each
|
|
134
|
+
tip. No persisted revision, failure, or runtime-isolation semantics change.
|
|
135
|
+
|
|
136
|
+
The shared renderer independently throttles busy tips to a fixed 1000ms window
|
|
137
|
+
for compatibility with older Hosts. Idle/legacy tips use 100ms debounce and
|
|
138
|
+
shorten a pending busy deadline without extending it. Stream-ready and safety
|
|
139
|
+
timeout repair retain their existing behavior; GET remains authoritative.
|
|
140
|
+
|
|
124
141
|
The OpenCode proxy calls `noteInteractiveRequest()` for selected-session reads
|
|
125
142
|
and mutations. That aborts the current background list, yields for one second,
|
|
126
143
|
then resumes the same directory. Consequently a startup index refresh cannot
|
|
@@ -7,6 +7,7 @@ const FULL_RECONCILE_INTERVAL_MS = 24 * 60 * 60 * 1000;
|
|
|
7
7
|
const FETCH_TIMEOUT_MS = 6_000;
|
|
8
8
|
const INTERACTIVE_YIELD_MS = 1_000;
|
|
9
9
|
const LONG_POLL_MAX_MS = 25_000;
|
|
10
|
+
const REVISION_TIP_THROTTLE_MS = 500;
|
|
10
11
|
|
|
11
12
|
const normalizeDirectory = (value) => {
|
|
12
13
|
if (typeof value !== 'string') return null;
|
|
@@ -64,7 +65,7 @@ export const createSessionIndexSyncRuntime = ({
|
|
|
64
65
|
buildOpenCodeUrl,
|
|
65
66
|
getOpenCodeAuthHeaders,
|
|
66
67
|
waitForOpenCodeReady,
|
|
67
|
-
/** Optional tip sink: ({ revision, sync })
|
|
68
|
+
/** Optional tip sink: ({ revision, sync }); background progress is coalesced. */
|
|
68
69
|
onRevisionTip = null,
|
|
69
70
|
/**
|
|
70
71
|
* Optional Host authority projection applied to each upstream list page
|
|
@@ -96,10 +97,11 @@ export const createSessionIndexSyncRuntime = ({
|
|
|
96
97
|
const failedDirectories = new Set();
|
|
97
98
|
const waiters = new Set();
|
|
98
99
|
let batchDirectories = new Set();
|
|
100
|
+
let revisionTipTimer;
|
|
99
101
|
|
|
100
102
|
const syncState = () => ({
|
|
101
|
-
active: running || queue.length > 0,
|
|
102
|
-
enriching: enrichingChildFlags || childFlagQueue.length > 0,
|
|
103
|
+
active: !stopped && (running || queue.length > 0),
|
|
104
|
+
enriching: !stopped && (enrichingChildFlags || childFlagQueue.length > 0),
|
|
103
105
|
completed: completedDirectories.size,
|
|
104
106
|
total: batchDirectories.size,
|
|
105
107
|
pendingDirectories: [...queuedDirectories],
|
|
@@ -113,17 +115,17 @@ export const createSessionIndexSyncRuntime = ({
|
|
|
113
115
|
...sessionIndexService.snapshot(),
|
|
114
116
|
});
|
|
115
117
|
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
118
|
+
const broadcastRevisionTip = () => {
|
|
119
|
+
if (revisionTipTimer !== undefined) clearTimer(revisionTipTimer);
|
|
120
|
+
revisionTipTimer = undefined;
|
|
121
|
+
const sync = syncState();
|
|
120
122
|
if (typeof onRevisionTip === 'function') {
|
|
121
123
|
try {
|
|
122
124
|
onRevisionTip({
|
|
123
|
-
revision
|
|
125
|
+
revision,
|
|
124
126
|
sync: {
|
|
125
|
-
active:
|
|
126
|
-
enriching:
|
|
127
|
+
active: sync.active,
|
|
128
|
+
enriching: sync.enriching,
|
|
127
129
|
},
|
|
128
130
|
occurredAt: now(),
|
|
129
131
|
});
|
|
@@ -133,6 +135,21 @@ export const createSessionIndexSyncRuntime = ({
|
|
|
133
135
|
}
|
|
134
136
|
};
|
|
135
137
|
|
|
138
|
+
const notify = (immediate = false) => {
|
|
139
|
+
if (stopped && !immediate) return;
|
|
140
|
+
revision += 1;
|
|
141
|
+
if (waiters.size > 0) {
|
|
142
|
+
const value = snapshot();
|
|
143
|
+
for (const resolve of [...waiters]) resolve(value);
|
|
144
|
+
}
|
|
145
|
+
const sync = syncState();
|
|
146
|
+
if (immediate || (!sync.active && !sync.enriching)) {
|
|
147
|
+
broadcastRevisionTip();
|
|
148
|
+
} else if (revisionTipTimer === undefined && typeof onRevisionTip === 'function') {
|
|
149
|
+
revisionTipTimer = setTimer(broadcastRevisionTip, REVISION_TIP_THROTTLE_MS);
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
|
|
136
153
|
const delay = (ms, signal) => new Promise((resolve) => {
|
|
137
154
|
if (ms <= 0 || signal?.aborted) {
|
|
138
155
|
resolve();
|
|
@@ -414,6 +431,7 @@ export const createSessionIndexSyncRuntime = ({
|
|
|
414
431
|
};
|
|
415
432
|
|
|
416
433
|
const enqueue = (directories) => {
|
|
434
|
+
if (stopped) return snapshot();
|
|
417
435
|
const nextRuntimeKey = sessionIndexService.getRuntimeKey();
|
|
418
436
|
if (nextRuntimeKey !== runtimeKey) {
|
|
419
437
|
runtimeKey = nextRuntimeKey;
|
|
@@ -427,7 +445,8 @@ export const createSessionIndexSyncRuntime = ({
|
|
|
427
445
|
failedDirectories.clear();
|
|
428
446
|
batchDirectories = new Set();
|
|
429
447
|
}
|
|
430
|
-
|
|
448
|
+
const newBatch = !running && queue.length === 0;
|
|
449
|
+
if (newBatch) {
|
|
431
450
|
completedDirectories.clear();
|
|
432
451
|
failedDirectories.clear();
|
|
433
452
|
batchDirectories = new Set();
|
|
@@ -443,7 +462,7 @@ export const createSessionIndexSyncRuntime = ({
|
|
|
443
462
|
currentWasPreempted = true;
|
|
444
463
|
currentController.abort();
|
|
445
464
|
}
|
|
446
|
-
notify();
|
|
465
|
+
notify(newBatch);
|
|
447
466
|
void run();
|
|
448
467
|
return snapshot();
|
|
449
468
|
};
|
|
@@ -458,7 +477,7 @@ export const createSessionIndexSyncRuntime = ({
|
|
|
458
477
|
|
|
459
478
|
const publishChange = () => {
|
|
460
479
|
if (stopped) return;
|
|
461
|
-
notify();
|
|
480
|
+
notify(true);
|
|
462
481
|
};
|
|
463
482
|
|
|
464
483
|
const waitForChange = (since, { signal, timeoutMs = LONG_POLL_MAX_MS } = {}) => {
|
|
@@ -481,6 +500,7 @@ export const createSessionIndexSyncRuntime = ({
|
|
|
481
500
|
};
|
|
482
501
|
|
|
483
502
|
const stop = () => {
|
|
503
|
+
if (stopped) return;
|
|
484
504
|
stopped = true;
|
|
485
505
|
currentController?.abort();
|
|
486
506
|
const interruptedDirectories = [...queuedDirectories];
|
|
@@ -489,7 +509,7 @@ export const createSessionIndexSyncRuntime = ({
|
|
|
489
509
|
queuedDirectories.clear();
|
|
490
510
|
queuedChildFlagKeys.clear();
|
|
491
511
|
for (const directory of interruptedDirectories) failedDirectories.add(directory);
|
|
492
|
-
notify();
|
|
512
|
+
notify(true);
|
|
493
513
|
};
|
|
494
514
|
|
|
495
515
|
return { enqueue, noteInteractiveRequest, publishChange, snapshot, waitForChange, stop };
|
|
@@ -41,6 +41,127 @@ const isFullyIdle = (runtime) => (
|
|
|
41
41
|
);
|
|
42
42
|
|
|
43
43
|
describe('session index background sync runtime', () => {
|
|
44
|
+
it('coalesces hundreds of directory/child checks and immediately publishes the final revision', async () => {
|
|
45
|
+
vi.useFakeTimers();
|
|
46
|
+
try {
|
|
47
|
+
const service = createService();
|
|
48
|
+
const revisionsAtDelivery = [];
|
|
49
|
+
const tips = vi.fn(() => { revisionsAtDelivery.push(runtime.snapshot().revision); });
|
|
50
|
+
const runtime = createSessionIndexSyncRuntime({
|
|
51
|
+
sessionIndexService: service,
|
|
52
|
+
buildOpenCodeUrl: (route) => `http://opencode.test${route}`,
|
|
53
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
54
|
+
onRevisionTip: tips,
|
|
55
|
+
setTimer: setTimeout,
|
|
56
|
+
clearTimer: clearTimeout,
|
|
57
|
+
fetchFn: async (url) => {
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
59
|
+
const directory = url.searchParams.get('directory');
|
|
60
|
+
return new Response(JSON.stringify({ data: url.searchParams.has('parentID') ? []
|
|
61
|
+
: Array.from({ length: 20 }, (_, i) => session(`${directory}-${i}`, 100 - i, directory)) }));
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
runtime.enqueue(Array.from({ length: 15 }, (_, i) => `/repo/${i}`));
|
|
65
|
+
expect(tips).toHaveBeenCalledTimes(1);
|
|
66
|
+
expect(tips.mock.calls[0][0]).toMatchObject({ revision: 1, sync: { active: true } });
|
|
67
|
+
await vi.advanceTimersByTimeAsync(499);
|
|
68
|
+
expect(tips).toHaveBeenCalledTimes(1);
|
|
69
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
70
|
+
expect(tips).toHaveBeenCalledTimes(2);
|
|
71
|
+
const progress = runtime.waitForChange(runtime.snapshot().revision);
|
|
72
|
+
await vi.advanceTimersByTimeAsync(10);
|
|
73
|
+
expect((await progress).revision).toBe(runtime.snapshot().revision);
|
|
74
|
+
const final = (async () => {
|
|
75
|
+
let value = runtime.snapshot();
|
|
76
|
+
while (value.sync.active || value.sync.enriching) value = await runtime.waitForChange(value.revision);
|
|
77
|
+
return value;
|
|
78
|
+
})();
|
|
79
|
+
await vi.advanceTimersByTimeAsync(2640);
|
|
80
|
+
expect(service.replaceDirectory).toHaveBeenCalledTimes(15);
|
|
81
|
+
expect(service.replaceChildSessions).toHaveBeenCalledTimes(300);
|
|
82
|
+
expect(isFullyIdle(runtime)).toBe(true);
|
|
83
|
+
expect(tips).toHaveBeenCalledTimes(8);
|
|
84
|
+
expect(tips.mock.calls.map(([tip]) => tip.revision)).toEqual(revisionsAtDelivery);
|
|
85
|
+
expect(tips.mock.lastCall[0]).toMatchObject({ revision: runtime.snapshot().revision, sync: { active: false, enriching: false } });
|
|
86
|
+
expect((await final).revision).toBe(runtime.snapshot().revision);
|
|
87
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
88
|
+
await vi.advanceTimersByTimeAsync(500);
|
|
89
|
+
expect(tips).toHaveBeenCalledTimes(8);
|
|
90
|
+
runtime.stop();
|
|
91
|
+
} finally {
|
|
92
|
+
vi.useRealTimers();
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('publishes external changes immediately while busy and stop cancels the pending tip', async () => {
|
|
97
|
+
vi.useFakeTimers();
|
|
98
|
+
try {
|
|
99
|
+
const tips = vi.fn();
|
|
100
|
+
let releaseReady;
|
|
101
|
+
const runtime = createSessionIndexSyncRuntime({
|
|
102
|
+
sessionIndexService: createService(),
|
|
103
|
+
onRevisionTip: tips,
|
|
104
|
+
waitForOpenCodeReady: () => new Promise((resolve) => { releaseReady = resolve; }),
|
|
105
|
+
setTimer: setTimeout,
|
|
106
|
+
clearTimer: clearTimeout,
|
|
107
|
+
});
|
|
108
|
+
runtime.enqueue(['/repo']);
|
|
109
|
+
expect(vi.getTimerCount()).toBe(1);
|
|
110
|
+
runtime.publishChange();
|
|
111
|
+
expect(tips).toHaveBeenCalledTimes(2);
|
|
112
|
+
expect(tips.mock.lastCall[0].revision).toBe(runtime.snapshot().revision);
|
|
113
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
114
|
+
runtime.enqueue(['/repo/second']);
|
|
115
|
+
expect(vi.getTimerCount()).toBe(1);
|
|
116
|
+
const final = runtime.waitForChange(runtime.snapshot().revision);
|
|
117
|
+
runtime.stop();
|
|
118
|
+
const stoppedRevision = runtime.snapshot().revision;
|
|
119
|
+
expect(tips).toHaveBeenCalledTimes(3);
|
|
120
|
+
expect(tips.mock.lastCall[0]).toMatchObject({ revision: stoppedRevision, sync: { active: false, enriching: false } });
|
|
121
|
+
await expect(final).resolves.toMatchObject({ revision: stoppedRevision, sync: { active: false, failedDirectories: ['/repo', '/repo/second'] } });
|
|
122
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
123
|
+
releaseReady(true);
|
|
124
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
125
|
+
expect(runtime.snapshot().revision).toBe(stoppedRevision);
|
|
126
|
+
expect(tips).toHaveBeenCalledTimes(3);
|
|
127
|
+
} finally {
|
|
128
|
+
vi.useRealTimers();
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('stopping during enrichment flushes idle and fences late worker notifications', async () => {
|
|
133
|
+
vi.useFakeTimers();
|
|
134
|
+
try {
|
|
135
|
+
const tips = vi.fn();
|
|
136
|
+
const runtime = createSessionIndexSyncRuntime({
|
|
137
|
+
sessionIndexService: createService(),
|
|
138
|
+
buildOpenCodeUrl: (route) => `http://opencode.test${route}`,
|
|
139
|
+
getOpenCodeAuthHeaders: () => ({}),
|
|
140
|
+
onRevisionTip: tips,
|
|
141
|
+
fetchFn: async (url, { signal }) => {
|
|
142
|
+
if (!url.searchParams.has('parentID')) return new Response(JSON.stringify([session('root', 1)]));
|
|
143
|
+
return new Promise((_resolve, reject) => {
|
|
144
|
+
signal.addEventListener('abort', () => reject(new DOMException('aborted', 'AbortError')), { once: true });
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
runtime.enqueue(['/repo']);
|
|
149
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
150
|
+
expect(runtime.snapshot().sync.enriching).toBe(true);
|
|
151
|
+
const final = runtime.waitForChange(runtime.snapshot().revision);
|
|
152
|
+
runtime.stop();
|
|
153
|
+
const revision = runtime.snapshot().revision;
|
|
154
|
+
await expect(final).resolves.toMatchObject({ revision, sync: { active: false, enriching: false } });
|
|
155
|
+
await vi.advanceTimersByTimeAsync(1000);
|
|
156
|
+
expect(tips).toHaveBeenCalledTimes(2);
|
|
157
|
+
expect(tips.mock.lastCall[0]).toMatchObject({ revision, sync: { active: false, enriching: false } });
|
|
158
|
+
expect(runtime.snapshot().revision).toBe(revision);
|
|
159
|
+
expect(vi.getTimerCount()).toBe(0);
|
|
160
|
+
} finally {
|
|
161
|
+
vi.useRealTimers();
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
44
165
|
it.each([1, 20, 21])('only advertises more when terminal pages contain over 20 visible roots (%i roots)', async (rootCount) => {
|
|
45
166
|
const service = createService();
|
|
46
167
|
const rows = Array.from({ length: Math.max(20, rootCount) }, (_, i) => ({
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/renderElectronMiniChatApp-t1IsnPfc.js","assets/rolldown-runtime-hePW80VL.js","assets/persistence-B4Ni3Gmv.js","assets/runtime-auth-C8w0nlKy.js","assets/runtime-switch-3ad3yzM7.js","assets/runtime-fetch-QCWYlAx0.js","assets/vendor-zustand-o69pgTEK.js","assets/vendor-base-ui-CdzJ6My_.js","assets/useUIStore-CDMI_Yzu.js","assets/vendor-react-BP2cRVqX.js","assets/vendor-pathe-vBmvQE8f.js","assets/vendor-tanstack-query-core-BWz0ose6.js","assets/platform-BQPsr8Zl.js","assets/vendor-opencode-client-MF2GBrFx.js","assets/vendor-aparajita-capacitor-secure-storage-xol_6BsB.js","assets/vendor-reactuses-core-BpfIZ1tg.js","assets/vendor-lodash-es-DfLbG5Af.js","assets/vendor-js-cookie-hSOiFTtX.js","assets/button-BupmKVUs.js","assets/store-CG7Ytsg9.js","assets/vendor-class-variance-authority-BLUaYARZ.js","assets/vendor-tailwind-merge-BicNWLTg.js","assets/useMobileNavigationStore-DXpdkzw_.js","assets/vendor-tanstack-react-query-CdqCAt40.js","assets/vendor-sonner-ZS8XIOVl.js","assets/useAssistantUIStore-Dlvb7P0q.js","assets/vendor-zod-Dzk5kdUU.js","assets/vendor-fuse.js-DB5wDJbI.js","assets/Icon-B4xKSuz3.js","assets/useAppFontEffects-Ba6H7bLU.js","assets/desktopHosts-BZ7iYE-l.js","assets/desktopRelayRestore-S9wEiY2U.js","assets/vendor-pierre-diffs-DPBtNDJ5.js","assets/vendor-hast-util-to-html-Bw_SgZ1t.js","assets/vendor-character-entities-legacy-Wut6vMuj.js","assets/vendor-character-entities-html4-DlN_yAFd.js","assets/vendor-ccount-CyGsA0u7.js","assets/vendor-comma-separated-tokens-B9ouxNNS.js","assets/vendor-oniguruma-to-es-CrHzjjSU.js","assets/vendor-oniguruma-parser-D2DIvhSP.js","assets/vendor-diff-Dq7CZNim.js","assets/vendor-lru_map-BMOrLc9L.js","assets/rasterizePng-CpK-xCkB.js","assets/dualLimitLru-D8Sphgua.js","assets/AgentAvatar-CJQwq8UM.js","assets/vendor-dicebear-core-CV95S5Pg.js","assets/vendor-dicebear-styles-7lEG-JsF.js","assets/useThemeSystem-8jhE4Zyk.js","assets/dialog-BYLD4Cm3.js","assets/gitApi-BtJfaFQy.js","assets/vendor-simplewebauthn-browser-Dwhcdg_5.js","assets/projectMeta-zESSNFVw.js","assets/input-DgrLdIys.js","assets/tooltip-Dbgnq-Mh.js","assets/OverlayScrollbar-DIsrS93m.js","assets/textarea-Boj9axpc.js","assets/vendor-framer-motion-D-oiwsxL.js","assets/vendor-dnd-kit-core-5pNnzVdw.js","assets/vendor-dnd-kit-accessibility-COSwgjFs.js","assets/vendor-dnd-kit-sortable-Dm50I0m7.js","assets/speechSettingsQueries-PsDvs5QG.js","assets/dropdown-menu.styles-HOWSubFF.js","assets/image-loading-CsxDGgek.js","assets/fileQueries-DZR-mK8h.js","assets/useRuntimeAPIs-Cx8S1HLz.js","assets/relay-image-stream-qI2NTXbG.js","assets/image-loading-CObM4tcX.css","assets/sessionChangeSummary-CUMpXSiU.js","assets/LoadingImage-CoT3GWhg.js","assets/ImageLoadingPlaceholder-xB8WeKks.js","assets/sessionChangeSummary-vC84qM3v.css","assets/insertionBoundaries-z2K9a6oY.js","assets/fileMentionTouchSelection-BcqQPEDT.js","assets/appThemeRegistry-DUH4Fprj.js","assets/MarkdownRenderer-TmfHeWHu.js","assets/markdownHeightCache-Dyctmwqv.js","assets/url-Bd61gNgl.js","assets/markdown-worker-DFNZ3KJ_.js","assets/useEffectiveDirectory-wtPL_zdo.js","assets/FadeInOnReveal-BNVExwoL.js","assets/markdownTheme-CV7_0QAL.js","assets/useWorkerHighlightedLines-BXDSKlPX.js","assets/vendor-tanstack-react-virtual-B_EO--9t.js","assets/vendor-cmdk-RDxC-9Iz.js","assets/vendor-aria-hidden-BbL3cEyI.js","assets/sortable-tabs-strip-DogWaIrG.js","assets/vendor-motion-88WXBOBf.js","assets/vendor-legendapp-list-CNxU_ALy.js","assets/useFontPreferences-lAw0Cbza.js","assets/terminalApi-kRMttlcP.js","assets/useDesktopWindowControlsLayout-CbeUwtDM.js","assets/src-CdGit3gf.css"])))=>i.map(i=>d[i]);
|
|
2
|
-
import{t as e}from"./runtimeConfig-5NAbSrPI.js";import{o as t}from"./vendor-aparajita-capacitor-secure-storage-xol_6BsB.js";/* empty css */window.__OPENCHAMBER_RUNTIME_APIS__=e(),t(async()=>{let{renderElectronMiniChatApp:e}=await import(`./renderElectronMiniChatApp-t1IsnPfc.js`);return{renderElectronMiniChatApp:e}},__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91])).then(({renderElectronMiniChatApp:t})=>{t(window.__OPENCHAMBER_RUNTIME_APIS__??e())});
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["assets/renderMobileApp-BrC28EkU.js","assets/rolldown-runtime-hePW80VL.js","assets/runtime-auth-C8w0nlKy.js","assets/runtime-switch-3ad3yzM7.js","assets/runtime-fetch-QCWYlAx0.js","assets/persistence-B4Ni3Gmv.js","assets/vendor-zustand-o69pgTEK.js","assets/vendor-base-ui-CdzJ6My_.js","assets/useUIStore-CDMI_Yzu.js","assets/vendor-react-BP2cRVqX.js","assets/vendor-pathe-vBmvQE8f.js","assets/vendor-tanstack-query-core-BWz0ose6.js","assets/platform-BQPsr8Zl.js","assets/vendor-opencode-client-MF2GBrFx.js","assets/vendor-aparajita-capacitor-secure-storage-xol_6BsB.js","assets/capgoAdapter-KNLHDj7G.js","assets/types-BuiqMmh_.js","assets/vendor-pierre-diffs-DPBtNDJ5.js","assets/vendor-hast-util-to-html-Bw_SgZ1t.js","assets/vendor-character-entities-legacy-Wut6vMuj.js","assets/vendor-character-entities-html4-DlN_yAFd.js","assets/vendor-ccount-CyGsA0u7.js","assets/vendor-comma-separated-tokens-B9ouxNNS.js","assets/vendor-oniguruma-to-es-CrHzjjSU.js","assets/vendor-oniguruma-parser-D2DIvhSP.js","assets/vendor-diff-Dq7CZNim.js","assets/vendor-lru_map-BMOrLc9L.js","assets/vendor-sonner-ZS8XIOVl.js","assets/vendor-reactuses-core-BpfIZ1tg.js","assets/vendor-lodash-es-DfLbG5Af.js","assets/vendor-js-cookie-hSOiFTtX.js","assets/button-BupmKVUs.js","assets/store-CG7Ytsg9.js","assets/vendor-class-variance-authority-BLUaYARZ.js","assets/vendor-tailwind-merge-BicNWLTg.js","assets/MobileShareBridge-pYQFFYmm.js","assets/MobileDetailNavigation-CtGncmBY.js","assets/vendor-tanstack-react-query-CdqCAt40.js","assets/useMobileNavigationStore-DXpdkzw_.js","assets/useAssistantUIStore-Dlvb7P0q.js","assets/vendor-zod-Dzk5kdUU.js","assets/vendor-fuse.js-DB5wDJbI.js","assets/Icon-B4xKSuz3.js","assets/rasterizePng-CpK-xCkB.js","assets/AgentAvatar-CJQwq8UM.js","assets/vendor-dicebear-core-CV95S5Pg.js","assets/vendor-dicebear-styles-7lEG-JsF.js","assets/dualLimitLru-D8Sphgua.js","assets/useThemeSystem-8jhE4Zyk.js","assets/dialog-BYLD4Cm3.js","assets/gitApi-BtJfaFQy.js","assets/useAppFontEffects-Ba6H7bLU.js","assets/desktopHosts-BZ7iYE-l.js","assets/desktopRelayRestore-S9wEiY2U.js","assets/vendor-simplewebauthn-browser-Dwhcdg_5.js","assets/projectMeta-zESSNFVw.js","assets/input-DgrLdIys.js","assets/tooltip-Dbgnq-Mh.js","assets/OverlayScrollbar-DIsrS93m.js","assets/textarea-Boj9axpc.js","assets/vendor-framer-motion-D-oiwsxL.js","assets/vendor-dnd-kit-core-5pNnzVdw.js","assets/vendor-dnd-kit-accessibility-COSwgjFs.js","assets/vendor-dnd-kit-sortable-Dm50I0m7.js","assets/speechSettingsQueries-PsDvs5QG.js","assets/dropdown-menu.styles-HOWSubFF.js","assets/image-loading-CsxDGgek.js","assets/fileQueries-DZR-mK8h.js","assets/useRuntimeAPIs-Cx8S1HLz.js","assets/relay-image-stream-qI2NTXbG.js","assets/image-loading-CObM4tcX.css","assets/sessionChangeSummary-CUMpXSiU.js","assets/LoadingImage-CoT3GWhg.js","assets/ImageLoadingPlaceholder-xB8WeKks.js","assets/sessionChangeSummary-vC84qM3v.css","assets/insertionBoundaries-z2K9a6oY.js","assets/fileMentionTouchSelection-BcqQPEDT.js","assets/appThemeRegistry-DUH4Fprj.js","assets/MarkdownRenderer-TmfHeWHu.js","assets/markdownHeightCache-Dyctmwqv.js","assets/url-Bd61gNgl.js","assets/markdown-worker-DFNZ3KJ_.js","assets/useEffectiveDirectory-wtPL_zdo.js","assets/FadeInOnReveal-BNVExwoL.js","assets/markdownTheme-CV7_0QAL.js","assets/useWorkerHighlightedLines-BXDSKlPX.js","assets/vendor-tanstack-react-virtual-B_EO--9t.js","assets/vendor-cmdk-RDxC-9Iz.js","assets/vendor-aria-hidden-BbL3cEyI.js","assets/sortable-tabs-strip-DogWaIrG.js","assets/vendor-motion-88WXBOBf.js","assets/vendor-legendapp-list-CNxU_ALy.js","assets/useFontPreferences-lAw0Cbza.js","assets/terminalApi-kRMttlcP.js","assets/MobileTabPageHeader-D7NAfatx.js","assets/vendor-codemirror-autocomplete-7c2L_4dM.js","assets/vendor-codemirror-lang-angular-DXOgWEVj.js","assets/vendor-codemirror-lang-json-mLulNbwB.js","assets/vendor-codemirror-lang-markdown-CV1IO6Qs.js","assets/vendor-codemirror-lang-python-YM2WVLHF.js","assets/vendor-codemirror-legacy-modes-DwltVFbT.js","assets/mcpQueries-BSwb38Pz.js","assets/collapsible-BAPWS6G-.js","assets/vendor-codemirror-commands-B8KrBQ9B.js","assets/vendor-codemirror-search-ZqtrPLjU.js","assets/vendor-replit-codemirror-vim-DDj0dSVY.js","assets/vendor-codemirror-language-data-Ctd4WxgW.js","assets/projectActions-CWvNGKfA.js","assets/runtimeEndpointReset-CxreCHBU.js","assets/scheduledTaskNavigation-C1PrFEhV.js","assets/sessionLookup-DD8YnthC.js","assets/vendor-cron-parser-BmO80h0C.js","assets/runtimeEndpointReset-ByXtaeWD.css","assets/SettingsView-D7Ykm-CM.js","assets/vendor-yaml-C9KlJAlj.js","assets/vendor-qrcode-D7jBCyl-.js","assets/vendor-dijkstrajs-BAzPDGZM.js","assets/AssistantView-j9aI8OWI.js","assets/assistant-attachment-cache-Bzewqov4.js","assets/src-CdGit3gf.css"])))=>i.map(i=>d[i]);
|
|
2
|
-
import{t as e}from"./runtimeConfig-5NAbSrPI.js";import{o as t}from"./vendor-aparajita-capacitor-secure-storage-xol_6BsB.js";/* empty css */window.__OPENCHAMBER_RUNTIME_APIS__=e(),t(async()=>{let{renderMobileApp:e}=await import(`./renderMobileApp-BrC28EkU.js`);return{renderMobileApp:e}},__vite__mapDeps([0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119])).then(({renderMobileApp:t})=>{t(window.__OPENCHAMBER_RUNTIME_APIS__??e())});
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{n as e}from"./rolldown-runtime-hePW80VL.js";var t=`openchamber-relay-v1`,n=65526,r={HttpRequest:1,HttpBody:2,HttpResponse:3,StreamEnd:4,StreamAbort:5,WsOpen:6,WsOpened:7,WsText:8,WsBinary:9,WsClose:10,Ping:11,Pong:12},i=new Set(Object.values(r)),a=e=>i.has(e),o={ControlReplaced:4001,DuplicateClient:4002,StuckControlReset:4003,HostUnavailable:4008,AuthFailed:4010,LimitExceeded:4029,HostWentAway:1012,RekeyMismatch:1008,ChannelFailure:1011},s=globalThis.crypto.subtle,c={name:`ECDH`,namedCurve:`P-256`},l=16,u=32,d=16,f=4,p=8,m=class extends Error{constructor(e){super(e),this.name=`RelayCryptoError`}},h=()=>s.generateKey(c,!0,[`deriveBits`]),g=async e=>{let t=await s.exportKey(`jwk`,e);return{kty:t.kty,crv:t.crv,x:t.x,y:t.y}},_=async e=>{if(e.kty!==`EC`||e.crv!==`P-256`||typeof e.x!=`string`||typeof e.y!=`string`)throw new m(`invalid ECDH public key JWK`);try{return await s.importKey(`jwk`,{kty:e.kty,crv:e.crv,x:e.x,y:e.y,ext:!0},c,!0,[])}catch{throw new m(`invalid ECDH public key JWK`)}},v=()=>{let e=new Uint8Array(l);return globalThis.crypto.getRandomValues(e),e},y=async(e,n,r)=>{if(r.length!==l)throw new m(`invalid handshake nonce length`);let i=await s.deriveBits({name:`ECDH`,public:n},e,256),a=await s.importKey(`raw`,i,`HKDF`,!1,[`deriveBits`]),o=new Uint8Array(await s.deriveBits({name:`HKDF`,hash:`SHA-256`,salt:r,info:new TextEncoder().encode(t)},a,512)),c=(e,t)=>s.importKey(`raw`,e,{name:`AES-GCM`},!1,t);return{clientToHost:await c(o.slice(0,u),[`encrypt`,`decrypt`]),hostToClient:await c(o.slice(u),[`encrypt`,`decrypt`])}},b=(e,t,n)=>{for(let r=7;r>=0;--r)e[t+r]=Number(n&255n),n>>=8n},x=(e,t)=>{let n=0n;for(let r=0;r<p;r+=1)n=n<<8n|BigInt(e[t+r]);return n},S=e=>{let t=new Uint8Array(f);globalThis.crypto.getRandomValues(t);let n=0n;return{async encrypt(r){if(r.length>65536)throw new m(`plaintext frame exceeds maximum size`);n+=1n;let i=new Uint8Array(12);i.set(t,0),b(i,f,n);let a=new Uint8Array(await s.encrypt({name:`AES-GCM`,iv:i},e,r)),o=new Uint8Array(13+a.length);return o[0]=1,o.set(i,1),o.set(a,13),o}}},C=e=>{let t=0n;return{async decrypt(n){if(n.length<13+d)throw new m(`encrypted frame too short`);if(n[0]!==1)throw new m(`unsupported encrypted frame version`);let r=n.slice(1,13),i=x(r,f);if(i<=t)throw new m(`frame counter regression`);let a;try{a=await s.decrypt({name:`AES-GCM`,iv:r},e,n.slice(13))}catch{throw new m(`frame decryption failed`)}return t=i,new Uint8Array(a)}}},w=`ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_`,ee=e=>{let t=``;for(let n=0;n<e.length;n+=3){let r=e[n],i=n+1<e.length?e[n+1]:void 0,a=n+2<e.length?e[n+2]:void 0;t+=w[r>>2],t+=w[(r&3)<<4|(i??0)>>4],i!==void 0&&(t+=w[(i&15)<<2|(a??0)>>6]),a!==void 0&&(t+=w[a&63])}return t},te=e=>{let t;try{t=JSON.parse(e)}catch{return null}if(typeof t!=`object`||!t)return null;let n=t;if(n.v!==1)return null;let r=n.batch===!0;return n.t===`ready`?{t:`ready`,v:1,batch:r}:n.t===`hello`&&typeof n.nonce==`string`&&typeof n.clientPubJwk==`object`&&n.clientPubJwk!==null?{t:`hello`,v:1,clientPubJwk:n.clientPubJwk,nonce:n.nonce,batch:r}:null},ne=e=>({type:`fail`,closeCode:o.ChannelFailure,reason:e}),re=async(e,t={})=>{let n=t.batch!==!1,r=await _(e),i=await h(),a=v(),o={t:`hello`,v:1,clientPubJwk:await g(i.publicKey),nonce:ee(a),...n?{batch:!0}:{}},s=!1;return{helloText:JSON.stringify(o),get established(){return s},async handleText(e){if(s)return te(e)?.t===`ready`?{type:`ignore`}:ne(`plaintext frame on established channel`);let t=te(e);if(t?.t!==`ready`)return{type:`ignore`};let o=await y(i.privateKey,r,a);return s=!0,{type:`established`,batch:n&&t.batch===!0,channel:{encryptor:S(o.clientToHost),decryptor:C(o.hostToClient)}}}}},T=4294967295,E=class extends Error{constructor(e){super(e),this.name=`TunnelCodecError`}},D=(e,t,n,r=!1)=>{if(!Number.isInteger(t)||t<0||t>T)throw new E(`invalid stream id`);if(n.length>65526)throw new E(`tunnel payload exceeds maximum size`);let i=new Uint8Array(5+n.length);return i[0]=r?e|128:e,i[1]=t>>>24&255,i[2]=t>>>16&255,i[3]=t>>>8&255,i[4]=t&255,i.set(n,5),i},ie=e=>{if(e.length<5)throw new E(`tunnel frame too short`);let t=e[0],n=!!(t&128),r=t&-129;if(!a(r))throw new E(`unknown tunnel frame type ${r}`);return{frameType:r,streamId:(e[1]<<24|e[2]<<16|e[3]<<8|e[4])>>>0,payload:e.slice(5),hasMoreFragments:n}},O=new TextEncoder,k=new TextDecoder,A=e=>O.encode(JSON.stringify(e)),j=(e,t)=>{let n;try{n=JSON.parse(k.decode(e))}catch{throw new E(`malformed JSON tunnel payload`)}if(!t(n))throw new E(`unexpected JSON tunnel payload shape`);return n},M=(e,t=n)=>{if(t<=0||t>65526)throw new E(`invalid chunk size`);if(e.length===0)return[new Uint8Array];let r=[];for(let n=0;n<e.length;n+=t)r.push(e.slice(n,n+t));return r},N=(e,t,n)=>{let r=M(n);return r.map((n,i)=>D(e,t,n,i<r.length-1))},ae=(e=16777216)=>{let t=new Map;return{push(n){let r=`${n.streamId}:${n.frameType}`,i=t.get(r);if(!n.hasMoreFragments&&!i)return n.payload;let a=i?.chunks??[],o=(i?.totalBytes??0)+n.payload.length;if(o>e)throw t.delete(r),new E(`fragmented message exceeds maximum size`);if(a.push(n.payload),n.hasMoreFragments)return t.set(r,{chunks:a,totalBytes:o}),null;t.delete(r);let s=new Uint8Array(o),c=0;for(let e of a)s.set(e,c),c+=e.length;return s},dropStream(e){for(let n of t.keys())n.startsWith(`${e}:`)&&t.delete(n)}}},oe=e=>{if(e.length===0)throw new E(`cannot encode an empty frame batch`);if(e.length===1){let t=e[0],n=new Uint8Array(1+t.length);if(n[0]=0,n.set(t,1),n.length>65536)throw new E(`frame batch exceeds maximum plaintext size`);return n}let t=1;for(let n of e)t+=4+n.length;if(t>65536)throw new E(`frame batch exceeds maximum plaintext size`);let n=new Uint8Array(t);n[0]=1;let r=1;for(let t of e)n[r]=t.length>>>24&255,n[r+1]=t.length>>>16&255,n[r+2]=t.length>>>8&255,n[r+3]=t.length&255,r+=4,n.set(t,r),r+=t.length;return n},se=e=>{if(e.length<1)throw new E(`empty batch plaintext`);let t=e[0];if(t===0)return[e.slice(1)];if(t!==1)throw new E(`unknown batch container tag ${t}`);let n=[],r=1;for(;r<e.length;){if(r+4>e.length)throw new E(`truncated batch frame length`);let t=(e[r]<<24|e[r+1]<<16|e[r+2]<<8|e[r+3])>>>0;if(r+=4,r+t>e.length)throw new E(`truncated batch frame body`);n.push(e.slice(r,r+t)),r+=t}if(n.length===0)throw new E(`empty frame batch`);return n},ce=new Set([r.HttpBody,r.WsText,r.WsBinary]),le=e=>{let t=e.windowMs??150,n=e.maxBatchBytes??24576,r=e.maxBatchFrames??32,i=e.now??(()=>Date.now()),a=e.setTimer??((e,t)=>setTimeout(e,t)),o=e.clearTimer??(e=>clearTimeout(e)),s=[],c=0,l=null,u=0,d=!1,f=()=>{l!==null&&(o(l),l=null)},p=()=>{if(f(),s.length===0)return;let t=s;s=[],c=0,u=i(),e.sendBatch(oe(t))};return{enqueue:e=>{if(d)return;let o=e[0]&-129;if(!ce.has(o)){s.push(e),p();return}let f=i();if(s.length===0&&f-u>=t){s.push(e),p();return}let m=4+e.length;if(s.length>0&&1+c+m>65536&&p(),s.push(e),c+=m,c>=n||s.length>=r){p();return}l===null&&(l=a(p,t))},flush:p,dispose(){d=!0,f(),s=[],c=0}}},ue=()=>{let e=1;return{next(){if(e>T)throw new E(`stream id space exhausted`);let t=e;return e+=2,t}}},P=e=>typeof e==`object`&&!!e&&!Array.isArray(e),F=e=>P(e)&&Object.values(e).every(e=>typeof e==`string`),de=e=>P(e)&&typeof e.status==`number`&&F(e.headers),I=e=>P(e)&&typeof e.reason==`string`,fe=e=>P(e)&&typeof e.code==`number`&&typeof e.reason==`string`,pe=/^[a-z][a-z\d+.-]*:\/\//i,L=`http://tunnel.invalid`,me=e=>{try{let t=pe.test(e)?new URL(e):new URL(e,L);return`${t.pathname}${t.search}`}catch{return e}},R=e=>({async*[Symbol.asyncIterator](){yield e}}),he=e=>({async*[Symbol.asyncIterator](){let t=e.getReader();try{for(;;){let{done:e,value:n}=await t.read();if(e)return;n&&(yield n)}}finally{t.releaseLock()}}}),ge=e=>{let t=new Uint8Array(e.byteLength);return t.set(new Uint8Array(e.buffer,e.byteOffset,e.byteLength)),t},_e=async e=>{if(e==null)return{body:null};if(e instanceof ReadableStream)return{body:he(e)};if(typeof e==`string`)return{body:R(new TextEncoder().encode(e))};if(e instanceof ArrayBuffer)return{body:R(new Uint8Array(e.slice(0)))};if(ArrayBuffer.isView(e))return{body:R(ge(e))};let t=new Response(e),n=t.headers.get(`content-type`)??void 0;return{body:R(new Uint8Array(await t.arrayBuffer())),contentType:n}},ve=async(e,t)=>{let n,r=new Headers,i=`GET`,a=null,o;e instanceof Request?(n=e.url,i=e.method,e.headers.forEach((e,t)=>r.set(t,e)),o=e.signal,a=e.body):n=e.toString(),t&&(t.method&&(i=t.method),t.headers&&new Headers(t.headers).forEach((e,t)=>r.set(t,e)),t.body!==void 0&&(a=t.body),t.signal&&(o=t.signal));let s=pe.test(n)?new URL(n):new URL(n,L),{body:c,contentType:l}=await _e(a);l&&!r.has(`content-type`)&&r.set(`content-type`,l);let u={};return r.forEach((e,t)=>{u[t]=e}),{method:i.toUpperCase(),path:s.pathname,query:s.search.startsWith(`?`)?s.search.slice(1):s.search,headers:u,body:c,signal:o}},ye=new Uint8Array,be=new TextEncoder,xe=new TextDecoder,z=e=>e instanceof Error?e:Error(String(e)),B=()=>new DOMException(`The operation was aborted.`,`AbortError`),Se=e=>{e.binaryType=`arraybuffer`;let t={get readyState(){return e.readyState},send(t){e.send(t)},close(t,n){e.close(t,n)},onopen:null,onmessage:null,onclose:null,onerror:null};return e.onopen=()=>t.onopen?.(),e.onmessage=e=>t.onmessage?.({data:e.data}),e.onclose=e=>t.onclose?.({code:e.code,reason:e.reason}),e.onerror=()=>t.onerror?.(),t},Ce=e=>{let t={get readyState(){return e.readyState},get binaryType(){return e.binaryType},set binaryType(t){t&&(e.binaryType=t)},onopen:null,onmessage:null,onerror:null,onclose:null,send(t){e.send(t)},close(t,n){e.close(t,n)}};return e.onopen=()=>t.onopen?.(),e.onmessage=e=>{let n=e.data;(typeof n==`string`||n instanceof ArrayBuffer)&&t.onmessage?.({data:n})},e.onerror=()=>t.onerror?.(),e.onclose=e=>t.onclose?.({code:e.code,reason:e.reason}),t},we=0,Te=1,Ee=2,De=3,Oe=new Set([o.AuthFailed,o.DuplicateClient,o.LimitExceeded]),ke={[o.AuthFailed]:`relay authentication failed`,[o.DuplicateClient]:`relay connection replaced by another client`,[o.LimitExceeded]:`relay connection limit reached`},Ae=()=>{let e=typeof navigator<`u`&&navigator.onLine===!1,t=typeof document<`u`&&document.visibilityState===`hidden`;return e||t},je=e=>{let t=e.helloRetryMs??1e3,n=e.helloTimeoutMs??3e4,i=e.pingIntervalMs??3e4,a=e.pingTimeoutMs??15e3,o=e.batchWindowMs??150,s=e.batch!==!1,c=e.reconnectBaseDelayMs??1e3,l=e.reconnectMaxDelayMs??3e4,u=e.hiddenOrOfflineMaxDelayMs??6e4,d=e.createWireSocket??(e=>Se(new WebSocket(e))),f=!1,p={state:`idle`},m=new Set,h=null,g=null,_=null,v=0,y=0,b=null,x=[],S=!1,C=e=>{if(p.state!==e.state||p.lastError!==e.lastError){p=e;for(let e of m)try{e(p)}catch{}}},w=e=>{let t=x;x=[];for(let n of t)n.reject(e)},ee=e=>{let t=x;x=[];for(let n of t)n.resolve(e)},te=(e,t)=>{e.dead=!0;let n=Array.from(e.streams.values());e.streams.clear();for(let e of n)try{e.fail(t)}catch{}},ne=()=>{b!==null&&(clearTimeout(b),b=null)},T=()=>{f||b===null||Ae()||(ne(),k(),P())},E=()=>{typeof document<`u`&&document.visibilityState===`visible`&&T()},O=()=>{S||typeof window>`u`||(S=!0,window.addEventListener(`online`,T),typeof document<`u`&&document.addEventListener(`visibilitychange`,E))},k=()=>{!S||typeof window>`u`||(S=!1,window.removeEventListener(`online`,T),typeof document<`u`&&document.removeEventListener(`visibilitychange`,E))},oe=()=>{if(f||b!==null)return;let e=Math.max(0,y-1),t=c*2**Math.min(e,10),n=Ae()?u:l,r=Math.min(n,t);O(),b=setTimeout(()=>{b=null,k(),P()},r)},ce=()=>{let t=new URL(e.relayUrl);return t.searchParams.set(`v`,`1`),t.searchParams.set(`role`,`client`),t.searchParams.set(`serverId`,e.serverId),e.grant&&t.searchParams.set(`grant`,e.grant),t.toString()},P=async()=>{if(f)return;ne(),v+=1;let c=v;C({state:y>0?`reconnecting`:`connecting`,lastError:p.lastError});let l;try{l=await re(e.hostEncPubJwk,{batch:s})}catch(e){if(c!==v||f)return;fe(c,z(e),!0);return}if(c!==v||f)return;let u;try{u=d(ce())}catch(e){fe(c,z(e));return}g=u;let m=!1,b=null,x=null,S=null,T=null,E=Promise.resolve(),O=null,k=null,A=!1,j=null,M=Date.now(),N=()=>{b!==null&&(clearInterval(b),b=null),x!==null&&(clearTimeout(x),x=null),S!==null&&(clearInterval(S),S=null),T!==null&&(clearTimeout(T),T=null),j!==null&&(j.dispose(),j=null)};_=N;function P(e,t=!1,n=!1){if(!(m||c!==v)){m=!0,N(),O&&(h=null,te(O,Error(`relay tunnel reset: ${e.message}`))),w(e);try{u.close()}catch{}if(g===u&&(g=null),!f){if(y+=1,n){C({state:`error`,lastError:e.message});return}C({state:t?`error`:`reconnecting`,lastError:e.message}),oe()}}}let F=()=>{try{u.send(l.helloText)}catch{}},de=(e,t)=>{k=e,A=t,b!==null&&(clearInterval(b),b=null),x!==null&&(clearTimeout(x),x=null);let n=new Map,s=ue(),c=ae(),l=Promise.resolve(),d=t=>{l=l.then(async()=>{if(p.dead)return;let n=await e.encryptor.encrypt(t);u.send(n)}).catch(()=>{})},f=t?le({windowMs:o,sendBatch:d}):null;j=f;let p={streams:n,assembler:c,nextStreamId:()=>s.next(),dead:!1,send(e){if(p.dead)return;let t=e[0]&-129;t!==r.Ping&&t!==r.Pong&&(M=Date.now()),f?f.enqueue(e):d(e)}};O=p,h=p,y=0,M=Date.now(),C({state:`connected`}),ee(p),S=setInterval(()=>{Date.now()-M<i||(p.send(D(r.Ping,0,ye)),T===null&&(T=setTimeout(()=>{T=null,P(Error(`relay keepalive timeout`))},a)))},i)},I=(e,t)=>{let n;try{n=ie(t)}catch(e){P(z(e));return}if(T!==null&&(clearTimeout(T),T=null),n.frameType===r.Ping){e.send(D(r.Pong,n.streamId,ye));return}if(n.frameType===r.Pong)return;M=Date.now();let i=n.payload;if(n.frameType===r.WsText||n.frameType===r.WsBinary){let t;try{t=e.assembler.push(n)}catch(e){P(z(e));return}if(t===null)return;i=t}else if(n.hasMoreFragments){P(Error(`unexpected fragmented tunnel frame`));return}let a=e.streams.get(n.streamId);a&&a.handleFrame(n.frameType,i)};u.onopen=()=>{m||c!==v||(F(),b=setInterval(F,t))},u.onmessage=e=>{if(m||c!==v)return;let t=e.data;if(typeof t==`string`){E=E.then(async()=>{if(m||c!==v)return;let e=await l.handleText(t);if(e.type===`established`){if(k)return;de(e.channel,e.batch)}else e.type===`fail`&&P(Error(`relay handshake failed: ${e.reason}`))}).catch(e=>{P(z(e))});return}let n=t instanceof ArrayBuffer?new Uint8Array(t):t instanceof Uint8Array?t:null;n&&(E=E.then(async()=>{if(m||c!==v)return;let e=O,t=k;if(!e||!t){P(Error(`encrypted frame before handshake completed`));return}let r;try{r=await t.decryptor.decrypt(n)}catch(e){P(z(e));return}if(A){let t;try{t=se(r)}catch(e){P(z(e));return}for(let n of t){if(m||c!==v||e.dead)return;I(e,n)}return}I(e,r)}).catch(e=>{P(z(e))}))},u.onclose=e=>{let t=Oe.has(e.code);P(Error(ke[e.code]??`relay socket closed (code ${e.code})`),t,t)},u.onerror=()=>{},x=setTimeout(()=>{x=null,P(Error(`relay handshake timeout`),!0)},n);function fe(e,t,n=!1){e!==v||f||(w(t),y+=1,C({state:n?`error`:`reconnecting`,lastError:t.message}),oe())}},F=e=>f?Promise.reject(Error(`relay tunnel closed`)):e?.aborted?Promise.reject(B()):h&&!h.dead?Promise.resolve(h):new Promise((t,n)=>{let r=null,i={resolve(n){r&&e&&e.removeEventListener(`abort`,r),t(n)},reject(t){r&&e&&e.removeEventListener(`abort`,r),n(t)}};e&&(r=()=>{x=x.filter(e=>e!==i),n(B())},e.addEventListener(`abort`,r,{once:!0})),x.push(i)}),pe=async(e,t)=>{let n=await ve(e,t),i=n.signal;if(i?.aborted)throw B();let a=await F(i),o=a.nextStreamId();return await new Promise((e,t)=>{let s=!1,c=!1,l=null,u=null,d=()=>{a.streams.delete(o),a.assembler.dropStream(o),u&&i&&i.removeEventListener(`abort`,u)},f=e=>{if(!c){if(c=!0,d(),!s){t(e);return}try{l?.error(e)}catch{}}},p=e=>{a.dead||a.send(D(r.StreamAbort,o,A({reason:e})))};u=()=>{p(`aborted`),f(B())},a.streams.set(o,{handleFrame(t,n){if(t===r.HttpResponse){if(s||c)return;let t;try{t=j(n,de)}catch(e){p(`malformed response head`),f(z(e));return}let r=t.status===204||t.status===205||t.status===304,i=null;r||(i=new ReadableStream({start(e){l=e},cancel(){c||(c=!0,d(),p(`response body cancelled`))}})),s=!0,e(new Response(i,{status:t.status,headers:t.headers})),r&&(c=!0,d());return}if(t===r.HttpBody){if(!s||c)return;try{l?.enqueue(n)}catch{}return}if(t===r.StreamEnd){if(c)return;if(!s){f(Error(`tunnel stream ended before response head`));return}c=!0,d();try{l?.close()}catch{}return}if(t===r.StreamAbort){let e=`stream aborted by host`;try{e=j(n,I).reason}catch{}f(Error(e))}},fail(e){f(e)}}),i&&i.addEventListener(`abort`,u,{once:!0});let m={method:n.method,path:n.path,query:n.query,headers:n.headers};a.send(D(r.HttpRequest,o,A(m))),(async()=>{try{if(n.body)for await(let e of n.body){if(c||a.dead)return;for(let t of M(e))a.send(D(r.HttpBody,o,t))}!c&&!a.dead&&a.send(D(r.StreamEnd,o,ye))}catch(e){p(`request body failed`),f(z(e))}})()})},L=e=>{let t=e.indexOf(`?`);return t===-1?{path:e,query:``}:{path:e.slice(0,t),query:e.slice(t+1)}};return P(),{fetch:pe,openWebSocket:(e,t,n)=>{let i=we,a=null,o=0,s=!1,c={get readyState(){return i},onopen:null,onmessage:null,onerror:null,onclose:null,send(e){if(i!==Te||!a||a.dead)throw Error(`relay tunnel socket is not open`);if(typeof e==`string`){for(let t of N(r.WsText,o,be.encode(e)))a.send(t);return}let t=e instanceof ArrayBuffer?new Uint8Array(e.slice(0)):(()=>{let t=new Uint8Array(e.byteLength);return t.set(new Uint8Array(e.buffer,e.byteOffset,e.byteLength)),t})();for(let e of N(r.WsBinary,o,t))a.send(e)},close(e=1e3,t=``){i!==De&&i!==Ee&&(i===Te&&a&&!a.dead&&(i=Ee,a.send(D(r.WsClose,o,A({code:e,reason:t})))),l(e,t))}},l=(e,t,n=!1)=>{if(!s){if(s=!0,a&&(a.streams.delete(o),a.assembler.dropStream(o)),i=De,n)try{c.onerror?.()}catch{}try{c.onclose?.({code:e,reason:t})}catch{}}};return(async()=>{let u;try{u=await F()}catch(e){l(1006,z(e).message,!0);return}if(s)return;a=u,o=u.nextStreamId(),u.streams.set(o,{handleFrame(e,t){if(e===r.WsOpened){if(i===we){i=Te;try{c.onopen?.()}catch{}}return}if(e===r.WsText){try{c.onmessage?.({data:xe.decode(t)})}catch{}return}if(e===r.WsBinary){let e=new ArrayBuffer(t.byteLength);new Uint8Array(e).set(t);try{c.onmessage?.({data:e})}catch{}return}if(e===r.WsClose){let e=1e3,n=``;try{let r=j(t,fe);e=r.code,n=r.reason}catch{}l(e,n);return}if(e===r.StreamAbort){let e=`stream aborted`;try{e=j(t,I).reason}catch{}l(1006,e,!0)}},fail(e){l(1012,e.message,!0)}});let{path:d,query:f}=L(e),p={path:d,query:f,...t&&t.length>0?{protocols:t}:{},...n&&Object.keys(n).length>0?{headers:n}:{}};u.send(D(r.WsOpen,o,A(p)))})(),c},getStatus:()=>p,subscribeStatus(e){return m.add(e),()=>{m.delete(e)}},close:()=>{if(f)return;f=!0,v+=1,ne(),k(),_?.(),_=null;let e=h;h=null;let t=Error(`relay tunnel closed`);e&&te(e,t),w(t);try{g?.close()}catch{}g=null,C({state:`idle`})}}},V=null,H=null,Me=(e,t)=>e.relayUrl===t.relayUrl&&e.serverId===t.serverId&&e.grant===t.grant&&JSON.stringify(e.hostEncPubJwk)===JSON.stringify(t.hostEncPubJwk),Ne=()=>V,Pe=()=>H,Fe=()=>V!==null,Ie=()=>V?.getStatus().state===`connected`,Le=e=>V&&H&&Me(H,e)?V:(V?.close(),H=e,V=je(e),V),Re=(e,t)=>{V!==t&&(V?.close(),H=e,V=t)},ze=()=>{V?.close(),V=null,H=null},Be=e({acquireRuntimeUrlAuthToken:()=>kt,beginRuntimeAuthEndpointSwitch:()=>ot,buildRuntimeAuthHeaders:()=>jt,clearRuntimeUrlAuthToken:()=>X,endRuntimeAuthEndpointSwitch:()=>st,getLocalRuntimeUrlAuthTokenSync:()=>_t,getRuntimeAuthGeneration:()=>Ge,getRuntimeBearerTokenSync:()=>Z,getRuntimeExtraHeadersSync:()=>ft,getRuntimeUrlAuthTokenSync:()=>gt,invalidateRuntimeAuthSession:()=>lt,refreshLocalRuntimeUrlAuthToken:()=>St,refreshRuntimeUrlAuthToken:()=>xt,setRuntimeBearerToken:()=>ut,setRuntimeExtraHeaders:()=>dt,setRuntimeUrlAuthToken:()=>pt,subscribeRuntimeAuthGeneration:()=>ct,subscribeRuntimeUrlAuthToken:()=>At}),Ve=()=>null,He=``,Ue={},U=``,W=0,G=null,K=``,q=0,J=null,Y=0,We=1e4,Ge=()=>Y,Ke=e=>e.toLowerCase()===`authorization`,qe=e=>{let t={};for(let[n,r]of Object.entries(e||{})){let e=n.trim(),i=r.trim();e&&i&&!Ke(e)&&(t[e]=i)}return t},Je=(e,t)=>{let n=Object.entries(e);return n.length===Object.keys(t).length&&n.every(([e,n])=>t[e]===n)},Ye=e=>typeof e==`string`?e.trim():``,Xe=()=>{if(typeof window>`u`)return``;let e=window.__OPENCHAMBER_CLIENT_TOKEN__;return Ye(e)},Ze=()=>{if(typeof window>`u`)return``;let e=window.__OPENCHAMBER_API_BASE_URL__;return typeof e==`string`?e.trim():``},Qe=(e,t)=>{let n=typeof e==`string`&&e.trim()?e.trim():Ze();if(!n)return t;try{return new URL(t,`${n.replace(/\/+$/,``)}/`).toString()}catch{return t}},X=()=>{U=``,W=0,K=``,q=0},$e=new Set,et=null,tt=0,nt=async e=>{let t=new TextEncoder().encode(e),n=await crypto.subtle.digest(`SHA-256`,t);return[...new Uint8Array(n)].map(e=>e.toString(16).padStart(2,`0`)).join(``)},rt=e=>{for(let t of $e)try{t(e)}catch{}},it=e=>e||(tt>0?`endpoint-switch`:`credential`),at=(e={})=>{let t=e.previousBearerDigest===void 0?et??void 0:e.previousBearerDigest||void 0;Y+=1,G=null,X(),Ot(),rt({generation:Y,reason:it(e.reason),...t?{previousBearerDigest:t}:{}})},ot=()=>{tt+=1},st=()=>{tt=Math.max(0,tt-1)},ct=e=>($e.add(e),()=>{$e.delete(e)}),lt=()=>{at({reason:`invalidate`})},ut=e=>{let t=Ye(e),n=t===Z(),r=et;He=t,Ve=()=>t?{type:`bearer`,token:t}:null,!n&&(et=null,at({previousBearerDigest:r}),t&&nt(t).then(e=>{Z()===t&&(et=e)}).catch(()=>void 0))},dt=e=>{let t=qe(e);Je(Ue,t)||(Ue=t,at({previousBearerDigest:null}))},ft=()=>{if(Object.keys(Ue).length>0)return Ue;if(typeof window>`u`)return{};let e=window.__OPENCHAMBER_RUNTIME_HEADERS__;return e&&typeof e==`object`?qe(e):{}},Z=()=>He||Xe(),pt=(e,t)=>{let n=Ye(e);if(!n||typeof t!=`number`||!Number.isFinite(t)){X();return}let r=U;U=n,W=t,r&&r!==n&&Et()},mt=()=>!U||W<=Date.now()+We?(X(),``):U,ht=()=>!K||q<=Date.now()+We?(K=``,q=0,``):K,gt=()=>{let e=mt();return!e&&(Z()||typeof window<`u`)&&xt().catch(()=>{}),e},_t=e=>{let t=ht();return!t&&e&&typeof window<`u`&&St(e).catch(()=>{}),t},vt=async()=>{let e=await Ve(),t=e?.type===`bearer`?Ye(e.token):Z();return t?{type:`bearer`,token:t}:null},yt=e=>{if(G)return G;let t=Y,n=(async()=>{let n=await vt(),r=new Headers;for(let[e,t]of Object.entries(ft()))r.set(e,t);n?.type===`bearer`&&r.set(`Authorization`,`Bearer ${n.token}`);let i=Ne(),a=i?await i.fetch(`/auth/url-token`,{method:`POST`,headers:r}):await fetch(Qe(e,`/auth/url-token`),{method:`POST`,headers:r,credentials:`include`});if(!a.ok)throw t===Y&&X(),Error(`Failed to mint runtime URL auth token (${a.status})`);let o=await a.json().catch(()=>null),s=typeof o?.token==`string`?o.token.trim():``,c=typeof o?.expiresAt==`number`?o.expiresAt:0;if(t!==Y)throw Error(`Runtime URL auth token response is stale`);if(pt(s,c),!U)throw Error(`Runtime URL auth token response was invalid`);return U})().finally(()=>{G===n&&(G=null)});return G=n,G},bt=e=>{if(J)return J;let t=(async()=>{let t=await fetch(Qe(e,`/auth/url-token`),{method:`POST`,credentials:`include`});if(!t.ok)throw K=``,q=0,Error(`Failed to mint local runtime URL auth token (${t.status})`);let n=await t.json().catch(()=>null),r=typeof n?.token==`string`?n.token.trim():``,i=typeof n?.expiresAt==`number`?n.expiresAt:0;if(!r||!Number.isFinite(i))throw Error(`Local runtime URL auth token response was invalid`);return K=r,q=i,r})().finally(()=>{J===t&&(J=null)});return J=t,J},xt=async e=>mt()||yt(e),St=async e=>ht()||bt(e),Q=0,$=null,Ct=null,wt=new Set,Tt=5e3,Et=()=>{for(let e of wt)try{e()}catch{}},Dt=()=>{$!==null&&(clearTimeout($),$=null)},Ot=()=>{if(Dt(),Q<=0||typeof window>`u`)return;let e=W-We-Tt,t=W>0?Math.max(0,e-Date.now()):0;$=setTimeout(()=>{$=null,!(Q<=0)&&yt(Ct).catch(()=>{}).finally(()=>{Ot()})},t)},kt=e=>{typeof e==`string`&&e.trim()&&(Ct=e.trim()),Q+=1,Ot();let t=!1;return()=>{t||(t=!0,Q=Math.max(0,Q-1),Q===0&&Dt())}},At=e=>(wt.add(e),()=>{wt.delete(e)}),jt=async e=>{let t=new Headers(e);for(let[e,n]of Object.entries(ft()))t.has(e)||t.set(e,n);if(t.has(`Authorization`))return t;let n=await vt();return n?.type===`bearer`&&t.set(`Authorization`,`Bearer ${n.token}`),t};export{Ne as C,Ce as D,je as E,L as O,Pe as S,Ie as T,ct as _,st as a,Re as b,Z as c,lt as d,St as f,dt as g,ut as h,X as i,me as k,ft as l,Be as m,ot as n,_t as o,xt as p,jt as r,Ge as s,kt as t,gt as u,At as v,Fe as w,ze as x,Le as y};
|