@pyric/cli 0.1.0-alpha.12 → 0.1.0-alpha.14
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 +5 -5
- package/dist/cli/serve.js.map +1 -1
- package/dist/serve/bundler.d.ts +15 -8
- package/dist/serve/bundler.d.ts.map +1 -1
- package/dist/serve/bundler.js +38 -37
- 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 +22 -0
- package/dist/serve/runtime/worker-generation.d.ts.map +1 -0
- package/dist/serve/runtime/worker-generation.js +77 -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 +27 -0
- package/dist/serve/vite-ai-config.d.ts.map +1 -0
- package/dist/serve/vite-ai-config.js +49 -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 +29 -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 +5 -5
- package/src/serve/bundler.ts +49 -16
- 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 +97 -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 +85 -0
- package/src/serve/vite-plugin.ts +61 -87
- package/src/serve/vite-rules-source.ts +28 -0
- package/src/serve/vite-worker-runtime.ts +58 -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,18 @@ import {
|
|
|
91
87
|
createLinePrefixer,
|
|
92
88
|
registerModuleUrl,
|
|
93
89
|
} from '../cli/dev-runner.js';
|
|
90
|
+
import {
|
|
91
|
+
loadViteAiEnv,
|
|
92
|
+
resolveViteAiConfig,
|
|
93
|
+
viteWorkerEpochSalt,
|
|
94
|
+
type PyricAiOptions,
|
|
95
|
+
} from './vite-ai-config.js';
|
|
96
|
+
import { resolveViteRulesConfig } from './vite-rules-source.js';
|
|
97
|
+
import {
|
|
98
|
+
PYRIC_RUNTIME_CHIP_META,
|
|
99
|
+
runtimeChipMetaValue,
|
|
100
|
+
type PyricRuntimeChipOption,
|
|
101
|
+
} from './runtime/chip-config.js';
|
|
94
102
|
|
|
95
103
|
/**
|
|
96
104
|
* Whether a `vite build` should run the firebase→pyric swap (produce a SANDBOX
|
|
@@ -109,41 +117,6 @@ function swapsInBuild(env: ConfigEnv, swapInBuild: boolean | undefined): boolean
|
|
|
109
117
|
/** Any `firebase/<sub>` specifier. */
|
|
110
118
|
const FB_ANY = /^firebase\/([a-z-]+(?:\/[a-z-]+)*)$/;
|
|
111
119
|
|
|
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
120
|
/** The firebase subpaths with swap entries. */
|
|
148
121
|
const SERVED = new Set(SDK_MODULES.map((specifier) => specifier.slice('firebase/'.length)));
|
|
149
122
|
const entryKey = (subpath: string): string => subpath.replaceAll('/', '-');
|
|
@@ -159,8 +132,9 @@ function packageRootOf(file: string): string {
|
|
|
159
132
|
}
|
|
160
133
|
|
|
161
134
|
export interface PyricOptions {
|
|
162
|
-
/**
|
|
163
|
-
* `firestore.rules`,
|
|
135
|
+
/** Firestore rules path (relative to `root`). Default discovery prefers an
|
|
136
|
+
* authored `firestore.modules.rules`, then `firebase.json`, then
|
|
137
|
+
* `firestore.rules` in the project root. */
|
|
164
138
|
rules?: string;
|
|
165
139
|
/** Project dir for `firebase.json` / rules discovery. Default: Vite's `root`. */
|
|
166
140
|
root?: string;
|
|
@@ -193,6 +167,10 @@ export interface PyricOptions {
|
|
|
193
167
|
* so app, Studio, and agent all observe the one sandbox); pass `ui: false`
|
|
194
168
|
* to disable. */
|
|
195
169
|
ui?: boolean;
|
|
170
|
+
/** Inject the collapsed Pyric runtime chip into the app during sandbox Vite
|
|
171
|
+
* dev/builds. On by default. Pass `false` to hide it, or
|
|
172
|
+
* `{ initiallyOpen: true }` when actively debugging runtime errors. */
|
|
173
|
+
runtimeChip?: PyricRuntimeChipOption;
|
|
196
174
|
/** RTDB-triggered Cloud Functions under this dev server (the `pyric dev`
|
|
197
175
|
* parity fold). By default a `functions` block in `firebase.json` is
|
|
198
176
|
* discovered automatically: its `onValueCreated` triggers run in an isolated
|
|
@@ -229,11 +207,14 @@ export interface PyricOptions {
|
|
|
229
207
|
*
|
|
230
208
|
* pyric({
|
|
231
209
|
* ai: {
|
|
232
|
-
*
|
|
210
|
+
* model: 'llama3.2',
|
|
233
211
|
* proxyUpstream: 'http://localhost:11434/v1', // your Ollama
|
|
234
212
|
* },
|
|
235
213
|
* })
|
|
236
214
|
*
|
|
215
|
+
* - `model` is the simple OpenAI-compatible path. It uses the same-origin
|
|
216
|
+
* proxy and becomes the catch-all upstream model. `PYRIC_AI_MODEL` selects
|
|
217
|
+
* the same path when neither `model` nor `engine` is explicit.
|
|
237
218
|
* - `engine` is `pyric/ai`'s `EngineConfig` (scripted | openai), applied on
|
|
238
219
|
* both the SharedWorker and in-page paths. An openai `baseUrl` of
|
|
239
220
|
* `/__pyric/ai-proxy` (or omitted) routes through the same-origin proxy so a
|
|
@@ -241,20 +222,11 @@ export interface PyricOptions {
|
|
|
241
222
|
* - `proxyUpstream` sets what `/__pyric/ai-proxy` forwards to (beats the
|
|
242
223
|
* `PYRIC_AI_PROXY_UPSTREAM` env var; default `http://localhost:11434/v1`).
|
|
243
224
|
*
|
|
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.
|
|
225
|
+
* Precedence: explicit `engine` or `model`, then `PYRIC_AI_MODEL`, then an
|
|
226
|
+
* engine passed by the app's first `getAI()` call; with none, the zero-config
|
|
227
|
+
* scripted default applies. `model` and `engine` are mutually exclusive.
|
|
249
228
|
*/
|
|
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
|
-
};
|
|
229
|
+
ai?: PyricAiOptions;
|
|
258
230
|
}
|
|
259
231
|
|
|
260
232
|
/**
|
|
@@ -264,6 +236,7 @@ export interface PyricOptions {
|
|
|
264
236
|
* export default defineConfig({ plugins: [pyric()] });
|
|
265
237
|
*/
|
|
266
238
|
export function pyric(options: PyricOptions = {}): Plugin {
|
|
239
|
+
let resolvedAi = resolveViteAiConfig(options.ai, {});
|
|
267
240
|
// Resolved once. `defaultSdkEntries()` prefers compiled dist `.js` and falls
|
|
268
241
|
// back to source `.ts` in the workspace.
|
|
269
242
|
const entries = defaultSdkEntries(); // { app, auth, firestore, init } → abs paths
|
|
@@ -335,11 +308,10 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
335
308
|
};
|
|
336
309
|
|
|
337
310
|
// 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;
|
|
311
|
+
// a still-running OLD worker is detected as stale. The collaborator becomes
|
|
312
|
+
// ready once its bundle succeeds; until then (or on bundle failure) the page
|
|
313
|
+
// is forced onto the in-page sandbox path. transformIndexHtml reads its tag.
|
|
314
|
+
const workerRuntime = createViteWorkerRuntime();
|
|
343
315
|
|
|
344
316
|
// Set by the `config` hook. When the plugin runs under `vite build` at all it
|
|
345
317
|
// is a SANDBOX build (the `apply` gate below only lets build through under the
|
|
@@ -364,12 +336,12 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
364
336
|
// so multi-tab is disabled under bridge to keep agent + app on one backend.
|
|
365
337
|
const bridgeOpts = options.bridge === true ? {} : options.bridge || null;
|
|
366
338
|
|
|
367
|
-
// Plugin-level AI engine, normalized
|
|
339
|
+
// Plugin-level AI engine, normalized to the JSON-safe wire shape. Travels
|
|
368
340
|
// 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
|
-
|
|
341
|
+
// fallback via an injected synchronous global (see transformIndexHtml).
|
|
342
|
+
// An explicit engine is available immediately (some hook tests call the HTML
|
|
343
|
+
// transform directly); Vite's config hook may otherwise select the simple
|
|
344
|
+
// PYRIC_AI_MODEL environment path for the active mode/root.
|
|
373
345
|
return {
|
|
374
346
|
name: 'pyric:sandbox',
|
|
375
347
|
// Active for `vite dev` ALWAYS, and for `vite build` only when it is a
|
|
@@ -378,14 +350,17 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
378
350
|
// production output. A sandbox build applies the same swap so the output
|
|
379
351
|
// bundles pyric's in-page adapters (self-contained; preview it under
|
|
380
352
|
// `pyric dev`, never deploy it).
|
|
381
|
-
apply(
|
|
353
|
+
apply(config, env) {
|
|
354
|
+
void config;
|
|
382
355
|
if (env.command === 'serve') return true;
|
|
383
356
|
return swapsInBuild(env, options.swapInBuild);
|
|
384
357
|
},
|
|
385
358
|
enforce: 'pre',
|
|
386
359
|
|
|
387
|
-
config(
|
|
360
|
+
config(config, env) {
|
|
388
361
|
sandboxBuild = env.command === 'build';
|
|
362
|
+
const loadedEnv = loadViteAiEnv(env.mode, config.root, config.envDir);
|
|
363
|
+
resolvedAi = resolveViteAiConfig(options.ai, loadedEnv);
|
|
389
364
|
// Cast: the `esbuild` package's `Plugin` type skews slightly from Vite's
|
|
390
365
|
// bundled esbuild types (benign — the Plugin shape is stable across the
|
|
391
366
|
// versions in range).
|
|
@@ -457,10 +432,11 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
457
432
|
} catch {
|
|
458
433
|
/* optional — serve without a firebase.json */
|
|
459
434
|
}
|
|
460
|
-
//
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
435
|
+
// Convention-first development source: an explicit option wins; otherwise
|
|
436
|
+
// an authored 2+modules file wins over firebase.json's generated deployment
|
|
437
|
+
// target. Projects without that convention retain the normal Firebase
|
|
438
|
+
// discovery path.
|
|
439
|
+
const config = resolveViteRulesConfig(cwd, options.rules, fbJson);
|
|
464
440
|
const loaded = await loadProjectRules(cwd, config);
|
|
465
441
|
const loadedDatabase = await loadProjectDatabaseRules(cwd, config);
|
|
466
442
|
const loadedStorage = await loadProjectStorageRules(cwd, config);
|
|
@@ -530,12 +506,10 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
530
506
|
|
|
531
507
|
// ── M2 SharedWorker host: bundle it (cached per version) and serve it at
|
|
532
508
|
// /__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);
|
|
509
|
+
// On bundle failure, the collaborator stays unready and its HTML tag
|
|
510
|
+
// forces the in-page sandbox.
|
|
536
511
|
try {
|
|
537
|
-
await
|
|
538
|
-
workerReady = true;
|
|
512
|
+
await workerRuntime.prepare(viteWorkerEpochSalt(cwd, resolvedAi.engineWire));
|
|
539
513
|
} catch (e) {
|
|
540
514
|
server.config.logger.warn(
|
|
541
515
|
` ⚠ [pyric] SharedWorker bundle failed — using the in-page sandbox (single-tab, ephemeral): ${e instanceof Error ? e.message : String(e)}`,
|
|
@@ -629,7 +603,7 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
629
603
|
messaging: true,
|
|
630
604
|
// Plugin-level engine → the worker host's ctx.aiEngine (host-ai.ts),
|
|
631
605
|
// which wins over any op-carried engine. Null when unset.
|
|
632
|
-
ai:
|
|
606
|
+
ai: resolvedAi.engineWire ? { engine: resolvedAi.engineWire } : null,
|
|
633
607
|
});
|
|
634
608
|
// Pyric Studio: mount the disk-backed workspace/project routes that
|
|
635
609
|
// Studio's `local` mode talks to + serve the built Studio app at
|
|
@@ -657,6 +631,7 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
657
631
|
);
|
|
658
632
|
}
|
|
659
633
|
}
|
|
634
|
+
const { sdkDir } = workerRuntime.status();
|
|
660
635
|
const namespace = createPyricNamespace({
|
|
661
636
|
sdkDir,
|
|
662
637
|
initPayload,
|
|
@@ -668,7 +643,7 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
668
643
|
studioUiDir,
|
|
669
644
|
// `ai.proxyUpstream`: what `/__pyric/ai-proxy` forwards to (beats the
|
|
670
645
|
// PYRIC_AI_PROXY_UPSTREAM env var; falls back to the default when unset).
|
|
671
|
-
aiProxyUpstream:
|
|
646
|
+
aiProxyUpstream: resolvedAi.proxyUpstream,
|
|
672
647
|
// Adapt Vite's logger to the plain info/note shape the namespace's
|
|
673
648
|
// diagnostics (denial relay, future hot-reload lines) expect —
|
|
674
649
|
// matches the `↻`/`⚠ [pyric]` lines already logged elsewhere in this
|
|
@@ -1047,6 +1022,7 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
1047
1022
|
},
|
|
1048
1023
|
|
|
1049
1024
|
transformIndexHtml(html) {
|
|
1025
|
+
const runtimeChipTag = `<meta name="${PYRIC_RUNTIME_CHIP_META}" content="${runtimeChipMetaValue(options.runtimeChip)}" data-studio="${options.ui === false ? 'off' : 'on'}" data-pyric-sandbox>`;
|
|
1050
1026
|
// Sandbox BUILD: the app's own `firebase/*` imports were already swapped
|
|
1051
1027
|
// (resolveId, above) to pyric's in-page adapters and BUNDLED into the app
|
|
1052
1028
|
// chunk, and the emitted init chunk (script-tagged here) carries the
|
|
@@ -1063,7 +1039,7 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
1063
1039
|
const initTag = initChunkFile
|
|
1064
1040
|
? `<script type="module" crossorigin src="/${initChunkFile}" data-pyric-sandbox-init></script>`
|
|
1065
1041
|
: '';
|
|
1066
|
-
const tags = SANDBOX_BUILD_META + initTag;
|
|
1042
|
+
const tags = SANDBOX_BUILD_META + runtimeChipTag + initTag;
|
|
1067
1043
|
return html.includes('</head>')
|
|
1068
1044
|
? html.replace('</head>', `${tags}</head>`)
|
|
1069
1045
|
: tags + html;
|
|
@@ -1076,26 +1052,24 @@ export function pyric(options: PyricOptions = {}): Plugin {
|
|
|
1076
1052
|
// module evaluates — a classic inline script runs before the deferred module.
|
|
1077
1053
|
// The flag (not nulling `window.SharedWorker`) leaves the user's own
|
|
1078
1054
|
// SharedWorker usage intact. We force in-page ONLY when the worker bundle
|
|
1079
|
-
// failed (
|
|
1055
|
+
// failed (worker runtime unready) — the ephemeral fallback. `bridge` no longer
|
|
1080
1056
|
// forces in-page: the bridge peer routes agent tool-calls THROUGH the
|
|
1081
1057
|
// worker (see `connectBridgePeer`), so the agent shares the one sandbox the
|
|
1082
1058
|
// app + Studio use.
|
|
1083
|
-
const head =
|
|
1084
|
-
? `<meta name="pyric-worker-v" content="${workerVersion}" ${MARKER}>`
|
|
1085
|
-
: `<script ${MARKER}>globalThis.__PYRIC_FORCE_INPAGE__=true;</script>`;
|
|
1059
|
+
const head = workerRuntime.headTag(MARKER);
|
|
1086
1060
|
// Plugin-level engine for the IN-PAGE path: a classic inline script runs
|
|
1087
1061
|
// before the deferred init module AND before app code's `getAI`, so the
|
|
1088
1062
|
// served `getAI` (entries/ai.ts) reads it synchronously — init.json can't
|
|
1089
1063
|
// be awaited there. Harmless on the worker path (that branch ignores the
|
|
1090
1064
|
// global; the worker reads ctx.aiEngine from init.json). `<` is escaped so
|
|
1091
1065
|
// an engine value can never break out of the script tag.
|
|
1092
|
-
const aiEngineTag =
|
|
1093
|
-
? `<script ${MARKER}>globalThis.__PYRIC_AI_ENGINE__=${JSON.stringify(
|
|
1066
|
+
const aiEngineTag = resolvedAi.engineWire
|
|
1067
|
+
? `<script ${MARKER}>globalThis.__PYRIC_AI_ENGINE__=${JSON.stringify(resolvedAi.engineWire).replace(/</g, '\\u003c')};</script>`
|
|
1094
1068
|
: '';
|
|
1095
1069
|
// Boot the sandbox by loading the real init entry as a module (Vite
|
|
1096
1070
|
// serves + transforms it). The init module's top-level await deploys rules
|
|
1097
1071
|
// before app code runs. Mirrors serve's injectServeTags.
|
|
1098
|
-
const tag = head + aiEngineTag + `<script type="module" src="/@fs/${entries.init}" ${MARKER}></script>`;
|
|
1072
|
+
const tag = head + aiEngineTag + runtimeChipTag + `<script type="module" src="/@fs/${entries.init}" ${MARKER}></script>`;
|
|
1099
1073
|
return html.includes('</head>') ? html.replace('</head>', `${tag}</head>`) : tag + html;
|
|
1100
1074
|
},
|
|
1101
1075
|
};
|
|
@@ -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,58 @@
|
|
|
1
|
+
import { homedir } from 'node:os';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import {
|
|
5
|
+
bundleWorker,
|
|
6
|
+
workerSourceHash,
|
|
7
|
+
type WorkerBundleOptions,
|
|
8
|
+
type WorkerBundleResult,
|
|
9
|
+
} from './bundler.js';
|
|
10
|
+
|
|
11
|
+
export interface ViteWorkerRuntimeStatus {
|
|
12
|
+
sdkDir: string;
|
|
13
|
+
ready: boolean;
|
|
14
|
+
epoch: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ViteWorkerRuntime {
|
|
18
|
+
prepare(epochSalt?: string): Promise<void>;
|
|
19
|
+
status(): ViteWorkerRuntimeStatus;
|
|
20
|
+
headTag(marker: string): string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ViteWorkerRuntimeOptions {
|
|
24
|
+
cacheRoot?: string;
|
|
25
|
+
cacheKey?: string;
|
|
26
|
+
bundle?: (options: WorkerBundleOptions) => Promise<WorkerBundleResult>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Own the Vite plugin's worker build state and its one HTML projection. */
|
|
30
|
+
export function createViteWorkerRuntime(
|
|
31
|
+
options: ViteWorkerRuntimeOptions = {},
|
|
32
|
+
): ViteWorkerRuntime {
|
|
33
|
+
const cacheRoot = options.cacheRoot ?? join(homedir(), '.pyric', 'vite-worker');
|
|
34
|
+
const cacheKey = options.cacheKey ?? workerSourceHash();
|
|
35
|
+
const defaultSdkDir = join(cacheRoot, cacheKey);
|
|
36
|
+
const bundle = options.bundle ?? bundleWorker;
|
|
37
|
+
let current: ViteWorkerRuntimeStatus = { sdkDir: defaultSdkDir, ready: false, epoch: null };
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
async prepare(epochSalt) {
|
|
41
|
+
const sdkDir = epochSalt
|
|
42
|
+
? join(
|
|
43
|
+
cacheRoot,
|
|
44
|
+
`${cacheKey}-${createHash('sha256').update(epochSalt).digest('hex').slice(0, 12)}`,
|
|
45
|
+
)
|
|
46
|
+
: defaultSdkDir;
|
|
47
|
+
current = { sdkDir, ready: false, epoch: null };
|
|
48
|
+
const result = await bundle({ outDir: sdkDir, ...(epochSalt ? { epochSalt } : {}) });
|
|
49
|
+
current = { sdkDir, ready: true, epoch: result.epoch };
|
|
50
|
+
},
|
|
51
|
+
status: () => current,
|
|
52
|
+
headTag(marker) {
|
|
53
|
+
return current.ready && current.epoch
|
|
54
|
+
? `<meta name="pyric-worker-v" content="${current.epoch}" ${marker}>`
|
|
55
|
+
: `<script ${marker}>globalThis.__PYRIC_FORCE_INPAGE__=true;</script>`;
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
}
|
|
@@ -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
|