@pyric/cli 0.1.0-alpha.12 → 0.1.0-alpha.13
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/cli/serve.js +4 -4
- package/dist/cli/serve.js.map +1 -1
- package/dist/serve/bundler.d.ts +13 -8
- package/dist/serve/bundler.d.ts.map +1 -1
- package/dist/serve/bundler.js +31 -36
- package/dist/serve/bundler.js.map +1 -1
- package/dist/serve/docs-ui/docs/_rhythm/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/set-up-your-agent/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/watch-and-review/index.html +1 -1
- package/dist/serve/docs-ui/docs/agent/work-with-an-agent/index.html +1 -1
- package/dist/serve/docs-ui/docs/ai-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/api-reference/index.html +1 -1
- package/dist/serve/docs-ui/docs/app-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/auth-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/ai-logic/index.html +44 -22
- package/dist/serve/docs-ui/docs/build/ai-logic.md +69 -21
- package/dist/serve/docs-ui/docs/build/authentication/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-firestore/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-messaging/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/cloud-storage/index.html +1 -1
- package/dist/serve/docs-ui/docs/build/realtime-database/index.html +1 -1
- package/dist/serve/docs-ui/docs/conformance-scores/index.html +1 -1
- package/dist/serve/docs-ui/docs/create-pyric-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/database-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/firestore-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/functions-rtdb-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/get-started/how-the-swap-works/index.html +2 -1
- package/dist/serve/docs-ui/docs/get-started/how-the-swap-works.md +2 -0
- package/dist/serve/docs-ui/docs/get-started/start-building/index.html +3 -2
- package/dist/serve/docs-ui/docs/get-started/start-building.md +3 -1
- package/dist/serve/docs-ui/docs/index.json +149 -4
- package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/observe/resolve-runtime-status/index.html +28 -0
- package/dist/serve/docs-ui/docs/observe/resolve-runtime-status.md +47 -0
- package/dist/serve/docs-ui/docs/observe/see-whats-happening/index.html +1 -1
- package/dist/serve/docs-ui/docs/observe/shape-your-data/index.html +1 -1
- package/dist/serve/docs-ui/docs/overview/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-app-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-admin-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ai-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ai-scripting-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-app-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-assurance-browser-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-assurance-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-bridge-client-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-bridge-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-browser-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-docs-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-conformance-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-credentials-node-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-discover-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-register-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-remote-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api/index.html +38 -11
- package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +26 -8
- package/dist/serve/docs-ui/docs/pyric-cli-verify-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api/index.html +11 -8
- package/dist/serve/docs-ui/docs/pyric-cli-vite-reference-api.md +17 -4
- package/dist/serve/docs-ui/docs/pyric-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-messaging-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-messaging-sw-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-rules-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-database-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-agents-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-auth-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-auth-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-events-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-events-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-firestore-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-firestore-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-primitives-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rtdb-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rules-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-rules-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-storage-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-storage-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-traffic-hooks-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/pyric-ui-traffic-reference-api/index.html +1 -1
- package/dist/serve/docs-ui/docs/rules-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/audit-your-rules/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/firestore-rules-limits/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/read-a-denial/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/rtdb-rules-in-typescript/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/rules-standard-library/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/secure-it-with-rules/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/simulate-and-lint/index.html +1 -1
- package/dist/serve/docs-ui/docs/secure/write-a-rules-test-suite/index.html +1 -1
- package/dist/serve/docs-ui/docs/ship/ship-to-production/index.html +1 -1
- package/dist/serve/docs-ui/docs/ship/test-in-node/index.html +1 -1
- package/dist/serve/docs-ui/docs/storage-compat/index.html +1 -1
- package/dist/serve/docs-ui/docs/trust/how-we-know-it-matches-firebase/index.html +1 -1
- package/dist/serve/docs-ui/docs/trust/versioning-and-compatibility/index.html +1 -1
- package/dist/serve/docs-ui/docs/tutorial/index.html +429 -0
- package/dist/serve/docs-ui/docs/tutorial.md +701 -0
- package/dist/serve/docs-ui/llms.txt +2 -0
- package/dist/serve/entries/init.d.ts.map +1 -1
- package/dist/serve/entries/init.js +3 -0
- package/dist/serve/entries/init.js.map +1 -1
- package/dist/serve/entries/runtime.d.ts.map +1 -1
- package/dist/serve/entries/runtime.js +20 -5
- package/dist/serve/entries/runtime.js.map +1 -1
- package/dist/serve/entries/worker-runtime.d.ts +2 -2
- package/dist/serve/entries/worker-runtime.d.ts.map +1 -1
- package/dist/serve/entries/worker-runtime.js +45 -10
- package/dist/serve/entries/worker-runtime.js.map +1 -1
- package/dist/serve/runtime/chip-config.d.ts +19 -0
- package/dist/serve/runtime/chip-config.d.ts.map +1 -0
- package/dist/serve/runtime/chip-config.js +19 -0
- package/dist/serve/runtime/chip-config.js.map +1 -0
- package/dist/serve/runtime/chip-install.d.ts +10 -0
- package/dist/serve/runtime/chip-install.d.ts.map +1 -0
- package/dist/serve/runtime/chip-install.js +15 -0
- package/dist/serve/runtime/chip-install.js.map +1 -0
- package/dist/serve/runtime/chip.d.ts +17 -0
- package/dist/serve/runtime/chip.d.ts.map +1 -0
- package/dist/serve/runtime/chip.js +257 -0
- package/dist/serve/runtime/chip.js.map +1 -0
- package/dist/serve/runtime/manifest.d.ts +22 -0
- package/dist/serve/runtime/manifest.d.ts.map +1 -0
- package/dist/serve/runtime/manifest.js +20 -0
- package/dist/serve/runtime/manifest.js.map +1 -0
- package/dist/serve/runtime/status.d.ts +49 -0
- package/dist/serve/runtime/status.d.ts.map +1 -0
- package/dist/serve/runtime/status.js +164 -0
- package/dist/serve/runtime/status.js.map +1 -0
- package/dist/serve/runtime/worker-connection.d.ts +4 -0
- package/dist/serve/runtime/worker-connection.d.ts.map +1 -0
- package/dist/serve/runtime/worker-connection.js +11 -0
- package/dist/serve/runtime/worker-connection.js.map +1 -0
- package/dist/serve/runtime/worker-generation.d.ts +14 -0
- package/dist/serve/runtime/worker-generation.d.ts.map +1 -0
- package/dist/serve/runtime/worker-generation.js +56 -0
- package/dist/serve/runtime/worker-generation.js.map +1 -0
- package/dist/serve/runtime/worker-replacement.d.ts +18 -0
- package/dist/serve/runtime/worker-replacement.d.ts.map +1 -0
- package/dist/serve/runtime/worker-replacement.js +29 -0
- package/dist/serve/runtime/worker-replacement.js.map +1 -0
- package/dist/serve/standalone-assets.d.ts +2 -2
- package/dist/serve/standalone-assets.d.ts.map +1 -1
- package/dist/serve/standalone-assets.js +1 -1
- package/dist/serve/standalone-assets.js.map +1 -1
- package/dist/serve/studio-ui/assets/{RulesCodeEditor-CxVYcZXw.js → RulesCodeEditor-BDlH-5lp.js} +1 -1
- package/dist/serve/studio-ui/assets/{index-cpSfgQ7H.js → index-Bf9SSE8L.js} +5 -5
- package/dist/serve/studio-ui/assets/{seed-DeuFKi4J.js → seed-D_UjxfOU.js} +1 -1
- package/dist/serve/studio-ui/index.html +1 -1
- package/dist/serve/vite-ai-config.d.ts +25 -0
- package/dist/serve/vite-ai-config.d.ts.map +1 -0
- package/dist/serve/vite-ai-config.js +45 -0
- package/dist/serve/vite-ai-config.js.map +1 -0
- package/dist/serve/vite-plugin.d.ts +17 -36
- package/dist/serve/vite-plugin.d.ts.map +1 -1
- package/dist/serve/vite-plugin.js +38 -57
- package/dist/serve/vite-plugin.js.map +1 -1
- package/dist/serve/vite-rules-source.d.ts +8 -0
- package/dist/serve/vite-rules-source.d.ts.map +1 -0
- package/dist/serve/vite-rules-source.js +23 -0
- package/dist/serve/vite-rules-source.js.map +1 -0
- package/dist/serve/vite-worker-runtime.d.ts +20 -0
- package/dist/serve/vite-worker-runtime.d.ts.map +1 -0
- package/dist/serve/vite-worker-runtime.js +25 -0
- package/dist/serve/vite-worker-runtime.js.map +1 -0
- package/dist/serve/worker/client/connection.d.ts +7 -2
- package/dist/serve/worker/client/connection.d.ts.map +1 -1
- package/dist/serve/worker/client/connection.js +13 -16
- package/dist/serve/worker/client/connection.js.map +1 -1
- package/dist/serve/worker/client/core.d.ts +7 -0
- package/dist/serve/worker/client/core.d.ts.map +1 -1
- package/dist/serve/worker/client/core.js +28 -0
- package/dist/serve/worker/client/core.js.map +1 -1
- package/dist/serve/worker/client/runtime-control.d.ts +8 -0
- package/dist/serve/worker/client/runtime-control.d.ts.map +1 -0
- package/dist/serve/worker/client/runtime-control.js +11 -0
- package/dist/serve/worker/client/runtime-control.js.map +1 -0
- package/dist/serve/worker/entry.js +44 -2
- package/dist/serve/worker/entry.js.map +1 -1
- package/dist/serve/worker/host/studio.js +1 -1
- package/dist/serve/worker/host/studio.js.map +1 -1
- package/dist/serve/worker/host-context.d.ts +1 -1
- package/dist/serve/worker/host-context.d.ts.map +1 -1
- package/dist/serve/worker/protocol.d.ts +15 -1
- package/dist/serve/worker/protocol.d.ts.map +1 -1
- package/dist/serve/worker/protocol.js.map +1 -1
- package/dist/serve/worker/retirement.d.ts +21 -0
- package/dist/serve/worker/retirement.d.ts.map +1 -0
- package/dist/serve/worker/retirement.js +97 -0
- package/dist/serve/worker/retirement.js.map +1 -0
- package/dist/serve/worker/serve-init.d.ts.map +1 -1
- package/dist/serve/worker/serve-init.js +5 -6
- package/dist/serve/worker/serve-init.js.map +1 -1
- package/dist/vite.d.ts +1 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js.map +1 -1
- package/package.json +5 -4
- package/src/cli/serve.ts +4 -4
- package/src/serve/bundler.ts +41 -15
- package/src/serve/entries/init.ts +3 -0
- package/src/serve/entries/runtime.ts +23 -7
- package/src/serve/entries/worker-runtime.ts +69 -9
- package/src/serve/runtime/chip-config.ts +30 -0
- package/src/serve/runtime/chip-install.ts +27 -0
- package/src/serve/runtime/chip.ts +282 -0
- package/src/serve/runtime/manifest.ts +37 -0
- package/src/serve/runtime/status.ts +216 -0
- package/src/serve/runtime/worker-connection.ts +14 -0
- package/src/serve/runtime/worker-generation.ts +75 -0
- package/src/serve/runtime/worker-replacement.ts +42 -0
- package/src/serve/standalone-assets.ts +2 -2
- package/src/serve/vite-ai-config.ts +77 -0
- package/src/serve/vite-plugin.ts +60 -87
- package/src/serve/vite-rules-source.ts +28 -0
- package/src/serve/vite-worker-runtime.ts +51 -0
- package/src/serve/worker/client/connection.ts +29 -4
- package/src/serve/worker/client/core.ts +34 -0
- package/src/serve/worker/client/runtime-control.ts +22 -0
- package/src/serve/worker/entry.ts +46 -2
- package/src/serve/worker/host/studio.ts +1 -1
- package/src/serve/worker/host-context.ts +1 -1
- package/src/serve/worker/protocol.ts +13 -1
- package/src/serve/worker/retirement.ts +125 -0
- package/src/serve/worker/serve-init.ts +5 -6
- package/src/vite.ts +1 -0
package/src/serve/vite-plugin.ts
CHANGED
|
@@ -41,7 +41,6 @@
|
|
|
41
41
|
*/
|
|
42
42
|
import { readFile } from 'node:fs/promises';
|
|
43
43
|
import { existsSync, readFileSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
44
|
-
import { homedir } from 'node:os';
|
|
45
44
|
import path from 'node:path';
|
|
46
45
|
import type { IncomingMessage, ServerResponse, Server as HttpServer } from 'node:http';
|
|
47
46
|
import type { Plugin, UserConfig, ConfigEnv } from 'vite';
|
|
@@ -51,19 +50,16 @@ import {
|
|
|
51
50
|
defaultSdkEntries,
|
|
52
51
|
resolveStudioUiDir,
|
|
53
52
|
pyricPackageRoot,
|
|
54
|
-
bundleWorker,
|
|
55
|
-
workerSourceHash,
|
|
56
53
|
NODE_BUILTIN_RE,
|
|
57
54
|
NODE_BUILTIN_SHIMS,
|
|
58
55
|
} from './bundler.js';
|
|
56
|
+
import { createViteWorkerRuntime } from './vite-worker-runtime.js';
|
|
59
57
|
import {
|
|
60
58
|
createEventHub,
|
|
61
59
|
createPyricNamespace,
|
|
62
60
|
type InitPayload,
|
|
63
61
|
} from './namespace.js';
|
|
64
62
|
import { formatActivityWarning } from './activity-warning.js';
|
|
65
|
-
import type { AiEngineConfigWire } from './worker/protocol.js';
|
|
66
|
-
import type { AIOptions } from 'pyric/ai';
|
|
67
63
|
import { diskWorkspace, diskProjectStore } from './studio/index.js';
|
|
68
64
|
import { createBridgeMount } from './bridge-mount.js';
|
|
69
65
|
import {
|
|
@@ -91,6 +87,17 @@ import {
|
|
|
91
87
|
createLinePrefixer,
|
|
92
88
|
registerModuleUrl,
|
|
93
89
|
} from '../cli/dev-runner.js';
|
|
90
|
+
import {
|
|
91
|
+
loadViteAiEnv,
|
|
92
|
+
resolveViteAiConfig,
|
|
93
|
+
type PyricAiOptions,
|
|
94
|
+
} from './vite-ai-config.js';
|
|
95
|
+
import { resolveViteRulesConfig } from './vite-rules-source.js';
|
|
96
|
+
import {
|
|
97
|
+
PYRIC_RUNTIME_CHIP_META,
|
|
98
|
+
runtimeChipMetaValue,
|
|
99
|
+
type PyricRuntimeChipOption,
|
|
100
|
+
} from './runtime/chip-config.js';
|
|
94
101
|
|
|
95
102
|
/**
|
|
96
103
|
* Whether a `vite build` should run the firebase→pyric swap (produce a SANDBOX
|
|
@@ -109,41 +116,6 @@ function swapsInBuild(env: ConfigEnv, swapInBuild: boolean | undefined): boolean
|
|
|
109
116
|
/** Any `firebase/<sub>` specifier. */
|
|
110
117
|
const FB_ANY = /^firebase\/([a-z-]+(?:\/[a-z-]+)*)$/;
|
|
111
118
|
|
|
112
|
-
/** The serve proxy route the browser openai engine defaults to (#98.2). */
|
|
113
|
-
const AI_PROXY_PATH = '/__pyric/ai-proxy';
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* The plugin-level engine config surface = `pyric/ai`'s `EngineConfig` (what an
|
|
117
|
-
* app passes to `getAI`), minus the custom `AnswerEngine` object variant — a
|
|
118
|
-
* live object cannot cross to the SharedWorker host or serialize into the page,
|
|
119
|
-
* so the plugin's declarative surface is the JSON-able `scripted` / `openai`
|
|
120
|
-
* configs only (the same restriction the worker path already enforces).
|
|
121
|
-
*/
|
|
122
|
-
export type PyricAiEngineConfig = Extract<NonNullable<AIOptions['engine']>, { kind: string }>;
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Node-side `EngineConfig` → JSON-safe {@link AiEngineConfigWire}. Mirrors the
|
|
126
|
-
* browser `toEngineWire` (entries/ai.ts): an openai config with no `baseUrl`
|
|
127
|
-
* targets the same-origin proxy; scripted `script` entries pass through (plain
|
|
128
|
-
* authoring shapes — function/RegExp matchers can't survive JSON and are not a
|
|
129
|
-
* plugin-config use case).
|
|
130
|
-
*/
|
|
131
|
-
export function engineConfigToWire(engine: PyricAiEngineConfig): AiEngineConfigWire {
|
|
132
|
-
if (engine.kind === 'openai') {
|
|
133
|
-
return {
|
|
134
|
-
kind: 'openai',
|
|
135
|
-
baseUrl: engine.baseUrl ?? AI_PROXY_PATH,
|
|
136
|
-
...(engine.model !== undefined ? { model: engine.model } : {}),
|
|
137
|
-
...(engine.modelMap !== undefined ? { modelMap: engine.modelMap } : {}),
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
return {
|
|
141
|
-
kind: 'scripted',
|
|
142
|
-
...(engine.script !== undefined
|
|
143
|
-
? { script: engine.script as unknown as Array<Record<string, unknown>> }
|
|
144
|
-
: {}),
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
119
|
/** The firebase subpaths with swap entries. */
|
|
148
120
|
const SERVED = new Set(SDK_MODULES.map((specifier) => specifier.slice('firebase/'.length)));
|
|
149
121
|
const entryKey = (subpath: string): string => subpath.replaceAll('/', '-');
|
|
@@ -159,8 +131,9 @@ function packageRootOf(file: string): string {
|
|
|
159
131
|
}
|
|
160
132
|
|
|
161
133
|
export interface PyricOptions {
|
|
162
|
-
/**
|
|
163
|
-
* `firestore.rules`,
|
|
134
|
+
/** Firestore rules path (relative to `root`). Default discovery prefers an
|
|
135
|
+
* authored `firestore.modules.rules`, then `firebase.json`, then
|
|
136
|
+
* `firestore.rules` in the project root. */
|
|
164
137
|
rules?: string;
|
|
165
138
|
/** Project dir for `firebase.json` / rules discovery. Default: Vite's `root`. */
|
|
166
139
|
root?: string;
|
|
@@ -193,6 +166,10 @@ export interface PyricOptions {
|
|
|
193
166
|
* so app, Studio, and agent all observe the one sandbox); pass `ui: false`
|
|
194
167
|
* to disable. */
|
|
195
168
|
ui?: boolean;
|
|
169
|
+
/** Inject the collapsed Pyric runtime chip into the app during sandbox Vite
|
|
170
|
+
* dev/builds. On by default. Pass `false` to hide it, or
|
|
171
|
+
* `{ initiallyOpen: true }` when actively debugging runtime errors. */
|
|
172
|
+
runtimeChip?: PyricRuntimeChipOption;
|
|
196
173
|
/** RTDB-triggered Cloud Functions under this dev server (the `pyric dev`
|
|
197
174
|
* parity fold). By default a `functions` block in `firebase.json` is
|
|
198
175
|
* discovered automatically: its `onValueCreated` triggers run in an isolated
|
|
@@ -229,11 +206,14 @@ export interface PyricOptions {
|
|
|
229
206
|
*
|
|
230
207
|
* pyric({
|
|
231
208
|
* ai: {
|
|
232
|
-
*
|
|
209
|
+
* model: 'llama3.2',
|
|
233
210
|
* proxyUpstream: 'http://localhost:11434/v1', // your Ollama
|
|
234
211
|
* },
|
|
235
212
|
* })
|
|
236
213
|
*
|
|
214
|
+
* - `model` is the simple OpenAI-compatible path. It uses the same-origin
|
|
215
|
+
* proxy and becomes the catch-all upstream model. `PYRIC_AI_MODEL` selects
|
|
216
|
+
* the same path when neither `model` nor `engine` is explicit.
|
|
237
217
|
* - `engine` is `pyric/ai`'s `EngineConfig` (scripted | openai), applied on
|
|
238
218
|
* both the SharedWorker and in-page paths. An openai `baseUrl` of
|
|
239
219
|
* `/__pyric/ai-proxy` (or omitted) routes through the same-origin proxy so a
|
|
@@ -241,20 +221,11 @@ export interface PyricOptions {
|
|
|
241
221
|
* - `proxyUpstream` sets what `/__pyric/ai-proxy` forwards to (beats the
|
|
242
222
|
* `PYRIC_AI_PROXY_UPSTREAM` env var; default `http://localhost:11434/v1`).
|
|
243
223
|
*
|
|
244
|
-
* Precedence:
|
|
245
|
-
* app's
|
|
246
|
-
*
|
|
247
|
-
* engine the first `getAI()` call's engine is honored (first-call-wins), and
|
|
248
|
-
* with neither the zero-config scripted default applies.
|
|
224
|
+
* Precedence: explicit `engine` or `model`, then `PYRIC_AI_MODEL`, then an
|
|
225
|
+
* engine passed by the app's first `getAI()` call; with none, the zero-config
|
|
226
|
+
* scripted default applies. `model` and `engine` are mutually exclusive.
|
|
249
227
|
*/
|
|
250
|
-
ai?:
|
|
251
|
-
/** Engine config (scripted | openai). Custom `AnswerEngine` objects are not
|
|
252
|
-
* supported at the plugin level — they can't cross to the worker/page. */
|
|
253
|
-
engine?: PyricAiEngineConfig;
|
|
254
|
-
/** OpenAI-compatible upstream `/__pyric/ai-proxy` forwards to. Beats
|
|
255
|
-
* `PYRIC_AI_PROXY_UPSTREAM`; default `http://localhost:11434/v1`. */
|
|
256
|
-
proxyUpstream?: string;
|
|
257
|
-
};
|
|
228
|
+
ai?: PyricAiOptions;
|
|
258
229
|
}
|
|
259
230
|
|
|
260
231
|
/**
|
|
@@ -264,6 +235,7 @@ export interface PyricOptions {
|
|
|
264
235
|
* export default defineConfig({ plugins: [pyric()] });
|
|
265
236
|
*/
|
|
266
237
|
export function pyric(options: PyricOptions = {}): Plugin {
|
|
238
|
+
let resolvedAi = resolveViteAiConfig(options.ai, {});
|
|
267
239
|
// Resolved once. `defaultSdkEntries()` prefers compiled dist `.js` and falls
|
|
268
240
|
// back to source `.ts` in the workspace.
|
|
269
241
|
const entries = defaultSdkEntries(); // { app, auth, firestore, init } → abs paths
|
|
@@ -335,11 +307,10 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
335
307
|
};
|
|
336
308
|
|
|
337
309
|
// M2: the SharedWorker bundle's content hash (sync) — stamped into the page so
|
|
338
|
-
// a still-running OLD worker is detected as stale.
|
|
339
|
-
//
|
|
340
|
-
//
|
|
341
|
-
const
|
|
342
|
-
let workerReady = false;
|
|
310
|
+
// a still-running OLD worker is detected as stale. The collaborator becomes
|
|
311
|
+
// ready once its bundle succeeds; until then (or on bundle failure) the page
|
|
312
|
+
// is forced onto the in-page sandbox path. transformIndexHtml reads its tag.
|
|
313
|
+
const workerRuntime = createViteWorkerRuntime();
|
|
343
314
|
|
|
344
315
|
// Set by the `config` hook. When the plugin runs under `vite build` at all it
|
|
345
316
|
// is a SANDBOX build (the `apply` gate below only lets build through under the
|
|
@@ -364,12 +335,12 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
364
335
|
// so multi-tab is disabled under bridge to keep agent + app on one backend.
|
|
365
336
|
const bridgeOpts = options.bridge === true ? {} : options.bridge || null;
|
|
366
337
|
|
|
367
|
-
// Plugin-level AI engine, normalized
|
|
338
|
+
// Plugin-level AI engine, normalized to the JSON-safe wire shape. Travels
|
|
368
339
|
// to the worker host via the init payload (→ ctx.aiEngine) AND to the in-page
|
|
369
|
-
// fallback via an injected synchronous global (see transformIndexHtml).
|
|
370
|
-
//
|
|
371
|
-
|
|
372
|
-
|
|
340
|
+
// fallback via an injected synchronous global (see transformIndexHtml).
|
|
341
|
+
// An explicit engine is available immediately (some hook tests call the HTML
|
|
342
|
+
// transform directly); Vite's config hook may otherwise select the simple
|
|
343
|
+
// PYRIC_AI_MODEL environment path for the active mode/root.
|
|
373
344
|
return {
|
|
374
345
|
name: 'pyric:sandbox',
|
|
375
346
|
// Active for `vite dev` ALWAYS, and for `vite build` only when it is a
|
|
@@ -378,14 +349,17 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
378
349
|
// production output. A sandbox build applies the same swap so the output
|
|
379
350
|
// bundles pyric's in-page adapters (self-contained; preview it under
|
|
380
351
|
// `pyric dev`, never deploy it).
|
|
381
|
-
apply(
|
|
352
|
+
apply(config, env) {
|
|
353
|
+
void config;
|
|
382
354
|
if (env.command === 'serve') return true;
|
|
383
355
|
return swapsInBuild(env, options.swapInBuild);
|
|
384
356
|
},
|
|
385
357
|
enforce: 'pre',
|
|
386
358
|
|
|
387
|
-
config(
|
|
359
|
+
config(config, env) {
|
|
388
360
|
sandboxBuild = env.command === 'build';
|
|
361
|
+
const loadedEnv = loadViteAiEnv(env.mode, config.root, config.envDir);
|
|
362
|
+
resolvedAi = resolveViteAiConfig(options.ai, loadedEnv);
|
|
389
363
|
// Cast: the `esbuild` package's `Plugin` type skews slightly from Vite's
|
|
390
364
|
// bundled esbuild types (benign — the Plugin shape is stable across the
|
|
391
365
|
// versions in range).
|
|
@@ -457,10 +431,11 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
457
431
|
} catch {
|
|
458
432
|
/* optional — serve without a firebase.json */
|
|
459
433
|
}
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
434
|
+
// Convention-first development source: an explicit option wins; otherwise
|
|
435
|
+
// an authored 2+modules file wins over firebase.json's generated deployment
|
|
436
|
+
// target. Projects without that convention retain the normal Firebase
|
|
437
|
+
// discovery path.
|
|
438
|
+
const config = resolveViteRulesConfig(cwd, options.rules, fbJson);
|
|
464
439
|
const loaded = await loadProjectRules(cwd, config);
|
|
465
440
|
const loadedDatabase = await loadProjectDatabaseRules(cwd, config);
|
|
466
441
|
const loadedStorage = await loadProjectStorageRules(cwd, config);
|
|
@@ -530,12 +505,10 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
530
505
|
|
|
531
506
|
// ── M2 SharedWorker host: bundle it (cached per version) and serve it at
|
|
532
507
|
// /__pyric/sdk/worker.js. This is what flips runtime.ts to the worker path.
|
|
533
|
-
// On bundle failure,
|
|
534
|
-
//
|
|
535
|
-
const sdkDir = path.join(homedir(), '.pyric', 'vite-worker', workerVersion);
|
|
508
|
+
// On bundle failure, the collaborator stays unready and its HTML tag
|
|
509
|
+
// forces the in-page sandbox.
|
|
536
510
|
try {
|
|
537
|
-
await
|
|
538
|
-
workerReady = true;
|
|
511
|
+
await workerRuntime.prepare();
|
|
539
512
|
} catch (e) {
|
|
540
513
|
server.config.logger.warn(
|
|
541
514
|
` ⚠ [pyric] SharedWorker bundle failed — using the in-page sandbox (single-tab, ephemeral): ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -629,7 +602,7 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
629
602
|
messaging: true,
|
|
630
603
|
// Plugin-level engine → the worker host's ctx.aiEngine (host-ai.ts),
|
|
631
604
|
// which wins over any op-carried engine. Null when unset.
|
|
632
|
-
ai:
|
|
605
|
+
ai: resolvedAi.engineWire ? { engine: resolvedAi.engineWire } : null,
|
|
633
606
|
});
|
|
634
607
|
// Pyric Studio: mount the disk-backed workspace/project routes that
|
|
635
608
|
// Studio's `local` mode talks to + serve the built Studio app at
|
|
@@ -657,6 +630,7 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
657
630
|
);
|
|
658
631
|
}
|
|
659
632
|
}
|
|
633
|
+
const { sdkDir } = workerRuntime.status();
|
|
660
634
|
const namespace = createPyricNamespace({
|
|
661
635
|
sdkDir,
|
|
662
636
|
initPayload,
|
|
@@ -668,7 +642,7 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
668
642
|
studioUiDir,
|
|
669
643
|
// `ai.proxyUpstream`: what `/__pyric/ai-proxy` forwards to (beats the
|
|
670
644
|
// PYRIC_AI_PROXY_UPSTREAM env var; falls back to the default when unset).
|
|
671
|
-
aiProxyUpstream:
|
|
645
|
+
aiProxyUpstream: resolvedAi.proxyUpstream,
|
|
672
646
|
// Adapt Vite's logger to the plain info/note shape the namespace's
|
|
673
647
|
// diagnostics (denial relay, future hot-reload lines) expect —
|
|
674
648
|
// matches the `↻`/`⚠ [pyric]` lines already logged elsewhere in this
|
|
@@ -1047,6 +1021,7 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
1047
1021
|
},
|
|
1048
1022
|
|
|
1049
1023
|
transformIndexHtml(html) {
|
|
1024
|
+
const runtimeChipTag = `<meta name="${PYRIC_RUNTIME_CHIP_META}" content="${runtimeChipMetaValue(options.runtimeChip)}" data-studio="${options.ui === false ? 'off' : 'on'}" data-pyric-sandbox>`;
|
|
1050
1025
|
// Sandbox BUILD: the app's own `firebase/*` imports were already swapped
|
|
1051
1026
|
// (resolveId, above) to pyric's in-page adapters and BUNDLED into the app
|
|
1052
1027
|
// chunk, and the emitted init chunk (script-tagged here) carries the
|
|
@@ -1063,7 +1038,7 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
1063
1038
|
const initTag = initChunkFile
|
|
1064
1039
|
? `<script type="module" crossorigin src="/${initChunkFile}" data-pyric-sandbox-init></script>`
|
|
1065
1040
|
: '';
|
|
1066
|
-
const tags = SANDBOX_BUILD_META + initTag;
|
|
1041
|
+
const tags = SANDBOX_BUILD_META + runtimeChipTag + initTag;
|
|
1067
1042
|
return html.includes('</head>')
|
|
1068
1043
|
? html.replace('</head>', `${tags}</head>`)
|
|
1069
1044
|
: tags + html;
|
|
@@ -1076,26 +1051,24 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
1076
1051
|
// module evaluates — a classic inline script runs before the deferred module.
|
|
1077
1052
|
// The flag (not nulling `window.SharedWorker`) leaves the user's own
|
|
1078
1053
|
// SharedWorker usage intact. We force in-page ONLY when the worker bundle
|
|
1079
|
-
// failed (
|
|
1054
|
+
// failed (worker runtime unready) — the ephemeral fallback. `bridge` no longer
|
|
1080
1055
|
// forces in-page: the bridge peer routes agent tool-calls THROUGH the
|
|
1081
1056
|
// worker (see `connectBridgePeer`), so the agent shares the one sandbox the
|
|
1082
1057
|
// app + Studio use.
|
|
1083
|
-
const head =
|
|
1084
|
-
? `<meta name="pyric-worker-v" content="${workerVersion}" ${MARKER}>`
|
|
1085
|
-
: `<script ${MARKER}>globalThis.__PYRIC_FORCE_INPAGE__=true;</script>`;
|
|
1058
|
+
const head = workerRuntime.headTag(MARKER);
|
|
1086
1059
|
// Plugin-level engine for the IN-PAGE path: a classic inline script runs
|
|
1087
1060
|
// before the deferred init module AND before app code's `getAI`, so the
|
|
1088
1061
|
// served `getAI` (entries/ai.ts) reads it synchronously — init.json can't
|
|
1089
1062
|
// be awaited there. Harmless on the worker path (that branch ignores the
|
|
1090
1063
|
// global; the worker reads ctx.aiEngine from init.json). `<` is escaped so
|
|
1091
1064
|
// an engine value can never break out of the script tag.
|
|
1092
|
-
const aiEngineTag =
|
|
1093
|
-
? `<script ${MARKER}>globalThis.__PYRIC_AI_ENGINE__=${JSON.stringify(
|
|
1065
|
+
const aiEngineTag = resolvedAi.engineWire
|
|
1066
|
+
? `<script ${MARKER}>globalThis.__PYRIC_AI_ENGINE__=${JSON.stringify(resolvedAi.engineWire).replace(/</g, '\\u003c')};</script>`
|
|
1094
1067
|
: '';
|
|
1095
1068
|
// Boot the sandbox by loading the real init entry as a module (Vite
|
|
1096
1069
|
// serves + transforms it). The init module's top-level await deploys rules
|
|
1097
1070
|
// before app code runs. Mirrors serve's injectServeTags.
|
|
1098
|
-
const tag = head + aiEngineTag + `<script type="module" src="/@fs/${entries.init}" ${MARKER}></script>`;
|
|
1071
|
+
const tag = head + aiEngineTag + runtimeChipTag + `<script type="module" src="/@fs/${entries.init}" ${MARKER}></script>`;
|
|
1099
1072
|
return html.includes('</head>') ? html.replace('</head>', `${tag}</head>`) : tag + html;
|
|
1100
1073
|
},
|
|
1101
1074
|
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import type { FirebaseJson } from '../cli/firebase-json.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Apply the Vite authoring convention without replacing unrelated Firebase
|
|
7
|
+
* configuration. The normal rules loader retains responsibility for its
|
|
8
|
+
* `firebase.json` and root `firestore.rules` fallbacks.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveViteRulesConfig(
|
|
11
|
+
root: string,
|
|
12
|
+
explicitRules: string | undefined,
|
|
13
|
+
firebaseConfig: FirebaseJson | null,
|
|
14
|
+
): FirebaseJson | null {
|
|
15
|
+
const rulesSource = explicitRules
|
|
16
|
+
?? (existsSync(path.join(root, 'firestore.modules.rules'))
|
|
17
|
+
? 'firestore.modules.rules'
|
|
18
|
+
: undefined);
|
|
19
|
+
|
|
20
|
+
if (!rulesSource) return firebaseConfig;
|
|
21
|
+
return {
|
|
22
|
+
...(firebaseConfig ?? {}),
|
|
23
|
+
firestore: {
|
|
24
|
+
...(firebaseConfig?.firestore ?? {}),
|
|
25
|
+
rules: rulesSource,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import {
|
|
4
|
+
bundleWorker,
|
|
5
|
+
workerSourceHash,
|
|
6
|
+
type WorkerBundleOptions,
|
|
7
|
+
type WorkerBundleResult,
|
|
8
|
+
} from './bundler.js';
|
|
9
|
+
|
|
10
|
+
export interface ViteWorkerRuntimeStatus {
|
|
11
|
+
sdkDir: string;
|
|
12
|
+
ready: boolean;
|
|
13
|
+
epoch: string | null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ViteWorkerRuntime {
|
|
17
|
+
prepare(): Promise<void>;
|
|
18
|
+
status(): ViteWorkerRuntimeStatus;
|
|
19
|
+
headTag(marker: string): string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface ViteWorkerRuntimeOptions {
|
|
23
|
+
cacheRoot?: string;
|
|
24
|
+
cacheKey?: string;
|
|
25
|
+
bundle?: (options: WorkerBundleOptions) => Promise<WorkerBundleResult>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Own the Vite plugin's worker build state and its one HTML projection. */
|
|
29
|
+
export function createViteWorkerRuntime(
|
|
30
|
+
options: ViteWorkerRuntimeOptions = {},
|
|
31
|
+
): ViteWorkerRuntime {
|
|
32
|
+
const cacheRoot = options.cacheRoot ?? join(homedir(), '.pyric', 'vite-worker');
|
|
33
|
+
const cacheKey = options.cacheKey ?? workerSourceHash();
|
|
34
|
+
const sdkDir = join(cacheRoot, cacheKey);
|
|
35
|
+
const bundle = options.bundle ?? bundleWorker;
|
|
36
|
+
let current: ViteWorkerRuntimeStatus = { sdkDir, ready: false, epoch: null };
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
async prepare() {
|
|
40
|
+
current = { sdkDir, ready: false, epoch: null };
|
|
41
|
+
const result = await bundle({ outDir: sdkDir });
|
|
42
|
+
current = { sdkDir, ready: true, epoch: result.epoch };
|
|
43
|
+
},
|
|
44
|
+
status: () => current,
|
|
45
|
+
headTag(marker) {
|
|
46
|
+
return current.ready && current.epoch
|
|
47
|
+
? `<meta name="pyric-worker-v" content="${current.epoch}" ${marker}>`
|
|
48
|
+
: `<script ${marker}>globalThis.__PYRIC_FORCE_INPAGE__=true;</script>`;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
nextSubId,
|
|
12
12
|
wirePort,
|
|
13
13
|
rpc,
|
|
14
|
+
rpcWithTimeout,
|
|
14
15
|
rawRpc,
|
|
15
16
|
openSnapshotSubscription,
|
|
16
17
|
closeSubscription,
|
|
@@ -29,7 +30,15 @@ import type { ClientDb } from './handles.js';
|
|
|
29
30
|
* `getFirestore` mirrors `pyric/firestore`'s `getFirestore(sandbox)` shape
|
|
30
31
|
* but returns a `ClientDb` backed by a `MessagePort` instead of a sandbox.
|
|
31
32
|
*/
|
|
32
|
-
export
|
|
33
|
+
export interface SharedWorkerConnectionOptions {
|
|
34
|
+
onError?: (error: Error) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getFirestore(
|
|
38
|
+
workerUrl: string | URL,
|
|
39
|
+
name?: string,
|
|
40
|
+
options: SharedWorkerConnectionOptions = {},
|
|
41
|
+
): ClientDb {
|
|
33
42
|
if (typeof SharedWorker === 'undefined') {
|
|
34
43
|
throw new Error(
|
|
35
44
|
'SharedWorker is not available. ' +
|
|
@@ -41,9 +50,15 @@ export function getFirestore(workerUrl: string | URL, name?: string): ClientDb {
|
|
|
41
50
|
type: 'classic',
|
|
42
51
|
name: name ?? 'pyric-shared-worker',
|
|
43
52
|
});
|
|
53
|
+
if (options.onError) {
|
|
54
|
+
worker.addEventListener('error', (event) => {
|
|
55
|
+
const detail = event.message || `failed to load ${String(workerUrl)}`;
|
|
56
|
+
options.onError?.(new Error(`Pyric SharedWorker error: ${detail}`));
|
|
57
|
+
});
|
|
58
|
+
}
|
|
44
59
|
const port = worker.port;
|
|
45
|
-
port.start();
|
|
46
60
|
wirePort(port);
|
|
61
|
+
port.start();
|
|
47
62
|
return { __kind: 'client-db', port };
|
|
48
63
|
}
|
|
49
64
|
|
|
@@ -52,8 +67,18 @@ export function getFirestore(workerUrl: string | URL, name?: string): ClientDb {
|
|
|
52
67
|
* compares it to the served bundle version and warns when a still-running OLD
|
|
53
68
|
* worker is older than what's served (a SharedWorker can't hot-update).
|
|
54
69
|
*/
|
|
55
|
-
export async function getWorkerVersion(
|
|
56
|
-
|
|
70
|
+
export async function getWorkerVersion(
|
|
71
|
+
db: ClientDb,
|
|
72
|
+
options: { timeoutMs?: number } = {},
|
|
73
|
+
): Promise<string> {
|
|
74
|
+
const id = nextId();
|
|
75
|
+
const timeoutMs = options.timeoutMs ?? 2_000;
|
|
76
|
+
const r = (await rpcWithTimeout(
|
|
77
|
+
db.port,
|
|
78
|
+
{ t: 'op', id, method: 'getRuntimeEpoch' },
|
|
79
|
+
timeoutMs,
|
|
80
|
+
`Timed out waiting for the Pyric SharedWorker version handshake after ${timeoutMs}ms.`,
|
|
81
|
+
)) as { version: string };
|
|
57
82
|
return r.version;
|
|
58
83
|
}
|
|
59
84
|
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* they did when all of this lived inline in `client.ts`.
|
|
11
11
|
*/
|
|
12
12
|
import type { InboundMessage, OutboundMessage } from '../protocol.js';
|
|
13
|
+
import type { RuntimeReloadMessage } from '../protocol.js';
|
|
13
14
|
// TYPE-ONLY — the auth-lens contract + the cross-service event envelope, shared
|
|
14
15
|
// with the worker host + the sandbox's event provenance. Erased at build, so the
|
|
15
16
|
// leaf client bundle stays engine-free.
|
|
@@ -58,6 +59,14 @@ export const _eventSubs = new Map<string, {
|
|
|
58
59
|
next: (events: readonly SandboxEvent[]) => void;
|
|
59
60
|
}>();
|
|
60
61
|
const disconnectedPorts = new WeakSet<ClientPort>();
|
|
62
|
+
const runtimeReloadListeners = new Set<(message: RuntimeReloadMessage) => void>();
|
|
63
|
+
|
|
64
|
+
export function subscribeRuntimeReload(
|
|
65
|
+
listener: (message: RuntimeReloadMessage) => void,
|
|
66
|
+
): () => void {
|
|
67
|
+
runtimeReloadListeners.add(listener);
|
|
68
|
+
return () => runtimeReloadListeners.delete(listener);
|
|
69
|
+
}
|
|
61
70
|
|
|
62
71
|
function appDeletedError(): Error & { code: string } {
|
|
63
72
|
return Object.assign(new Error('Firebase App was deleted'), { code: 'app/app-deleted' });
|
|
@@ -231,6 +240,8 @@ export function wirePort(port: ClientPort): void {
|
|
|
231
240
|
// no rehydration. Deliver the whole batch to the registered subscriber.
|
|
232
241
|
const subscription = _eventSubs.get(msg.subId);
|
|
233
242
|
if (subscription) subscription.next(msg.events);
|
|
243
|
+
} else if (msg.t === 'runtime-reload') {
|
|
244
|
+
for (const listener of runtimeReloadListeners) listener(msg);
|
|
234
245
|
}
|
|
235
246
|
};
|
|
236
247
|
}
|
|
@@ -330,6 +341,29 @@ export function rpc(port: ClientPort, msg: InboundMessage): Promise<unknown> {
|
|
|
330
341
|
return rawRpc(port, stampIssuer(msg));
|
|
331
342
|
}
|
|
332
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Send a client RPC with a bounded wait, while keeping correlation cleanup
|
|
346
|
+
* inside the transport module that owns the pending-request registry.
|
|
347
|
+
*/
|
|
348
|
+
export function rpcWithTimeout(
|
|
349
|
+
port: ClientPort,
|
|
350
|
+
msg: InboundMessage,
|
|
351
|
+
timeoutMs: number,
|
|
352
|
+
timeoutMessage: string,
|
|
353
|
+
): Promise<unknown> {
|
|
354
|
+
const id = (msg as { id: string }).id;
|
|
355
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
356
|
+
const timeout = new Promise<never>((_, reject) => {
|
|
357
|
+
timer = setTimeout(() => {
|
|
358
|
+
if (!_pending.delete(id)) return;
|
|
359
|
+
reject(new Error(timeoutMessage));
|
|
360
|
+
}, timeoutMs);
|
|
361
|
+
});
|
|
362
|
+
return Promise.race([rpc(port, msg), timeout]).finally(() => {
|
|
363
|
+
if (timer) clearTimeout(timer);
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
333
367
|
/**
|
|
334
368
|
* Like {@link rpc} but stamps the active default auth lens onto the op message
|
|
335
369
|
* (Pyric Studio). Used by data-service ops so a `setLens(...)` choice
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { nextId, rpcWithTimeout, subscribeRuntimeReload } from './core.js';
|
|
2
|
+
import type { ClientDb } from './handles.js';
|
|
3
|
+
|
|
4
|
+
/** Ask the current SharedWorker to drain accepted work and retire. */
|
|
5
|
+
export async function retireWorkerRuntime(
|
|
6
|
+
db: ClientDb,
|
|
7
|
+
targetEpoch: string,
|
|
8
|
+
options: { timeoutMs?: number } = {},
|
|
9
|
+
): Promise<void> {
|
|
10
|
+
const timeoutMs = options.timeoutMs ?? 12_000;
|
|
11
|
+
await rpcWithTimeout(
|
|
12
|
+
db.port,
|
|
13
|
+
{ t: 'op', id: nextId(), method: 'retireRuntime', targetEpoch },
|
|
14
|
+
timeoutMs,
|
|
15
|
+
`Timed out waiting for the Pyric worker to retire after ${timeoutMs}ms.`,
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Observe the worker's all-pages reload signal. */
|
|
20
|
+
export function onWorkerRuntimeReload(listener: (epoch: string) => void): () => void {
|
|
21
|
+
return subscribeRuntimeReload((message) => listener(message.epoch));
|
|
22
|
+
}
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
// without enabling the full webworker lib.
|
|
49
49
|
interface SharedWorkerGlobalScope {
|
|
50
50
|
onconnect: ((e: MessageEvent) => void) | null;
|
|
51
|
+
close(): void;
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
import { createIndexedDBBackend } from 'pyric/sandbox';
|
|
@@ -65,6 +66,17 @@ import {
|
|
|
65
66
|
type ServiceWorkerChannelMessage,
|
|
66
67
|
} from './service-worker-channel.js';
|
|
67
68
|
import { createServiceWorkerRelay } from './service-worker-relay.js';
|
|
69
|
+
import { createWorkerRetirement } from './retirement.js';
|
|
70
|
+
|
|
71
|
+
declare const __PYRIC_WORKER_VERSION__: string;
|
|
72
|
+
const workerEpoch = typeof __PYRIC_WORKER_VERSION__ !== 'undefined'
|
|
73
|
+
? __PYRIC_WORKER_VERSION__
|
|
74
|
+
: 'dev';
|
|
75
|
+
const workerScope = self as unknown as SharedWorkerGlobalScope;
|
|
76
|
+
const retirement = createWorkerRetirement({
|
|
77
|
+
closeWorker: () => workerScope.close(),
|
|
78
|
+
beforeAnnounce: async () => { await _ctx?.captureFlush?.(); },
|
|
79
|
+
});
|
|
68
80
|
|
|
69
81
|
// ─── Singleton context ────────────────────────────────────────────────────
|
|
70
82
|
|
|
@@ -125,12 +137,37 @@ async function buildCtx(): Promise<HostCtx> {
|
|
|
125
137
|
* Each tab gets its own `MessagePort`; we start() it (required for
|
|
126
138
|
* classic-mode workers to un-pause the message queue) and wire onmessage.
|
|
127
139
|
*/
|
|
128
|
-
|
|
140
|
+
workerScope.onconnect = (e: MessageEvent) => {
|
|
129
141
|
const port = e.ports[0];
|
|
130
142
|
port.start();
|
|
143
|
+
retirement.connect(port);
|
|
131
144
|
|
|
132
145
|
let messageQueue = Promise.resolve();
|
|
133
146
|
port.onmessage = (ev: MessageEvent<InboundMessage>) => {
|
|
147
|
+
if (ev.data.t === 'op' && ev.data.method === 'getRuntimeEpoch') {
|
|
148
|
+
port.postMessage({
|
|
149
|
+
t: 'res',
|
|
150
|
+
id: ev.data.id,
|
|
151
|
+
ok: true,
|
|
152
|
+
value: {
|
|
153
|
+
version: workerEpoch,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (ev.data.t === 'op' && ev.data.method === 'retireRuntime') {
|
|
159
|
+
void retirement.retire(port, ev.data.id, ev.data.targetEpoch);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (!retirement.accepting()) {
|
|
163
|
+
if ('id' in ev.data) {
|
|
164
|
+
port.postMessage({
|
|
165
|
+
t: 'res', id: ev.data.id, ok: false,
|
|
166
|
+
error: { code: 'pyric/worker-retiring', message: 'The Pyric worker is restarting.' },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
134
171
|
// Serialize each port's frames. A disconnect acknowledgement therefore
|
|
135
172
|
// cannot overtake an already-posted mutation, and later frames see the
|
|
136
173
|
// disconnected-port tombstone instead of touching the shared backend.
|
|
@@ -143,12 +180,14 @@ async function buildCtx(): Promise<HostCtx> {
|
|
|
143
180
|
console.error('[pyric worker] message handler error:', (e as Error)?.stack ?? e, 'msg:', ev.data);
|
|
144
181
|
}
|
|
145
182
|
});
|
|
183
|
+
retirement.track(port, messageQueue);
|
|
146
184
|
};
|
|
147
185
|
|
|
148
186
|
// Best-effort port cleanup on tab close/navigation.
|
|
149
187
|
// Chrome doesn't reliably fire 'close' on MessagePort — handled as
|
|
150
188
|
// best-effort; subscriptions also GC when the worker itself dies.
|
|
151
189
|
port.addEventListener('close', () => {
|
|
190
|
+
retirement.disconnect(port);
|
|
152
191
|
if (_ctx) cleanupPort(_ctx, port as unknown as PortLike);
|
|
153
192
|
});
|
|
154
193
|
};
|
|
@@ -174,6 +213,11 @@ if (typeof BroadcastChannel !== 'undefined') {
|
|
|
174
213
|
channel.onmessage = (event: MessageEvent<ServiceWorkerChannelMessage>) => {
|
|
175
214
|
const envelope = event.data;
|
|
176
215
|
if (envelope.direction !== 'host') return;
|
|
177
|
-
|
|
216
|
+
// A generation-targeted replacement can briefly overlap this old worker
|
|
217
|
+
// with its successor. Only the accepting generation may answer the shared
|
|
218
|
+
// service-worker relay channel, otherwise both workers would reply.
|
|
219
|
+
if (!retirement.accepting()) return;
|
|
220
|
+
const work = relay.handle(envelope);
|
|
221
|
+
retirement.trackDetached(work);
|
|
178
222
|
};
|
|
179
223
|
}
|
|
@@ -60,7 +60,7 @@ export async function handleStudioOp(
|
|
|
60
60
|
// reset just emptied `sandbox.history()`, and waiting out the
|
|
61
61
|
// capture debounce leaves a window where a worker death resurrects
|
|
62
62
|
// the wiped session's events on the next boot.
|
|
63
|
-
ctx.captureFlush?.();
|
|
63
|
+
await ctx.captureFlush?.();
|
|
64
64
|
ok(port, msg.id, { errors });
|
|
65
65
|
} catch (e) {
|
|
66
66
|
fail(port, msg.id, e instanceof Error ? e : new Error(String(e)));
|
|
@@ -169,7 +169,7 @@ export interface HostCtx {
|
|
|
169
169
|
* worker would otherwise re-prime via `hydrateEventHistory` (issue #359
|
|
170
170
|
* extension). Absent when capture is off.
|
|
171
171
|
*/
|
|
172
|
-
captureFlush?: () => void
|
|
172
|
+
captureFlush?: () => Promise<void>;
|
|
173
173
|
/**
|
|
174
174
|
* Per-uid impersonation Firestore handles (Pyric Studio auth lens, T2).
|
|
175
175
|
* Keyed by the impersonated uid. Each is a FROZEN-identity
|