@pyric/cli 0.1.0-alpha.11 → 0.1.0-alpha.12
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/conformance/.generated/can-i-use-browser.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js +1 -1
- package/dist/conformance/.generated/can-i-use-browser.js.map +1 -1
- package/dist/conformance/.generated/can-i-use.d.ts.map +1 -1
- package/dist/conformance/.generated/can-i-use.js +2 -2
- package/dist/conformance/.generated/can-i-use.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 +2 -2
- package/dist/serve/docs-ui/docs/api-reference.md +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 +1 -1
- 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 +1 -1
- package/dist/serve/docs-ui/docs/get-started/start-building/index.html +1 -1
- package/dist/serve/docs-ui/docs/index.json +200 -25
- package/dist/serve/docs-ui/docs/messaging-compat/index.html +1 -1
- 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 +916 -0
- package/dist/serve/docs-ui/docs/pyric-admin-messaging-reference-api.md +706 -0
- 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 +131 -66
- package/dist/serve/docs-ui/docs/pyric-cli-serve-worker-reference-api.md +130 -65
- 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 +1 -1
- 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 +37 -14
- package/dist/serve/docs-ui/docs/pyric-sandbox-reference-api.md +28 -8
- 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/llms.txt +1 -1
- package/dist/serve/worker/host/core.js +1 -1
- package/dist/serve/worker/host/core.js.map +1 -1
- package/dist/serve/worker/host/firestore-writes.d.ts.map +1 -1
- package/dist/serve/worker/host/firestore-writes.js +3 -2
- package/dist/serve/worker/host/firestore-writes.js.map +1 -1
- package/dist/serve/worker/index.d.ts +2 -1
- package/dist/serve/worker/index.d.ts.map +1 -1
- package/dist/serve/worker/index.js +2 -1
- package/dist/serve/worker/index.js.map +1 -1
- package/dist/serve/worker/protocol.js +2 -2
- package/dist/serve/worker/protocol.js.map +1 -1
- package/package.json +5 -4
- package/src/assurance/.generated/conformance-verdicts.ts +1090 -0
- package/src/assurance/attachment.ts +211 -0
- package/src/assurance/browser.ts +80 -0
- package/src/assurance/campaign.ts +496 -0
- package/src/assurance/capabilities.ts +446 -0
- package/src/assurance/cases.ts +43 -0
- package/src/assurance/index.ts +75 -0
- package/src/assurance/runner.ts +885 -0
- package/src/assurance/tool-names.ts +15 -0
- package/src/assurance/tools.ts +830 -0
- package/src/assurance/types.ts +352 -0
- package/src/assurance/validation.ts +434 -0
- package/src/bridge/client/bridge.ts +550 -0
- package/src/bridge/client/dispatch.ts +137 -0
- package/src/bridge/client.ts +45 -0
- package/src/bridge/protocol.ts +351 -0
- package/src/bridge/server/audit.ts +53 -0
- package/src/bridge/server/bridge.ts +597 -0
- package/src/bridge/server/headless.ts +160 -0
- package/src/bridge/server/json-schema-to-zod.ts +108 -0
- package/src/bridge/server/local-bridge.ts +66 -0
- package/src/bridge/server/logger.ts +50 -0
- package/src/bridge/server/mcp-contract.ts +93 -0
- package/src/bridge/server/mcp.ts +129 -0
- package/src/bridge/server/peer.ts +232 -0
- package/src/bridge/server/standalone.ts +347 -0
- package/src/bridge/server/tool-metadata.ts +99 -0
- package/src/bridge/server.ts +28 -0
- package/src/cli/can-i-use.ts +50 -0
- package/src/cli/cli.test.ts +493 -0
- package/src/cli/database-rules.ts +329 -0
- package/src/cli/dev-runner.ts +282 -0
- package/src/cli/firebase-json.ts +100 -0
- package/src/cli/firestore-indexes.ts +70 -0
- package/src/cli/index.ts +425 -0
- package/src/cli/init.ts +260 -0
- package/src/cli/mcp-proxy.ts +196 -0
- package/src/cli/parse-args.ts +80 -0
- package/src/cli/rules.ts +259 -0
- package/src/cli/scope.ts +67 -0
- package/src/cli/serve.ts +996 -0
- package/src/cli/service-commands.ts +73 -0
- package/src/cli/snapshot.ts +164 -0
- package/src/cli/storage-rules.ts +172 -0
- package/src/cli/verify.ts +416 -0
- package/src/conformance/.generated/can-i-use-browser.ts +113 -0
- package/src/conformance/.generated/can-i-use.ts +117 -0
- package/src/conformance/.generated/conformance-docs.ts +14 -0
- package/src/conformance/browser.ts +24 -0
- package/src/conformance/can-i-use-tool.ts +51 -0
- package/src/conformance/can-i-use.ts +18 -0
- package/src/conformance/docs.ts +8 -0
- package/src/conformance/index.ts +20 -0
- package/src/conformance/tools.ts +16 -0
- package/src/credentials/core/memoize-ttl.ts +142 -0
- package/src/credentials/core/types.ts +10 -0
- package/src/credentials/node/from-adc.ts +96 -0
- package/src/credentials/node/from-service-account.ts +133 -0
- package/src/credentials/node/index.ts +6 -0
- package/src/discover/concurrency.ts +127 -0
- package/src/discover/crawler-adapter.ts +142 -0
- package/src/discover/crawler.ts +1127 -0
- package/src/discover/credential-free.ts +27 -0
- package/src/discover/findCollectionGroup.ts +131 -0
- package/src/discover/firestore-source.ts +59 -0
- package/src/discover/index.ts +5 -0
- package/src/discover/merge.ts +523 -0
- package/src/discover/session.ts +402 -0
- package/src/discover/tools.ts +201 -0
- package/src/discover/types.ts +187 -0
- package/src/discover/wire.ts +324 -0
- package/src/functions-rtdb/child.ts +382 -0
- package/src/functions-rtdb/delivery.ts +7 -0
- package/src/functions-rtdb/discovery.ts +136 -0
- package/src/functions-rtdb/event.ts +64 -0
- package/src/functions-rtdb/execution.ts +111 -0
- package/src/functions-rtdb/in-memory-delivery.ts +40 -0
- package/src/functions-rtdb/project.ts +74 -0
- package/src/functions-rtdb/projection.ts +104 -0
- package/src/functions-rtdb/reference-pattern.ts +20 -0
- package/src/functions-rtdb/remote-delivery.ts +23 -0
- package/src/pkg-version.ts +49 -0
- package/src/register/esm-exports.ts +63 -0
- package/src/register/hooks.ts +30 -0
- package/src/register/index.ts +150 -0
- package/src/register/mapping.ts +33 -0
- package/src/remote/index.ts +1050 -0
- package/src/rtdb/crawl-snapshot.ts +118 -0
- package/src/rtdb/inspection.ts +137 -0
- package/src/rtdb/load-rules-document.ts +56 -0
- package/src/rtdb/rules-generation-tool.ts +38 -0
- package/src/rtdb/rules-json.ts +29 -0
- package/src/serve/activity-guard.ts +29 -0
- package/src/serve/activity-route.ts +164 -0
- package/src/serve/activity-warning.ts +26 -0
- package/src/serve/bridge-mount.ts +200 -0
- package/src/serve/bundler.ts +493 -0
- package/src/serve/capture-store.ts +65 -0
- package/src/serve/discovery.ts +199 -0
- package/src/serve/entries/ai.ts +292 -0
- package/src/serve/entries/app-backend.ts +4 -0
- package/src/serve/entries/app-client.ts +26 -0
- package/src/serve/entries/app-session-store.ts +47 -0
- package/src/serve/entries/app.ts +11 -0
- package/src/serve/entries/auth-helper-core.ts +181 -0
- package/src/serve/entries/auth-helper-dom.ts +134 -0
- package/src/serve/entries/auth-helper-runtime.ts +20 -0
- package/src/serve/entries/auth.ts +255 -0
- package/src/serve/entries/bridge-url.ts +31 -0
- package/src/serve/entries/database.ts +129 -0
- package/src/serve/entries/firestore.ts +232 -0
- package/src/serve/entries/init.ts +46 -0
- package/src/serve/entries/keepalive.ts +46 -0
- package/src/serve/entries/messaging-sw.ts +75 -0
- package/src/serve/entries/messaging.ts +96 -0
- package/src/serve/entries/runtime.ts +540 -0
- package/src/serve/entries/session-store.ts +97 -0
- package/src/serve/entries/storage.ts +92 -0
- package/src/serve/entries/tab-sync-wiring.ts +274 -0
- package/src/serve/entries/worker-runtime.ts +75 -0
- package/src/serve/init-payload.ts +42 -0
- package/src/serve/namespace.ts +675 -0
- package/src/serve/open-browser.ts +68 -0
- package/src/serve/rules.ts +272 -0
- package/src/serve/sandbox-marker.ts +31 -0
- package/src/serve/server.ts +428 -0
- package/src/serve/standalone-assets.ts +180 -0
- package/src/serve/state-store.ts +166 -0
- package/src/serve/studio/disk-project-store.ts +185 -0
- package/src/serve/studio/disk-workspace.ts +162 -0
- package/src/serve/studio/index.ts +20 -0
- package/src/serve/studio/routes.ts +237 -0
- package/src/serve/studio/store-types.ts +48 -0
- package/src/serve/studio/studio-storage.test.ts +289 -0
- package/src/serve/vite-plugin.ts +1102 -0
- package/src/serve/worker/activity-bootstrap.ts +24 -0
- package/src/serve/worker/client/admin-firestore.ts +38 -0
- package/src/serve/worker/client/ai.ts +124 -0
- package/src/serve/worker/client/auth.ts +489 -0
- package/src/serve/worker/client/connection.ts +202 -0
- package/src/serve/worker/client/core.ts +350 -0
- package/src/serve/worker/client/disconnect.ts +45 -0
- package/src/serve/worker/client/firestore-reads.ts +181 -0
- package/src/serve/worker/client/firestore-refs.ts +226 -0
- package/src/serve/worker/client/firestore-writes.ts +232 -0
- package/src/serve/worker/client/handles.ts +91 -0
- package/src/serve/worker/client/messaging.ts +119 -0
- package/src/serve/worker/client/presence.ts +174 -0
- package/src/serve/worker/client/rtdb.ts +311 -0
- package/src/serve/worker/client/rules.ts +63 -0
- package/src/serve/worker/client/service-worker-connection.ts +59 -0
- package/src/serve/worker/client/snapshots.ts +86 -0
- package/src/serve/worker/client/storage.ts +195 -0
- package/src/serve/worker/client/studio.ts +88 -0
- package/src/serve/worker/client.ts +57 -0
- package/src/serve/worker/durable-persistence.ts +137 -0
- package/src/serve/worker/entry.ts +179 -0
- package/src/serve/worker/host/admin-firestore.ts +88 -0
- package/src/serve/worker/host/connection.ts +179 -0
- package/src/serve/worker/host/core.ts +387 -0
- package/src/serve/worker/host/dispatch.ts +316 -0
- package/src/serve/worker/host/firestore-reads.ts +117 -0
- package/src/serve/worker/host/firestore-writes.ts +456 -0
- package/src/serve/worker/host/presence.ts +312 -0
- package/src/serve/worker/host/rtdb.ts +136 -0
- package/src/serve/worker/host/rules.ts +128 -0
- package/src/serve/worker/host/storage.ts +312 -0
- package/src/serve/worker/host/studio.ts +75 -0
- package/src/serve/worker/host/subscriptions.ts +238 -0
- package/src/serve/worker/host-ai.ts +165 -0
- package/src/serve/worker/host-auth.ts +469 -0
- package/src/serve/worker/host-context.ts +273 -0
- package/src/serve/worker/host-events.ts +94 -0
- package/src/serve/worker/host-messaging.ts +240 -0
- package/src/serve/worker/host.ts +47 -0
- package/src/serve/worker/index.ts +196 -0
- package/src/serve/worker/presence-timing.ts +15 -0
- package/src/serve/worker/protocol.ts +1112 -0
- package/src/serve/worker/serve-init.ts +606 -0
- package/src/serve/worker/service-worker-channel.ts +33 -0
- package/src/serve/worker/service-worker-relay.ts +86 -0
- package/src/serve/writer-lock.ts +53 -0
- package/src/verify/cases.ts +233 -0
- package/src/verify/fixture.ts +258 -0
- package/src/verify/index.ts +519 -0
- package/src/verify/tools.ts +108 -0
- package/src/version/compat-target.ts +17 -0
- package/src/vite.ts +17 -0
- package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api/index.html +0 -31
- package/dist/serve/docs-ui/docs/pyric-firestore-values-reference-api.md +0 -29
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { spawn, type ChildProcess } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { createRequire } from 'node:module';
|
|
5
|
+
import { dirname, extname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
7
|
+
import type { RemoteSandbox } from '../remote/index.js';
|
|
8
|
+
import {
|
|
9
|
+
startOnValueCreatedExecution,
|
|
10
|
+
type OnValueCreatedExecutionHost,
|
|
11
|
+
} from './execution.js';
|
|
12
|
+
import {
|
|
13
|
+
inspectOnValueCreated,
|
|
14
|
+
listFirebaseEndpoints,
|
|
15
|
+
} from './discovery.js';
|
|
16
|
+
import type { CreatedExecutionResult } from './event.js';
|
|
17
|
+
import { RemoteRtdbTriggerDelivery } from './remote-delivery.js';
|
|
18
|
+
|
|
19
|
+
export interface SerializedFunctionsRtdbError {
|
|
20
|
+
name: string;
|
|
21
|
+
message: string;
|
|
22
|
+
stack?: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type FunctionsRtdbChildEvent =
|
|
26
|
+
| {
|
|
27
|
+
type: 'execution';
|
|
28
|
+
exportName: string;
|
|
29
|
+
ref: string;
|
|
30
|
+
params: Record<string, string>;
|
|
31
|
+
status: 'fulfilled';
|
|
32
|
+
}
|
|
33
|
+
| {
|
|
34
|
+
type: 'execution';
|
|
35
|
+
exportName: string;
|
|
36
|
+
ref: string;
|
|
37
|
+
params: Record<string, string>;
|
|
38
|
+
status: 'rejected';
|
|
39
|
+
error: SerializedFunctionsRtdbError;
|
|
40
|
+
}
|
|
41
|
+
| {
|
|
42
|
+
type: 'delivery-error';
|
|
43
|
+
exportName: string;
|
|
44
|
+
error: SerializedFunctionsRtdbError;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export interface FunctionsRtdbChildReady {
|
|
48
|
+
triggerCount: number;
|
|
49
|
+
unsupportedTriggers: UnsupportedFunctionsTrigger[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface UnsupportedFunctionsTrigger {
|
|
53
|
+
exportName: string;
|
|
54
|
+
eventType: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface SpawnFunctionsRtdbChildOptions {
|
|
58
|
+
cwd: string;
|
|
59
|
+
entry: string;
|
|
60
|
+
env: NodeJS.ProcessEnv;
|
|
61
|
+
instance: string;
|
|
62
|
+
location: string;
|
|
63
|
+
databaseHost?: string;
|
|
64
|
+
childModuleUrl?: string | URL;
|
|
65
|
+
nodeExecutable?: string;
|
|
66
|
+
onEvent?(event: FunctionsRtdbChildEvent): void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface FunctionsRtdbChildHandle {
|
|
70
|
+
child: ChildProcess;
|
|
71
|
+
ready: Promise<FunctionsRtdbChildReady>;
|
|
72
|
+
exited: Promise<number>;
|
|
73
|
+
stop(): Promise<number>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type FunctionsRtdbChildMessage =
|
|
77
|
+
| ({ type: 'ready' } & FunctionsRtdbChildReady)
|
|
78
|
+
| FunctionsRtdbChildEvent
|
|
79
|
+
| { type: 'fatal'; error: SerializedFunctionsRtdbError }
|
|
80
|
+
| { type: 'stopped' };
|
|
81
|
+
|
|
82
|
+
const FORCE_KILL_AFTER_MS = 2_000;
|
|
83
|
+
|
|
84
|
+
function serializeError(error: unknown): SerializedFunctionsRtdbError {
|
|
85
|
+
if (error instanceof Error) {
|
|
86
|
+
return {
|
|
87
|
+
name: error.name,
|
|
88
|
+
message: error.message,
|
|
89
|
+
...(error.stack === undefined ? {} : { stack: error.stack }),
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { name: 'Error', message: String(error) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function isChildMessage(value: unknown): value is FunctionsRtdbChildMessage {
|
|
96
|
+
return typeof value === 'object' && value !== null && 'type' in value;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Spawn the real Firebase Functions SDK in an isolated Node process. */
|
|
100
|
+
export function spawnFunctionsRtdbChild(
|
|
101
|
+
options: SpawnFunctionsRtdbChildOptions,
|
|
102
|
+
): FunctionsRtdbChildHandle {
|
|
103
|
+
const childModuleUrl = options.childModuleUrl ?? new URL('./child.js', import.meta.url);
|
|
104
|
+
const childModulePath = typeof childModuleUrl === 'string' && !childModuleUrl.startsWith('file:')
|
|
105
|
+
? resolve(childModuleUrl)
|
|
106
|
+
: fileURLToPath(childModuleUrl);
|
|
107
|
+
const child = spawn(options.nodeExecutable ?? 'node', [childModulePath], {
|
|
108
|
+
cwd: options.cwd,
|
|
109
|
+
env: {
|
|
110
|
+
...options.env,
|
|
111
|
+
PYRIC_FUNCTIONS_RTDB_CHILD: '1',
|
|
112
|
+
PYRIC_FUNCTIONS_ENTRY: resolve(options.entry),
|
|
113
|
+
PYRIC_FUNCTIONS_INSTANCE: options.instance,
|
|
114
|
+
PYRIC_FUNCTIONS_LOCATION: options.location,
|
|
115
|
+
PYRIC_FUNCTIONS_DATABASE_HOST: options.databaseHost ?? 'firebasedatabase.app',
|
|
116
|
+
},
|
|
117
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
let readySettled = false;
|
|
121
|
+
let stopping = false;
|
|
122
|
+
let stderr = '';
|
|
123
|
+
child.stderr?.setEncoding('utf8');
|
|
124
|
+
child.stderr?.on('data', (chunk: string) => {
|
|
125
|
+
stderr = `${stderr}${chunk}`.slice(-8_192);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
let resolveReady!: (ready: FunctionsRtdbChildReady) => void;
|
|
129
|
+
let rejectReady!: (error: Error) => void;
|
|
130
|
+
const ready = new Promise<FunctionsRtdbChildReady>((resolvePromise, rejectPromise) => {
|
|
131
|
+
resolveReady = resolvePromise;
|
|
132
|
+
rejectReady = rejectPromise;
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
child.on('message', (raw: unknown) => {
|
|
136
|
+
if (!isChildMessage(raw)) return;
|
|
137
|
+
if (raw.type === 'ready') {
|
|
138
|
+
readySettled = true;
|
|
139
|
+
resolveReady({
|
|
140
|
+
triggerCount: raw.triggerCount,
|
|
141
|
+
unsupportedTriggers: raw.unsupportedTriggers,
|
|
142
|
+
});
|
|
143
|
+
} else if (raw.type === 'fatal') {
|
|
144
|
+
if (!readySettled) {
|
|
145
|
+
readySettled = true;
|
|
146
|
+
rejectReady(new Error(
|
|
147
|
+
`Functions RTDB child failed: ${raw.error.stack ?? raw.error.message}`,
|
|
148
|
+
));
|
|
149
|
+
}
|
|
150
|
+
} else if (raw.type === 'execution' || raw.type === 'delivery-error') {
|
|
151
|
+
options.onEvent?.(raw);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
let forceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
156
|
+
const exited = new Promise<number>((resolveExited) => {
|
|
157
|
+
child.once('error', (error) => {
|
|
158
|
+
if (!readySettled) {
|
|
159
|
+
readySettled = true;
|
|
160
|
+
rejectReady(error);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
child.once('close', (code, signal) => {
|
|
164
|
+
if (forceTimer !== undefined) clearTimeout(forceTimer);
|
|
165
|
+
const exitCode = typeof code === 'number' ? code : signal ? 1 : 0;
|
|
166
|
+
if (!readySettled) {
|
|
167
|
+
readySettled = true;
|
|
168
|
+
const diagnostic = stderr.trim();
|
|
169
|
+
rejectReady(new Error(
|
|
170
|
+
`Functions RTDB child exited before ready (code ${exitCode})` +
|
|
171
|
+
(diagnostic ? `\n${diagnostic}` : ''),
|
|
172
|
+
));
|
|
173
|
+
}
|
|
174
|
+
resolveExited(exitCode);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
child,
|
|
180
|
+
ready,
|
|
181
|
+
exited,
|
|
182
|
+
async stop(): Promise<number> {
|
|
183
|
+
if (child.exitCode !== null || child.signalCode !== null) return exited;
|
|
184
|
+
if (!stopping) {
|
|
185
|
+
stopping = true;
|
|
186
|
+
if (child.connected) child.send({ type: 'stop' });
|
|
187
|
+
else child.kill('SIGTERM');
|
|
188
|
+
forceTimer = setTimeout(() => {
|
|
189
|
+
if (child.exitCode === null && child.signalCode === null) child.kill('SIGKILL');
|
|
190
|
+
}, FORCE_KILL_AFTER_MS);
|
|
191
|
+
forceTimer.unref();
|
|
192
|
+
}
|
|
193
|
+
return exited;
|
|
194
|
+
},
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
interface AdminAppModule {
|
|
199
|
+
initializeApp(): { sandbox: RemoteSandbox };
|
|
200
|
+
deleteApp(app: unknown): Promise<void>;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function send(message: FunctionsRtdbChildMessage): void {
|
|
204
|
+
process.send?.(message);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function usesCommonJs(entry: string): boolean {
|
|
208
|
+
const extension = extname(entry);
|
|
209
|
+
if (extension === '.cjs') return true;
|
|
210
|
+
if (extension === '.mjs') return false;
|
|
211
|
+
|
|
212
|
+
let directory = dirname(entry);
|
|
213
|
+
while (true) {
|
|
214
|
+
const packageJsonPath = join(directory, 'package.json');
|
|
215
|
+
if (existsSync(packageJsonPath)) {
|
|
216
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')) as {
|
|
217
|
+
type?: unknown;
|
|
218
|
+
};
|
|
219
|
+
return packageJson.type !== 'module';
|
|
220
|
+
}
|
|
221
|
+
const parent = dirname(directory);
|
|
222
|
+
if (parent === directory) return true;
|
|
223
|
+
directory = parent;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Load the entry with the semantics selected by its extension and package scope. */
|
|
228
|
+
async function loadFunctionsExports(entry: string): Promise<Record<string, unknown>> {
|
|
229
|
+
if (usesCommonJs(entry)) {
|
|
230
|
+
return createRequire(entry)(entry) as Record<string, unknown>;
|
|
231
|
+
}
|
|
232
|
+
return await import(pathToFileURL(entry).href) as Record<string, unknown>;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function runFunctionsRtdbChild(): Promise<void> {
|
|
236
|
+
const entry = process.env.PYRIC_FUNCTIONS_ENTRY;
|
|
237
|
+
const instance = process.env.PYRIC_FUNCTIONS_INSTANCE;
|
|
238
|
+
const location = process.env.PYRIC_FUNCTIONS_LOCATION;
|
|
239
|
+
const databaseHost = process.env.PYRIC_FUNCTIONS_DATABASE_HOST;
|
|
240
|
+
if (!entry || !instance || !location || !databaseHost) {
|
|
241
|
+
throw new Error('Functions RTDB child is missing its required environment');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const requireFromEntry = createRequire(entry);
|
|
245
|
+
const adminApp = requireFromEntry('firebase-admin/app') as AdminAppModule;
|
|
246
|
+
const app = adminApp.initializeApp();
|
|
247
|
+
let host: OnValueCreatedExecutionHost | undefined;
|
|
248
|
+
let closing: Promise<void> | undefined;
|
|
249
|
+
|
|
250
|
+
const close = (): Promise<void> => {
|
|
251
|
+
if (closing) return closing;
|
|
252
|
+
closing = (async () => {
|
|
253
|
+
host?.close();
|
|
254
|
+
await host?.idle();
|
|
255
|
+
app.sandbox.close();
|
|
256
|
+
await adminApp.deleteApp(app);
|
|
257
|
+
})();
|
|
258
|
+
return closing;
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const shutdown = async (): Promise<void> => {
|
|
262
|
+
await close();
|
|
263
|
+
send({ type: 'stopped' });
|
|
264
|
+
process.disconnect?.();
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
process.once('message', (raw: unknown) => {
|
|
268
|
+
if (isChildMessage(raw) && raw.type === 'stopped') return;
|
|
269
|
+
if (typeof raw === 'object' && raw !== null && 'type' in raw && raw.type === 'stop') {
|
|
270
|
+
void shutdown().catch((error) => {
|
|
271
|
+
send({ type: 'fatal', error: serializeError(error) });
|
|
272
|
+
process.exitCode = 1;
|
|
273
|
+
process.disconnect?.();
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
process.once('SIGINT', () => void shutdown());
|
|
278
|
+
process.once('SIGTERM', () => void shutdown());
|
|
279
|
+
|
|
280
|
+
try {
|
|
281
|
+
// The real database provider asks firebase-admin/app for its default app
|
|
282
|
+
// when it wraps a raw CloudEvent. Initializing that app before loading the
|
|
283
|
+
// user's module avoids importing the broad firebase-functions/v2 barrel.
|
|
284
|
+
const exported = await loadFunctionsExports(entry);
|
|
285
|
+
const effectiveInstances = [...new Set(
|
|
286
|
+
inspectOnValueCreated(exported).triggers.map((trigger) =>
|
|
287
|
+
trigger.instance === '*' ? instance : trigger.instance,
|
|
288
|
+
),
|
|
289
|
+
)].sort();
|
|
290
|
+
if (effectiveInstances.length > 1) {
|
|
291
|
+
throw new Error(
|
|
292
|
+
'Functions RTDB first slice supports one database instance; found ' +
|
|
293
|
+
effectiveInstances.join(', '),
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
host = startOnValueCreatedExecution({
|
|
297
|
+
exported,
|
|
298
|
+
delivery: new RemoteRtdbTriggerDelivery(app.sandbox.rtdb),
|
|
299
|
+
eventOptions: (_projection, sequence, trigger) => ({
|
|
300
|
+
id: `${randomUUID()}-${sequence}`,
|
|
301
|
+
time: new Date().toISOString(),
|
|
302
|
+
instance: trigger.instance === '*' ? instance : trigger.instance,
|
|
303
|
+
location: trigger.location ?? location,
|
|
304
|
+
databaseHost,
|
|
305
|
+
}),
|
|
306
|
+
onExecution(result, trigger, projection) {
|
|
307
|
+
sendExecution(result, trigger.exportName, projection.ref, projection.params);
|
|
308
|
+
},
|
|
309
|
+
onDeliveryError(error, trigger) {
|
|
310
|
+
send({
|
|
311
|
+
type: 'delivery-error',
|
|
312
|
+
exportName: trigger.exportName,
|
|
313
|
+
error: serializeError(error),
|
|
314
|
+
});
|
|
315
|
+
},
|
|
316
|
+
});
|
|
317
|
+
await host.ready;
|
|
318
|
+
send({
|
|
319
|
+
type: 'ready',
|
|
320
|
+
triggerCount: host.triggerCount,
|
|
321
|
+
unsupportedTriggers: findUnsupportedTriggers(exported),
|
|
322
|
+
});
|
|
323
|
+
} catch (error) {
|
|
324
|
+
await close();
|
|
325
|
+
throw error;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function findUnsupportedTriggers(exported: Record<string, unknown>): UnsupportedFunctionsTrigger[] {
|
|
330
|
+
const unsupported: UnsupportedFunctionsTrigger[] = [
|
|
331
|
+
...inspectOnValueCreated(exported).unsupported,
|
|
332
|
+
];
|
|
333
|
+
for (const { exportName, callable } of listFirebaseEndpoints(exported)) {
|
|
334
|
+
const endpoint = callable.__endpoint;
|
|
335
|
+
if (!endpoint) continue;
|
|
336
|
+
const eventType = endpoint.eventTrigger?.eventType;
|
|
337
|
+
if (eventType === 'google.firebase.database.ref.v1.created') continue;
|
|
338
|
+
const label = typeof eventType === 'string'
|
|
339
|
+
? eventType
|
|
340
|
+
: endpoint.callableTrigger !== undefined
|
|
341
|
+
? 'callable'
|
|
342
|
+
: endpoint.httpsTrigger !== undefined
|
|
343
|
+
? 'https'
|
|
344
|
+
: endpoint.scheduleTrigger !== undefined
|
|
345
|
+
? 'schedule'
|
|
346
|
+
: endpoint.taskQueueTrigger !== undefined
|
|
347
|
+
? 'task-queue'
|
|
348
|
+
: 'unknown';
|
|
349
|
+
unsupported.push({ exportName, eventType: label });
|
|
350
|
+
}
|
|
351
|
+
return unsupported;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function sendExecution(
|
|
355
|
+
result: CreatedExecutionResult,
|
|
356
|
+
exportName: string,
|
|
357
|
+
ref: string,
|
|
358
|
+
params: Record<string, string>,
|
|
359
|
+
): void {
|
|
360
|
+
if (result.status === 'fulfilled') {
|
|
361
|
+
send({ type: 'execution', exportName, ref, params, status: 'fulfilled' });
|
|
362
|
+
} else {
|
|
363
|
+
send({
|
|
364
|
+
type: 'execution',
|
|
365
|
+
exportName,
|
|
366
|
+
ref,
|
|
367
|
+
params,
|
|
368
|
+
status: 'rejected',
|
|
369
|
+
error: serializeError(result.error),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (process.env.PYRIC_FUNCTIONS_RTDB_CHILD === '1') {
|
|
375
|
+
void runFunctionsRtdbChild().catch((error) => {
|
|
376
|
+
const serialized = serializeError(error);
|
|
377
|
+
send({ type: 'fatal', error: serialized });
|
|
378
|
+
process.stderr.write(`${serialized.stack ?? `${serialized.name}: ${serialized.message}`}\n`);
|
|
379
|
+
process.exitCode = 1;
|
|
380
|
+
process.disconnect?.();
|
|
381
|
+
});
|
|
382
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeRtdbReference,
|
|
3
|
+
supportsRtdbReference,
|
|
4
|
+
} from './reference-pattern.js';
|
|
5
|
+
|
|
6
|
+
export type RtdbCreatedCallable = (
|
|
7
|
+
rawEvent: Record<string, unknown>,
|
|
8
|
+
) => unknown | Promise<unknown>;
|
|
9
|
+
|
|
10
|
+
export interface DiscoveredOnValueCreated {
|
|
11
|
+
exportName: string;
|
|
12
|
+
reference: string;
|
|
13
|
+
instance: string;
|
|
14
|
+
location?: string;
|
|
15
|
+
callable: RtdbCreatedCallable;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface UnsupportedOnValueCreated {
|
|
19
|
+
exportName: string;
|
|
20
|
+
eventType: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface OnValueCreatedInspection {
|
|
24
|
+
triggers: DiscoveredOnValueCreated[];
|
|
25
|
+
unsupported: UnsupportedOnValueCreated[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const CREATED_EVENT_TYPE = 'google.firebase.database.ref.v1.created';
|
|
29
|
+
|
|
30
|
+
export type FirebaseEndpointFunction = RtdbCreatedCallable & {
|
|
31
|
+
__endpoint?: {
|
|
32
|
+
omit?: unknown;
|
|
33
|
+
region?: unknown;
|
|
34
|
+
eventTrigger?: {
|
|
35
|
+
eventType?: unknown;
|
|
36
|
+
eventFilters?: Record<string, unknown>;
|
|
37
|
+
eventFilterPathPatterns?: Record<string, unknown>;
|
|
38
|
+
};
|
|
39
|
+
callableTrigger?: unknown;
|
|
40
|
+
httpsTrigger?: unknown;
|
|
41
|
+
scheduleTrigger?: unknown;
|
|
42
|
+
taskQueueTrigger?: unknown;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Walk exports using the same hyphenated nested names as Firebase's loader. */
|
|
47
|
+
export function listFirebaseEndpoints(
|
|
48
|
+
exported: Record<string, unknown>,
|
|
49
|
+
): Array<{ exportName: string; callable: FirebaseEndpointFunction }> {
|
|
50
|
+
const endpoints: Array<{ exportName: string; callable: FirebaseEndpointFunction }> = [];
|
|
51
|
+
const visit = (
|
|
52
|
+
value: Record<string, unknown>,
|
|
53
|
+
prefix: string,
|
|
54
|
+
ancestors: ReadonlySet<object>,
|
|
55
|
+
): void => {
|
|
56
|
+
if (ancestors.has(value)) return;
|
|
57
|
+
const nestedAncestors = new Set(ancestors).add(value);
|
|
58
|
+
for (const [name, candidate] of Object.entries(value)) {
|
|
59
|
+
const exportName = `${prefix}${name}`;
|
|
60
|
+
const callable = candidate as FirebaseEndpointFunction;
|
|
61
|
+
if (typeof candidate === 'function' && callable.__endpoint) {
|
|
62
|
+
endpoints.push({ exportName, callable });
|
|
63
|
+
} else if (typeof candidate === 'object' && candidate !== null) {
|
|
64
|
+
visit(candidate as Record<string, unknown>, `${exportName}-`, nestedAncestors);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
visit(exported, '', new Set());
|
|
69
|
+
return endpoints;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Classify unchanged v2 RTDB create exports against the admitted first slice. */
|
|
73
|
+
export function inspectOnValueCreated(
|
|
74
|
+
exported: Record<string, unknown>,
|
|
75
|
+
): OnValueCreatedInspection {
|
|
76
|
+
const triggers: DiscoveredOnValueCreated[] = [];
|
|
77
|
+
const unsupported: UnsupportedOnValueCreated[] = [];
|
|
78
|
+
for (const { exportName, callable } of listFirebaseEndpoints(exported)) {
|
|
79
|
+
const endpoint = callable.__endpoint;
|
|
80
|
+
const trigger = endpoint?.eventTrigger;
|
|
81
|
+
if (trigger?.eventType !== CREATED_EVENT_TYPE) continue;
|
|
82
|
+
if (endpoint?.omit === true) {
|
|
83
|
+
unsupported.push({
|
|
84
|
+
exportName,
|
|
85
|
+
eventType: `${CREATED_EVENT_TYPE} (omitted from emulation)`,
|
|
86
|
+
});
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (endpoint?.omit !== undefined && endpoint.omit !== false) {
|
|
90
|
+
unsupported.push({
|
|
91
|
+
exportName,
|
|
92
|
+
eventType: `${CREATED_EVENT_TYPE} (unsupported dynamic omit option)`,
|
|
93
|
+
});
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
const reference = trigger.eventFilterPathPatterns?.ref;
|
|
97
|
+
const exactInstance = trigger.eventFilters?.instance;
|
|
98
|
+
const instancePattern = trigger.eventFilterPathPatterns?.instance;
|
|
99
|
+
if (typeof reference !== 'string') continue;
|
|
100
|
+
if (typeof exactInstance !== 'string' && instancePattern !== '*') {
|
|
101
|
+
if (typeof instancePattern === 'string') {
|
|
102
|
+
unsupported.push({
|
|
103
|
+
exportName,
|
|
104
|
+
eventType: `${CREATED_EVENT_TYPE} (unsupported instance pattern: ${instancePattern})`,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const instance = typeof exactInstance === 'string' ? exactInstance : '*';
|
|
110
|
+
if (!supportsRtdbReference(reference)) {
|
|
111
|
+
unsupported.push({
|
|
112
|
+
exportName,
|
|
113
|
+
eventType: `${CREATED_EVENT_TYPE} (unsupported ref pattern: ${reference})`,
|
|
114
|
+
});
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const region = endpoint?.region;
|
|
118
|
+
const location = Array.isArray(region) && typeof region[0] === 'string'
|
|
119
|
+
? region[0]
|
|
120
|
+
: undefined;
|
|
121
|
+
triggers.push({
|
|
122
|
+
exportName,
|
|
123
|
+
reference: normalizeRtdbReference(reference),
|
|
124
|
+
instance,
|
|
125
|
+
...(location === undefined ? {} : { location }),
|
|
126
|
+
callable,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
return { triggers, unsupported };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function discoverOnValueCreated(
|
|
133
|
+
exported: Record<string, unknown>,
|
|
134
|
+
): DiscoveredOnValueCreated[] {
|
|
135
|
+
return inspectOnValueCreated(exported).triggers;
|
|
136
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { CREATED_EVENT_TYPE, type DiscoveredOnValueCreated } from './discovery.js';
|
|
2
|
+
import type { CreatedValueProjection } from './projection.js';
|
|
3
|
+
|
|
4
|
+
export interface CreatedEventOptions {
|
|
5
|
+
id: string;
|
|
6
|
+
time: string;
|
|
7
|
+
instance: string;
|
|
8
|
+
location: string;
|
|
9
|
+
databaseHost: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export type CreatedExecutionResult =
|
|
13
|
+
| { status: 'fulfilled'; event: Record<string, unknown> }
|
|
14
|
+
| { status: 'rejected'; event: Record<string, unknown>; error: unknown };
|
|
15
|
+
|
|
16
|
+
function canonicalizeRtdbValue(value: unknown): unknown {
|
|
17
|
+
if (Array.isArray(value)) return value.map(canonicalizeRtdbValue);
|
|
18
|
+
if (typeof value !== 'object' || value === null) return value;
|
|
19
|
+
return Object.fromEntries(
|
|
20
|
+
Object.keys(value as Record<string, unknown>)
|
|
21
|
+
.sort()
|
|
22
|
+
.map((key) => [
|
|
23
|
+
key,
|
|
24
|
+
canonicalizeRtdbValue((value as Record<string, unknown>)[key]),
|
|
25
|
+
]),
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Invoke the real Firebase Functions wrapper and await the user's result. */
|
|
30
|
+
export async function executeOnValueCreated(
|
|
31
|
+
trigger: DiscoveredOnValueCreated,
|
|
32
|
+
projection: CreatedValueProjection,
|
|
33
|
+
options: CreatedEventOptions,
|
|
34
|
+
): Promise<CreatedExecutionResult> {
|
|
35
|
+
const event: Record<string, unknown> = {
|
|
36
|
+
specversion: '1.0',
|
|
37
|
+
id: options.id,
|
|
38
|
+
// RTDB CloudEvents use the provider resource and a `_` project segment;
|
|
39
|
+
// the concrete database identity lives in `instances/<instance>`.
|
|
40
|
+
source:
|
|
41
|
+
'//firebasedatabase.googleapis.com/projects/_' +
|
|
42
|
+
`/locations/${options.location}/instances/${options.instance}`,
|
|
43
|
+
subject: `refs/${projection.ref}`,
|
|
44
|
+
type: CREATED_EVENT_TYPE,
|
|
45
|
+
time: options.time,
|
|
46
|
+
location: options.location,
|
|
47
|
+
instance: options.instance,
|
|
48
|
+
ref: projection.ref,
|
|
49
|
+
firebasedatabasehost: options.databaseHost,
|
|
50
|
+
authtype: 'unknown',
|
|
51
|
+
authid: null,
|
|
52
|
+
data: {
|
|
53
|
+
data: null,
|
|
54
|
+
delta: canonicalizeRtdbValue(projection.value),
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
await trigger.callable(event);
|
|
60
|
+
return { status: 'fulfilled', event };
|
|
61
|
+
} catch (error) {
|
|
62
|
+
return { status: 'rejected', event, error };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { discoverOnValueCreated, type DiscoveredOnValueCreated } from './discovery.js';
|
|
2
|
+
import type { RtdbTriggerDelivery } from './delivery.js';
|
|
3
|
+
import {
|
|
4
|
+
executeOnValueCreated,
|
|
5
|
+
type CreatedEventOptions,
|
|
6
|
+
type CreatedExecutionResult,
|
|
7
|
+
} from './event.js';
|
|
8
|
+
import {
|
|
9
|
+
projectValueCreates,
|
|
10
|
+
watchPath,
|
|
11
|
+
type CreatedValueProjection,
|
|
12
|
+
} from './projection.js';
|
|
13
|
+
|
|
14
|
+
export interface OnValueCreatedExecutionOptions {
|
|
15
|
+
exported: Record<string, unknown>;
|
|
16
|
+
delivery: RtdbTriggerDelivery;
|
|
17
|
+
eventOptions(
|
|
18
|
+
projection: CreatedValueProjection,
|
|
19
|
+
sequence: number,
|
|
20
|
+
trigger: DiscoveredOnValueCreated,
|
|
21
|
+
): CreatedEventOptions;
|
|
22
|
+
onExecution?(
|
|
23
|
+
result: CreatedExecutionResult,
|
|
24
|
+
trigger: DiscoveredOnValueCreated,
|
|
25
|
+
projection: CreatedValueProjection,
|
|
26
|
+
): void;
|
|
27
|
+
onDeliveryError?(error: unknown, trigger: DiscoveredOnValueCreated): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface OnValueCreatedExecutionHost {
|
|
31
|
+
readonly triggerCount: number;
|
|
32
|
+
readonly ready: Promise<void>;
|
|
33
|
+
idle(): Promise<void>;
|
|
34
|
+
close(): void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Subscribe discovered functions and serialize their snapshot executions. */
|
|
38
|
+
export function startOnValueCreatedExecution(
|
|
39
|
+
options: OnValueCreatedExecutionOptions,
|
|
40
|
+
): OnValueCreatedExecutionHost {
|
|
41
|
+
const triggers = discoverOnValueCreated(options.exported);
|
|
42
|
+
const unsubscribe: Array<() => void> = [];
|
|
43
|
+
let closed = false;
|
|
44
|
+
let sequence = 0;
|
|
45
|
+
let tail = Promise.resolve();
|
|
46
|
+
let baselinesRemaining = triggers.length;
|
|
47
|
+
let readinessFailed = false;
|
|
48
|
+
let markReady!: () => void;
|
|
49
|
+
let failReady!: (error: unknown) => void;
|
|
50
|
+
const ready = new Promise<void>((resolve, reject) => {
|
|
51
|
+
markReady = resolve;
|
|
52
|
+
failReady = reject;
|
|
53
|
+
});
|
|
54
|
+
if (baselinesRemaining === 0) markReady();
|
|
55
|
+
|
|
56
|
+
for (const trigger of triggers) {
|
|
57
|
+
const path = watchPath(trigger.reference);
|
|
58
|
+
let hasBaseline = false;
|
|
59
|
+
let previous: unknown;
|
|
60
|
+
unsubscribe.push(
|
|
61
|
+
options.delivery.subscribe(
|
|
62
|
+
path,
|
|
63
|
+
(next) => {
|
|
64
|
+
if (closed) return;
|
|
65
|
+
if (!hasBaseline) {
|
|
66
|
+
previous = next;
|
|
67
|
+
hasBaseline = true;
|
|
68
|
+
baselinesRemaining -= 1;
|
|
69
|
+
if (baselinesRemaining === 0 && !readinessFailed) markReady();
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const before = previous;
|
|
73
|
+
previous = next;
|
|
74
|
+
for (const projection of projectValueCreates(trigger.reference, {
|
|
75
|
+
path,
|
|
76
|
+
before,
|
|
77
|
+
after: next,
|
|
78
|
+
})) {
|
|
79
|
+
const deliverySequence = ++sequence;
|
|
80
|
+
tail = tail.then(async () => {
|
|
81
|
+
const result = await executeOnValueCreated(
|
|
82
|
+
trigger,
|
|
83
|
+
projection,
|
|
84
|
+
options.eventOptions(projection, deliverySequence, trigger),
|
|
85
|
+
);
|
|
86
|
+
options.onExecution?.(result, trigger, projection);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
(error) => {
|
|
91
|
+
if (!hasBaseline && !readinessFailed) {
|
|
92
|
+
readinessFailed = true;
|
|
93
|
+
failReady(error);
|
|
94
|
+
}
|
|
95
|
+
options.onDeliveryError?.(error, trigger);
|
|
96
|
+
},
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
triggerCount: triggers.length,
|
|
103
|
+
ready,
|
|
104
|
+
idle: () => tail,
|
|
105
|
+
close() {
|
|
106
|
+
if (closed) return;
|
|
107
|
+
closed = true;
|
|
108
|
+
for (const stop of unsubscribe.splice(0)) stop();
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|