From 5df2d1b3a2f5fc25f5838e3df83493d55fe20fab Mon Sep 17 00:00:00 2001 From: Tyler Dixon Date: Thu, 1 Oct 2026 14:29:13 -0700 Subject: [PATCH 1/2] spike: a Data Connect subscription hook, for review only Not for merge. The branch exists so the code can be read and run. Ships nothing: the root tsconfig includes only src and types, package.json publishes only dist and src, lint and format are scoped to src/test/ vite.config.ts, and the probe carries no test files so vitest does not collect it. The nested package.json is not a workspace, so root npm ci ignores it. Walking @firebase/data-connect 0.7.4, the SDK already supplies what a wrapper classically adds: fetchPolicy, a TTL cache, provenance, and a toJSON/toQueryRef handoff. Firestore supplies all four too, onSnapshotResume included. So the gap is the subscription lifecycle, which no data SDK can supply from outside React. subscribe() exists and nothing binds it: @tanstack-query-firebase/react 2.1.1 ships two one-shot hooks and subscribe appears 0 times in the package, against an executeQuery control of 4 in the same module. Pinned and reproducible: npm ci && npx tsc, expect exit 0. The README leads with the two questions worth a reviewer's time, the first being whether subscribe() can be exercised against the emulator. It also records a mutation control that did NOT fire: deleting unsubscribe() from the cleanup compiles exit 0, so the typecheck says nothing about lifecycle correctness, and the lifecycle is the whole point of the hook. --- spikes/data-connect-subscription/.gitignore | 1 + spikes/data-connect-subscription/README.md | 76 ++ .../package-lock.json | 1150 +++++++++++++++++ spikes/data-connect-subscription/package.json | 1 + .../src/probe-types.ts | 19 + .../src/useDataConnectSubscription.ts | 68 + .../data-connect-subscription/tsconfig.json | 14 + 7 files changed, 1329 insertions(+) create mode 100644 spikes/data-connect-subscription/.gitignore create mode 100644 spikes/data-connect-subscription/README.md create mode 100644 spikes/data-connect-subscription/package-lock.json create mode 100644 spikes/data-connect-subscription/package.json create mode 100644 spikes/data-connect-subscription/src/probe-types.ts create mode 100644 spikes/data-connect-subscription/src/useDataConnectSubscription.ts create mode 100644 spikes/data-connect-subscription/tsconfig.json diff --git a/spikes/data-connect-subscription/.gitignore b/spikes/data-connect-subscription/.gitignore new file mode 100644 index 00000000..c2658d7d --- /dev/null +++ b/spikes/data-connect-subscription/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/spikes/data-connect-subscription/README.md b/spikes/data-connect-subscription/README.md new file mode 100644 index 00000000..3a6a191e --- /dev/null +++ b/spikes/data-connect-subscription/README.md @@ -0,0 +1,76 @@ +# Spike: a Data Connect subscription hook + +**Not for merge.** This branch exists so the code can be read and run. It ships +nothing: the root `tsconfig.json` includes only `src` and `types`, `package.json` +publishes only `dist` and `src`, and `lint`/`format` are scoped to `src test +vite.config.ts`, so nothing here is typechecked, linted, bundled or published by +the repo. + +## The question I actually want answered + +**Can `subscribe()` be exercised against the Data Connect emulator?** + +Everything below is compile-time. If the emulator does not support `subscribe`, +there is no local development path and CI cannot exercise this, which changes the +answer to "should ReactFire support Data Connect" a lot more than the line count +does. `search()` is the precedent: no emulator path meant CI could not cover it, +so it got held to a later minor. + +Second question, smaller: **do you know of any React binding that wires up Data +Connect subscriptions, or AI Logic live sessions?** I have `npm search` on that +and it is keyword matching rather than enumeration, so my "nobody does" is really +"none found". + +## Why the hook exists + +Walking `@firebase/data-connect` 0.7.4 (`public.d.ts`, 524 lines), the SDK +already supplies the things a wrapper classically adds: cache-source choice +(`fetchPolicy`), a cache with a TTL (`CacheProvider`, `maxAgeSeconds`), +provenance (`OpResult.source`, `fetchTime`) and a serialized server-to-client +handoff (`toJSON()` / `toQueryRef()`). Firestore supplies all four too, including +`onSnapshotResume()`. + +So the gap is not data access. It is the subscription lifecycle, which no data +SDK can supply from outside React. `subscribe()` exists and nothing binds it: +`@tanstack-query-firebase/react` 2.1.1 ships `useDataConnectQuery` and +`useDataConnectMutation`, both one-shot, and `subscribe` appears **0** times in +that package (positive control: `executeQuery` appears 4 times in the same +module, so the search can return non-empty). + +## Running it + +``` +cd spikes/data-connect-subscription +npm ci +npx tsc -p tsconfig.json # expect exit 0 +``` + +Pinned: `firebase` 12.19.0, which resolves `@firebase/data-connect` 0.7.4 and +`@firebase/firestore` 4.17.2. TypeScript 5.6.3, `@types/react` 19.0.0. + +## What the typecheck does and does not prove + +Mutation controls, with `tsc`'s exit captured directly rather than after a pipe: + +| Mutation | Result | +| --- | --- | +| Baseline | exit 0 | +| `onNext: (res: number)` | exit 2, `TS2769` | +| `notAny: IsAny = true` | exit 2, `TS2322` | +| `src: DataSource = 'BOGUS'` | exit 2, `TS2322` | +| **Remove `unsubscribe()` from the cleanup** | **exit 0** | + +**The last row is the one worth knowing.** Deleting the teardown typechecks +clean, so "compiles under `strict`" says nothing about whether the lifecycle is +right, and the lifecycle is the entire point of the hook. The teardown, the +liveness guard and the re-subscribe key are backed by review only. That is the +other reason this is a branch rather than a number in a doc. + +## Known limits + +- Compile-time only. Nothing ran against a live backend. +- `subscribe()`'s semantics are uncharacterized: how it invalidates, whether it + polls or pushes, and what it does on reconnect are unknown. +- No tests. ~60 lines is one competent implementation, not a floor. +- The `initialData` seam is a convenience over an SDK feature, not a capability + nobody has. diff --git a/spikes/data-connect-subscription/package-lock.json b/spikes/data-connect-subscription/package-lock.json new file mode 100644 index 00000000..8af6742d --- /dev/null +++ b/spikes/data-connect-subscription/package-lock.json @@ -0,0 +1,1150 @@ +{ + "name": "dc-verify", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "dc-verify", + "dependencies": { + "@types/react": "^19.0.0", + "firebase": "^12.19.0", + "react": "^19.0.0", + "typescript": "^5.6.3" + } + }, + "node_modules/@firebase/ai": { + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/@firebase/ai/-/ai-2.16.0.tgz", + "integrity": "sha512-WRVxl58B61Orwf7st7949ZIW2L4huV80Gpaum1z8v2eH+m2xaxWtC8mOjQyVAOirr6O/yf89cBjlPvoyBpBfTg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/analytics": { + "version": "0.10.25", + "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.25.tgz", + "integrity": "sha512-oDagV3PHeNXBdxemksZCdj+GmUT19eCnTOcLdQePIIovFXlm3zWy3Gjeaokt9Nqx24kc4ljK8lU0XGcGmmKZ1A==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/analytics-compat": { + "version": "0.2.31", + "resolved": "https://registry.npmjs.org/@firebase/analytics-compat/-/analytics-compat-0.2.31.tgz", + "integrity": "sha512-msTlG9REujk5rjtOHNLngiuPmvaWGQte8ugL4/F6ZCMb+92Ue7SgjSEpNxINkM8ZtTrbGS83j5SZkt0r+M1Tjw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/analytics": "0.10.25", + "@firebase/analytics-types": "0.8.5", + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/analytics-types": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@firebase/analytics-types/-/analytics-types-0.8.5.tgz", + "integrity": "sha512-kdnooE7Bis2jEnsqcerRwn/UQpH5D3uvHpku7OdUM9TJN3omlu6iYtbyAQ6XkAJRgJtO0aNf9OOkW8J6D59oCA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.16.2.tgz", + "integrity": "sha512-fyCLPahl3r0Zb7Wzm+JLWcOOH3SaDgAyJzJ8EWIDiv51aNiv+2Pjbpa6myEuP7iXxjmJqrcbVm0GUF7h229T1Q==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-check": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.13.1.tgz", + "integrity": "sha512-l8y3dmnhodXks/APAwx4tWqRl3tk8b9874KF1FJyKg1DIU+kMD0l52iz7S9/MW+eK8k6cjJg0G0iJ5KQAsQpow==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/app-check-compat": { + "version": "0.4.7", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.4.7.tgz", + "integrity": "sha512-fBb/xSMyIKv1nDmccfzz3IAGBzx/cQOxmZai66rJzifb1hMMkztf824Xx7LhpTUe7HNUbXwY90IvJ66fi8q6yg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check": "0.13.1", + "@firebase/app-check-types": "0.5.5", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.5.tgz", + "integrity": "sha512-qId34pVZ2CXTmtu4ofW5leiI93DvnOvIcr/5GT7MZPw5WXAi6nZoU+g9cNRgl7K90UJSbK0cXxten4meYMPyZg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-check-types": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@firebase/app-check-types/-/app-check-types-0.5.5.tgz", + "integrity": "sha512-+DF4gzFrlwGFyky38o4T/YN/r51l70yCtJ2HTkwmRj8FCMwPjm4hLH4fQbj6BUvzkiFqliBkitFsRpf1/YZkWA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/app-compat": { + "version": "0.5.18", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.5.18.tgz", + "integrity": "sha512-77H57WuGEsgrv59HdtuhHG+eQdP8di6myz7O93yW6yTZlR/tLSmsCxCq5vHp6AWWDwSV0EVtKUvizXQiQZVBWA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app": "0.16.2", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/app-types": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.6.tgz", + "integrity": "sha512-yPLahy7Esfu2w/yme3msVK4xTkDXQqq6szfQn8yVOQpCKiT5GVFjqNpLbuz6NkX0WuTwUidkmPewW3r4xkvpeg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/logger": "0.5.2" + } + }, + "node_modules/@firebase/auth-compat": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.6.11.tgz", + "integrity": "sha512-L+xp3DTJQrBV08Vt0UhcL/ofUOSjXom7JOJfbT6oas4o7gCrQnTYFKs8wfiuKLmHoo8BxMr5LdgjAs7o6aAsqQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth": "1.13.6", + "@firebase/auth-types": "0.13.2", + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/auth-compat/node_modules/@firebase/auth": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.13.6.tgz", + "integrity": "sha512-MKRLvF72HJ/irNn2fJR0Gv5FOZNtqv1IBeJHRwK8l8pZ/wi9vuUw8ecyCMvQDDNxfS6uW0MWACL5KC4Y8Pp+fA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.6.tgz", + "integrity": "sha512-FgwZqDrBqgK0BHI70QTv1v/5wmuDF9f8fsJFvKSxoRlK2PPfSQtZAk2jhXu4pe9BZzFi/e4UYusU/bEQHdf6Qg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/auth-types": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/@firebase/auth-types/-/auth-types-0.13.2.tgz", + "integrity": "sha512-OU+miuoSxIWYN7GT291d2ylVJBL3k/OePo7/JDSoQtkFQoEiGBc4AHuMjj/seqFIsHrqnjrAfRCk4pfufoJolQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/component": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.7.5.tgz", + "integrity": "sha512-vuFDcL91Q+2ZuBJkyOh86T4q0B4ffNTDjc/A38tybO56odQABxRTFLTIowCWqAKeIcgo37GowWMVgFF73gD8Qw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/data-connect": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/@firebase/data-connect/-/data-connect-0.7.4.tgz", + "integrity": "sha512-su1aGWlzhxb+xtggCUSsufJn1FDa06SBDK71y+fpQ+g2zMhMExik6FUv/odkKzOF/xfWVjabCbWBcjJY3MceDA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/database": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.1.5.tgz", + "integrity": "sha512-/JGpvszLoNXNgzilRXocigGfFF4hbcPA9wN1i1kjJx6oKkXgkHteZYl3lQs1lJX3ETDf6bv7zI15lPMVUp4sAQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.1.7.tgz", + "integrity": "sha512-lBq9sJm8MnJINKJnkAKSOj2MbC66xGoSVCcGkPtstl+lkoKEBtD46qHLbuDgW/WbLPoKVm17RIN8BxHj+Z2F2w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/database": "1.1.5", + "@firebase/database-types": "1.0.22", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + }, + "peerDependenciesMeta": { + "@firebase/app": { + "optional": true + }, + "@firebase/app-compat": { + "optional": true + } + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.22", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.22.tgz", + "integrity": "sha512-YAZNXsjY9EQQ+pKw/3ax8n5FgolHC7Qew7EY5RceYdl0R2ZP+kCv1O0DkKlcceue8uQCOreHCNN7PWVFpY1Nug==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-types": "0.9.6", + "@firebase/util": "1.15.3" + } + }, + "node_modules/@firebase/firestore": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.17.2.tgz", + "integrity": "sha512-SiyQEK1dYPOjItFVA+kZGdvOoRAtMebDBvUaSxJPNZMkKcRwYYK2kzWo5UXTZZj523uMxJnkr+KKVySEMme3kQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "@firebase/webchannel-wrapper": "1.0.7", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "re2js": "^2.8.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/firestore-compat": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.4.14.tgz", + "integrity": "sha512-HJ2HBNgNPrRsWIJ4E+Kc0u0h1U7HlF7If8AtYjie9HZXlMUmwYSJaXBj9VFm6gkSj5dlKZNcOQ0Ou25CVVG6rA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/firestore": "4.17.2", + "@firebase/firestore-types": "3.0.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/firestore-types": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.5.tgz", + "integrity": "sha512-dbdMAQkMd5dwWc48eupz/Y6/E9ruat3+gY5lhVKscvvT/HnDBEEMzJW38zdKhgdnglZHGk2vUsJMOHAphMHGMA==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/functions": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.14.0.tgz", + "integrity": "sha512-DhuYFr0eMhp+s/PNEk6SiMsYkc00+XVOUeKbrl8MOzQNZI3SKQpqoDR1d+keNDAutwmHRWTUAdXBoVPD+xxVUw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.5", + "@firebase/auth-interop-types": "0.2.6", + "@firebase/component": "0.7.5", + "@firebase/messaging-interop-types": "0.2.6", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/functions-compat": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.5.0.tgz", + "integrity": "sha512-T3BDIToESZUHt7438wyKYMkRjKg3m1xAIux5fVpNvTRQlDOFLukWniQWBDhJ2znpU9kxMTR6+e31TVo8P15PZw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/functions": "0.14.0", + "@firebase/functions-types": "0.6.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/functions-types": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/@firebase/functions-types/-/functions-types-0.6.5.tgz", + "integrity": "sha512-Zc0pURjthHXzSj54ZivCkzKDSV1r/wIpnmHdhq82q2yFFPVoncG/ZJjnVMiANvQfeww/QnElhzODpfwUucVwdA==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/installations": { + "version": "0.6.24", + "resolved": "https://registry.npmjs.org/@firebase/installations/-/installations-0.6.24.tgz", + "integrity": "sha512-Ui52ey8wHoWqkBbXRKJEKYWylI0JZogZmoLS+o8Anh1bxWtK27ZYjLla1UJAAdC5DCKLJ2qAp0VpJOjg8VOX1g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/installations-compat": { + "version": "0.2.24", + "resolved": "https://registry.npmjs.org/@firebase/installations-compat/-/installations-compat-0.2.24.tgz", + "integrity": "sha512-8M5nlcWwYt881x83COP2odq5vgf+NgwJh+RMd4LRSv8JI1pxwDfgrDJOERrjTgfC9J5Z0vnQX4b1pdN914e0Zw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/installations-types": "0.5.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/installations-types": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@firebase/installations-types/-/installations-types-0.5.5.tgz", + "integrity": "sha512-e9UYcju3puDl1vdrcKIi5dExzHLameOT/Tc61Q48PYwxtsM1NzZh/ikGbdBQTsbRgg0EMZqdPr0/m5ODUBobrg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x" + } + }, + "node_modules/@firebase/logger": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.5.2.tgz", + "integrity": "sha512-J2VO4NFTc0xQFrxV1B/lm5balicm9cwuX2acR9Yn41fN8KgUeQFo+VJV222IqW2FPSXKDu9uo5WdQFWf9TPbYg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/messaging": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.13.3.tgz", + "integrity": "sha512-nRXPAp+z0kA3KziJtRY9kktHECvRwaaONzHvislFgllhCwJMscW7icWNQA5IjyP8Uc2+GS/4px8Uz6rx6ftTag==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/messaging-interop-types": "0.2.6", + "@firebase/util": "1.15.3", + "idb": "7.1.1", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/messaging-compat": { + "version": "0.2.30", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.30.tgz", + "integrity": "sha512-m1DPT2ET5x0qYqXLoHwKMAyf1mgZZOhuoE6H6/H4OPsp8zSxMIDEFPaW7MaffPJsCA5J7kHzo+BHVNXK4ZoV+w==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/messaging": "0.13.3", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/messaging-interop-types": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.6.tgz", + "integrity": "sha512-MVzvkKe2V4H2dHu5oOxRfeKQcfTwWmCgnzsC4V1q3ixun5iiL8riCZ2qI35rDhCL4glPGiu0jHxDbpVNuTKfow==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/performance": { + "version": "0.7.14", + "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.7.14.tgz", + "integrity": "sha512-9PH1XEZVHErxGdbXluvGz4Uyjw4W955H8v7Mv9rHIybRrg5F2Ac2tvf5+mSf5EtnxWNmxrD2WLnvMFaZP4sMrQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/performance-compat": { + "version": "0.2.27", + "resolved": "https://registry.npmjs.org/@firebase/performance-compat/-/performance-compat-0.2.27.tgz", + "integrity": "sha512-O/ozTf/EbChN94Pk7bd1eUC0PAC36726AwsaiJyC0bZzSBWfLGSWASGPH5pebUWXQPJtGxIJYRkkbX5l0Qa+Hw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/performance": "0.7.14", + "@firebase/performance-types": "0.2.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/performance-types": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@firebase/performance-types/-/performance-types-0.2.5.tgz", + "integrity": "sha512-PRzOgB+/M+6AKlEkY8a9xy5Ff5SfZPIh4iShXhn2WAcL/euSbK7bdLqWysHduunNJhGFsXa22Ag3ixqL6rQtYg==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/remote-config": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@firebase/remote-config/-/remote-config-0.9.2.tgz", + "integrity": "sha512-Rii93DkXjM+RE/ytHdYa7EIiQLJbdBr+W8EEiqd/vbLCOC+1FdkDuRiumJBp6t3yewHGbdqbpjB3fk3z0cZ+8g==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/installations": "0.6.24", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/remote-config-compat": { + "version": "0.2.29", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-compat/-/remote-config-compat-0.2.29.tgz", + "integrity": "sha512-mY7JtTISK6F4g4T0x3kVepr5cp0NXL7f5yjIUXXGaTrg4qUlFBWRbENaHaqZ78dwmNcLm24h/yPsOVRlDXEXhA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/remote-config": "0.9.2", + "@firebase/remote-config-types": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/remote-config-types": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@firebase/remote-config-types/-/remote-config-types-0.5.2.tgz", + "integrity": "sha512-i8k1omVfoAnaT1ZPv2FFjxMZrATYsO/GnFgHEj5+7fAJLzi8wLnGGv1WK06oEAD6ltrWXSO1HzeNyajn/NjMWw==", + "license": "Apache-2.0" + }, + "node_modules/@firebase/storage": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.14.5.tgz", + "integrity": "sha512-r2tozN/BlEewLi70tJNUQPzWwbex9GM7NgXZsamHKQrjKEfcR0i4jGgHBKAKA4hbwiPE9eNMb3hcxWnDpeJZwg==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x" + } + }, + "node_modules/@firebase/storage-compat": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.4.5.tgz", + "integrity": "sha512-vO0tFPxXbKDKdlTu8tYT08S9t9ezUTJYEdLCULpWxWq2aq6zojwN1QmAB+1a50AI/g47GlWUJn1XPncm/PDZsw==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/storage": "0.14.5", + "@firebase/storage-types": "0.8.5", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@firebase/app-compat": "0.x" + } + }, + "node_modules/@firebase/storage-types": { + "version": "0.8.5", + "resolved": "https://registry.npmjs.org/@firebase/storage-types/-/storage-types-0.8.5.tgz", + "integrity": "sha512-GEDs5P+rNUfNcS+wxIdOLAHficife2YXtvTJnyi3ssrX11AAtBg+nDUdbYh8vuBzWehVQZPmztGUUnud4L+4yg==", + "license": "Apache-2.0", + "peerDependencies": { + "@firebase/app-types": "0.x", + "@firebase/util": "1.x" + } + }, + "node_modules/@firebase/util": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.15.3.tgz", + "integrity": "sha512-c/z/gaIlaaLZEuGbE6sLUuJ61tskg1JghvhcNQzW948ASBinbVBBRnZTC4b4yt4LaEtJQkYlyzqLHcutFwEIvA==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@firebase/webchannel-wrapper": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.7.tgz", + "integrity": "sha512-phBFwieDLvkZGYN9CE9ZFNEIoBVksprzsnCzQejCmCHtgwCXReeuRpoEGN9C4EbhONztv8NRV1tau6Rb9pONwQ==", + "license": "Apache-2.0" + }, + "node_modules/@grpc/grpc-js": { + "version": "1.9.16", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.16.tgz", + "integrity": "sha512-wE4Ut/olIzfKqp631XrG+wbF0v1vWFN4YL9FyXC2LJiG33DsV7PLzURjrCvY/6je2ntdRkeLpPDluzSRGaVltQ==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.7.8", + "@types/node": ">=12.12.47" + }, + "engines": { + "node": "^8.13.0 || >=10.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.15", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", + "integrity": "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "license": "BSD-3-Clause" + }, + "node_modules/@types/node": { + "version": "26.6.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.6.3.tgz", + "integrity": "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg==", + "license": "MIT", + "dependencies": { + "undici-types": "~8.9.0" + } + }, + "node_modules/@types/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.0.tgz", + "integrity": "sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==", + "license": "MIT", + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "license": "Apache-2.0", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/firebase": { + "version": "12.19.0", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-12.19.0.tgz", + "integrity": "sha512-kwXCLcI0ly2lkwygkbuRx6SsX3DSO96355YrWh9SWZ5ncsxK/y5cirn3sY6nZVSiBVE++2nL6Hchzu1EB8uohQ==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/ai": "2.16.0", + "@firebase/analytics": "0.10.25", + "@firebase/analytics-compat": "0.2.31", + "@firebase/app": "0.16.2", + "@firebase/app-check": "0.13.1", + "@firebase/app-check-compat": "0.4.7", + "@firebase/app-compat": "0.5.18", + "@firebase/app-types": "0.9.6", + "@firebase/auth": "1.13.6", + "@firebase/auth-compat": "0.6.11", + "@firebase/data-connect": "0.7.4", + "@firebase/database": "1.1.5", + "@firebase/database-compat": "2.1.7", + "@firebase/firestore": "4.17.2", + "@firebase/firestore-compat": "0.4.14", + "@firebase/functions": "0.14.0", + "@firebase/functions-compat": "0.5.0", + "@firebase/installations": "0.6.24", + "@firebase/installations-compat": "0.2.24", + "@firebase/messaging": "0.13.3", + "@firebase/messaging-compat": "0.2.30", + "@firebase/performance": "0.7.14", + "@firebase/performance-compat": "0.2.27", + "@firebase/remote-config": "0.9.2", + "@firebase/remote-config-compat": "0.2.29", + "@firebase/storage": "0.14.5", + "@firebase/storage-compat": "0.4.5", + "@firebase/util": "1.15.3" + } + }, + "node_modules/firebase/node_modules/@firebase/auth": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.13.6.tgz", + "integrity": "sha512-MKRLvF72HJ/irNn2fJR0Gv5FOZNtqv1IBeJHRwK8l8pZ/wi9vuUw8ecyCMvQDDNxfS6uW0MWACL5KC4Y8Pp+fA==", + "license": "Apache-2.0", + "dependencies": { + "@firebase/component": "0.7.5", + "@firebase/logger": "0.5.2", + "@firebase/util": "1.15.3", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^2.2.0 || ^3.0.0" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", + "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", + "license": "MIT" + }, + "node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==", + "license": "ISC" + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "license": "Apache-2.0" + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/re2js": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/re2js/-/re2js-2.8.6.tgz", + "integrity": "sha512-xLgQil4kIUCrAzVk9fRSkxkFNwmygLFjVxXrLc65aE1F0+Zsb8rxumFBy4XKyvgMCTL6kilDq3EZ0piE2dP/Dg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/react": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.0.0.tgz", + "integrity": "sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz", + "integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==", + "license": "MIT" + }, + "node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, + "node_modules/websocket-driver": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", + "license": "Apache-2.0", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/spikes/data-connect-subscription/package.json b/spikes/data-connect-subscription/package.json new file mode 100644 index 00000000..5ab6e66b --- /dev/null +++ b/spikes/data-connect-subscription/package.json @@ -0,0 +1 @@ +{"name":"dc-verify","private":true,"type":"module","dependencies":{"@types/react":"^19.0.0","firebase":"^12.19.0","react":"^19.0.0","typescript":"^5.6.3"}} \ No newline at end of file diff --git a/spikes/data-connect-subscription/src/probe-types.ts b/spikes/data-connect-subscription/src/probe-types.ts new file mode 100644 index 00000000..5b7f5812 --- /dev/null +++ b/spikes/data-connect-subscription/src/probe-types.ts @@ -0,0 +1,19 @@ +import type { SubscriptionState } from './useDataConnectSubscription'; +import type { DataSource } from 'firebase/data-connect'; + +// Assert Data flows through rather than collapsing to never/any. +type Extract_ = T extends SubscriptionState ? D : never; +type IsAny = 0 extends 1 & T ? true : false; +type IsNever = [T] extends [never] ? true : false; + +type D = Extract_>; + +const notAny: IsAny = false; +const notNever: IsNever = false; +const realShape: D = { id: 'x' }; + +// source must be the real two-member union, not never. +const src: DataSource = 'CACHE'; +const srcNotNever: IsNever = false; + +export { notAny, notNever, realShape, src, srcNotNever }; diff --git a/spikes/data-connect-subscription/src/useDataConnectSubscription.ts b/spikes/data-connect-subscription/src/useDataConnectSubscription.ts new file mode 100644 index 00000000..cd2fd65e --- /dev/null +++ b/spikes/data-connect-subscription/src/useDataConnectSubscription.ts @@ -0,0 +1,68 @@ +import { useEffect, useRef, useState } from 'react'; +import { + subscribe, + toQueryRef, + type QueryRef, + type SerializedRef, + type DataConnectError, + type DataSource, +} from 'firebase/data-connect'; + +export interface SubscriptionState { + data: Data | undefined; + source: DataSource | undefined; + fetchTime: string | undefined; + error: DataConnectError | undefined; + status: 'loading' | 'success' | 'error'; +} + +export interface UseDataConnectSubscriptionOptions { + initialData?: SerializedRef; +} + +export function useDataConnectSubscription( + queryRef: QueryRef, + options?: UseDataConnectSubscriptionOptions, +): SubscriptionState { + const seed = options?.initialData; + const [state, setState] = useState>(() => + seed + ? { data: seed.data, source: seed.source, fetchTime: seed.fetchTime, error: undefined, status: 'success' } + : { data: undefined, source: undefined, fetchTime: undefined, error: undefined, status: 'loading' }, + ); + + const key = `${queryRef.name}:${JSON.stringify(queryRef.variables)}`; + const live = useRef(true); + + useEffect(() => { + live.current = true; + const unsubscribe = subscribe(queryRef, { + onNext: (res) => { + if (!live.current) return; + setState({ + data: res.data, + source: res.source, + fetchTime: res.fetchTime, + error: undefined, + status: 'success', + }); + }, + onErr: (e) => { + if (!live.current) return; + setState((prev) => ({ ...prev, error: e, status: 'error' })); + }, + }); + return () => { + live.current = false; + unsubscribe(); + }; + }, [key]); + + return state; +} + +export function hydrateQueryRef( + serialized: SerializedRef, +): QueryRef { + return toQueryRef(serialized); +} diff --git a/spikes/data-connect-subscription/tsconfig.json b/spikes/data-connect-subscription/tsconfig.json new file mode 100644 index 00000000..fa3420df --- /dev/null +++ b/spikes/data-connect-subscription/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM"], + "jsx": "react-jsx", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noUnusedLocals": true, + "noEmit": true, + "skipLibCheck": true + }, + "include": ["src"] +} From c5f61f768b6eb5c2872b52ae3030eae093609bf2 Mon Sep 17 00:00:00 2001 From: Tyler Dixon Date: Fri, 2 Oct 2026 12:56:21 -0700 Subject: [PATCH 2/2] docs(spike): correct the mutation-control row in the README The table claimed that deleting unsubscribe() from the cleanup compiles at exit 0. Deleting the call alone gives exit 2, TS6133, because tsconfig.json sets noUnusedLocals. Exit 0 needs the const unsubscribe = binding deleted too. Armando caught it on the PR. All three states re-run at 5df2d1b: baseline exit 0, call-only exit 2, both-deleted exit 0. The conclusion holds in the second form, and the README now says it is weaker than first stated: noUnusedLocals does catch the most likely accidental break, so the compiler is not quite as blind to the lifecycle as the original row implied. The same wrong sentence remains in the commit message for 5df2d1b, which cannot be fixed without a force-push to this branch. --- spikes/data-connect-subscription/README.md | 24 +++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/spikes/data-connect-subscription/README.md b/spikes/data-connect-subscription/README.md index 3a6a191e..41bd78b3 100644 --- a/spikes/data-connect-subscription/README.md +++ b/spikes/data-connect-subscription/README.md @@ -58,13 +58,23 @@ Mutation controls, with `tsc`'s exit captured directly rather than after a pipe: | `onNext: (res: number)` | exit 2, `TS2769` | | `notAny: IsAny = true` | exit 2, `TS2322` | | `src: DataSource = 'BOGUS'` | exit 2, `TS2322` | -| **Remove `unsubscribe()` from the cleanup** | **exit 0** | - -**The last row is the one worth knowing.** Deleting the teardown typechecks -clean, so "compiles under `strict`" says nothing about whether the lifecycle is -right, and the lifecycle is the entire point of the hook. The teardown, the -liveness guard and the re-subscribe key are backed by review only. That is the -other reason this is a branch rather than a number in a doc. +| Remove the `unsubscribe()` call alone | exit 2, `TS6133` | +| **Remove the call and the `const unsubscribe =` binding** | **exit 0** | + +**Corrected 2026-10-02, Armando's catch on the PR.** The last two rows were +previously one row reading "Remove `unsubscribe()` from the cleanup: exit 0". +That is wrong: `noUnusedLocals` catches the orphaned binding, so deleting the +call alone fails. Exit 0 needs the binding deleted too. Re-run at `5df2d1b` +confirms all three states. + +**The point survives in the second form, and it is the one worth knowing.** A +teardown removed properly typechecks clean, so "compiles under `strict`" says +nothing about whether the lifecycle is right, and the lifecycle is the entire +point of the hook. ⚠️ **It also makes the original claim weaker than stated: +`noUnusedLocals` does catch the single most likely way someone breaks this by +accident.** The teardown, the liveness guard and the re-subscribe key are still +backed by review rather than by the compiler, which is the other reason this is a +branch rather than a number in a doc. ## Known limits