refactor: recipe demo app rebuilt on AngularFire- #3753 - #5
Open
armando-navarro wants to merge 8 commits into
Open
refactor: recipe demo app rebuilt on AngularFire- #3753#5armando-navarro wants to merge 8 commits into
armando-navarro wants to merge 8 commits into
Conversation
Commit b83343d is the latest canary as of this writing.
…ders The four hand-made injection tokens and their platform guards are gone, along with the file that declared them. AngularFire's providers take over: provideFirebaseApp once per platform, plus provideFirestore, provideAuth, provideAppCheck and provideAI.
The browser's cookie sync moves from a raw onIdTokenChanged subscription with a manual teardown to AngularFire's idToken observable with takeUntilDestroyed. Only the beforeAuthStateChanged half still needs an unsubscribe of its own. The server factory no longer builds a Firebase server app for anonymous requests. A server app carries two things into a render, a user identity and a pre-minted App Check token, and an anonymous request has neither here, so a plain app does the same job with nothing to release. App Check is enforced only on AI Logic, which this app calls from the browser alone. The signed-in path hands the request context to releaseOnDeref, so the SDK drops the app once that object is collected, and the explicit deleteApp teardown goes with it.
AuthStore drops its hand-rolled onAuthStateChanged subscription, its signal and its teardown for one toSignal over authState. The guard stops going through AuthStore and reads the same observable directly, because a signal cannot distinguish "not resolved yet" from "resolved, signed out". Both read null, and a guard that confuses them sends a signed-in visitor to the sign-in page. The server config loses its provideAppInitializer that awaited authStateReady. AngularFire holds a pending task open from the moment authState is subscribed until its first emission, and AuthStore subscribes as soon as a component injects it, so the render already waits without the initializer. The two are not the same guarantee. The initializer blocked bootstrap, so nothing ran until auth resolved. A pending task only defers the point at which the render counts as stable, so work can still run before auth resolves. Data reads are unaffected, because Firestore waits for its own credentials.
The browser reads recipes through a live listener via collectionData. The server performs a single read via getDocs. rxResource is used to more easily work with AngularFire's RxJs observables. AngularFire-wrapped functions that need an injection context are wrapped in runInInjectionContext. The document-reading converter goes. Dropping it is what makes the read return untyped documents, and the two casts that follow are the price. They are deliberate and they are the measured cost of the current API, which is why they are written rather than worked around. TransferState stays hand-wired: the state key, the set on the server, the signal seeded from it, the computed that prefers the transferred list, and the clearing once the listener answers. AngularFire supplies none of those.
The likes read becomes an rxResource over collectionData, keeping an explicit readiness flag. The flag resets in finalize alone, which works because rxResource tears the old stream down before subscribing the new one. The writes now import from AngularFire, which has a cost. An AngularFire-wrapped function needs an injection context.
Anything the two branches share is inherited from branch 1 unchanged. A README difference that AngularFire did not cause is noise in the diff that is the whole deliverable. The production gaps are deliberate and each says what it costs. The unvalidated read is the current state of the library rather than a workaround, and the two casts it needs are named.
The inherited config named raw-sdk, which is the other branch's live site. firebase init apphosting made that worse rather than better, turning the apphosting field into an array holding both backends, so a deploy could have reached the wrong one. allowedHosts carried the same four raw-sdk hostnames. Without replacing them the deployed site answers HTTP 400 to every request. apphosting.yaml is deliberately not in this commit. The init rewrote it with a newer template, twenty lines of added comments and no change to any setting, and reverting it keeps the branch-to-branch diff free of noise the CLI introduced rather than AngularFire.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is the same application as the
recipe-demobranch, with its Firebase layer rebuilt on AngularFire.It is 8 commits, 13 files, and 249 lines added against 262 removed. Each commit converts one area, so the diff can be read a commit at a time.
What AngularFire replaces
In plain terms
In detail
firebase-tokens.tsholding fourInjectionTokens, wired through five hand-written provider objects across three config files, becomes sixprovide*calls. The file is deleted.onAuthStateChangedsubscription, the signal it pushed into, the teardown that unsubscribed it and awhenAuthResolved()wrapper become onetoSignal(authState(auth), { initialValue: null }).resourcewhose loader openedonSnapshotby hand, pushed snapshots and errors into a signal and registered the unsubscribe function as anabortlistener on the loader'sabortSignal, becomes anrxResourceovercollectionData.deleteAppin aDestroyRef.onDestroyblock becomes areleaseOnDerefproperty, following the pattern indocs/auth.md.provideAppChecksets it already.Across the four files that hold the Firebase layer, the base branch is 397 lines and this branch is 364.
What it does not change
These are invisible in a diff, and each is a place a reader could otherwise draw a stronger conclusion than the change supports.
TransferStatehandoff carrying the recipe list from server to browser. Angular's automatic state transfer coversHttpClient, which Firestore never touches, and AngularFire adds nothing here, so the same hand-written code is needed on both branches.server.ts, including the__sessioncookie plumbing behind the signed-in server render.beforeAuthStateChanged, which has no observable form in the library and stays imported fromfirebase/authon both branches.hasAuthIdTokentype predicate that narrowsREQUEST_CONTEXT.What it gains
Less code is the visible part. These are the things the app stops being responsible for.
Teardown becomes the framework's job. Two subscriptions the base branch has to store and unsubscribe itself are now torn down by Angular. There is no unsubscribe function to keep hold of, so there is none to forget.
Initialization order comes free.
provideFirestorelists Auth and App Check among its own dependencies, with the library's comment reading "Firestore+Auth work better if Auth is loaded first", so Firestore resolves after Auth without the app arranging it. The base branch owns that ordering by hand.The error path arrives with the read. The base branch passes an explicit error callback to
onSnapshotand pushes the error into a signal itself.rxResourcesurfaces it without that.The App Check debug token is handled more broadly than the hand-written version. The base branch sets the flag for an exact
localhostmatch.provideAppChecksets it whenever the app is in development mode or the hostname islocalhost,0.0.0.0or127.0.0.1. Worth an honest caveat: this app'sangular.jsonallows onlylocalhost, so the wider coverage cannot actually be observed here.The server's wait for auth stops being explicit. The base branch waits with an app initializer calling
authStateReady(). HereauthStateis zone-wrapped, and that wrapper registers the pending task that holds the server render open, so the initializer is deleted. One precision worth stating, because it is easy to over-credit: the recipe list's own server wait comes from Angular'srxResource, not from AngularFire. Auth is the place where AngularFire demonstrably carries the wait in this app.What it costs
collectionData(query, { idField: 'id' })returns untyped documents, so the store crosses them to the app'sRecipetype with two casts and nothing checks the shape at runtime. The base branch used aFirestoreDataConverter.EnvironmentInjectorand wrap their calls inrunInInjectionContext. Nine call sites, six in the recipe store and three in the auth store. Template event handlers are not injection contexts, which is why the write paths need this and the base branch does not.Running it
Deployed instance: https://angularfire--recipe-demo-97859.us-central1.hosted.app
recipe-demo/README.mdon this branch covers the project setup, the run commands, the design decisions that are hard to deduce from the code, and the gaps a production app would close that this one deliberately leaves open.What happens to this pull request
This should never be merged. It exists as a reference demo.