@xmachines/play-tanstack-react-router 1.0.0-beta.1
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/.oxfmtrc.json +3 -0
- package/.oxlintrc.json +3 -0
- package/README.md +177 -0
- package/dist/extract-params.d.ts +45 -0
- package/dist/extract-params.d.ts.map +1 -0
- package/dist/extract-params.js +70 -0
- package/dist/extract-params.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +17 -0
- package/dist/index.js.map +1 -0
- package/dist/play-router-provider.d.ts +33 -0
- package/dist/play-router-provider.d.ts.map +1 -0
- package/dist/play-router-provider.js +31 -0
- package/dist/play-router-provider.js.map +1 -0
- package/dist/route-map.d.ts +101 -0
- package/dist/route-map.d.ts.map +1 -0
- package/dist/route-map.js +139 -0
- package/dist/route-map.js.map +1 -0
- package/dist/tanstack-router-bridge.d.ts +115 -0
- package/dist/tanstack-router-bridge.d.ts.map +1 -0
- package/dist/tanstack-router-bridge.js +112 -0
- package/dist/tanstack-router-bridge.js.map +1 -0
- package/dist/types.d.ts +26 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +7 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +9 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +10 -0
- package/dist/utils.js.map +1 -0
- package/examples/demo/README.md +100 -0
- package/examples/demo/docs/ARCHITECTURE.md +643 -0
- package/examples/demo/docs/INVARIANTS.md +461 -0
- package/examples/demo/docs/SWAP-REACT.md +635 -0
- package/examples/demo/index.html +16 -0
- package/examples/demo/package.json +39 -0
- package/examples/demo/src/App.tsx +148 -0
- package/examples/demo/src/components/About.tsx +49 -0
- package/examples/demo/src/components/Contact.tsx +43 -0
- package/examples/demo/src/components/Dashboard.tsx +46 -0
- package/examples/demo/src/components/DebugPanel.tsx +68 -0
- package/examples/demo/src/components/HeaderNav.tsx +103 -0
- package/examples/demo/src/components/Home.tsx +41 -0
- package/examples/demo/src/components/Login.tsx +82 -0
- package/examples/demo/src/components/Navigation.tsx +262 -0
- package/examples/demo/src/components/Profile.tsx +46 -0
- package/examples/demo/src/components/Register.tsx +109 -0
- package/examples/demo/src/components/Settings.tsx +92 -0
- package/examples/demo/src/components/index.ts +16 -0
- package/examples/demo/src/main.tsx +20 -0
- package/examples/demo/test/actor-authority.test.ts +50 -0
- package/examples/demo/test/browser/__screenshots__/back-button-duplicate.browser.test.tsx/Browser-back-button-navigates-through-unique-history--no-duplicates--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-button-duplicate.browser.test.tsx/GAP-12--navigation-via-goto---events-creates-single-history-entries-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-button-duplicate.browser.test.tsx/GAP-12--navigation-via-goto---events-creates-single-history-entries-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--After-authentication-flow-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Multiple-rapid-navigations-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Multiple-rapid-navigations-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--URL-stays-in-sync-with-actor-state-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--URL-stays-in-sync-with-actor-state-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Works-correctly-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/back-forward-sync.browser.test.tsx/Back-Forward--Works-correctly-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--about-loads-about-page-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--contact-loads-contact-page-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to--home-loads-home-page-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to-protected-route-while-authenticated-loads-dashboard-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/direct-navigation.browser.test.ts/Direct-navigation-to-protected-route-while-unauthenticated-redirects-to-login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/Debug--Print-history-after-each-navigation-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/Debug--Print-history-after-each-navigation-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/EXACT-USER-SCENARIO--home---about---home---contact---home--then-back-3x-should-land-on-about-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/exact-user-scenario.browser.test.tsx/EXACT-USER-SCENARIO--home---about---home---contact---home--then-back-3x-should-land-on-about-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Actor-Authority---infrastructure-cannot-override-guards-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Actor-Authority---infrastructure-cannot-override-guards-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Guards-reject-invalid-navigation-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/guard-rejection.browser.test.tsx/E2E--Guards-reject-invalid-navigation-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-investigation.browser.test.tsx/baseHistory-back---navigation--avoiding-window-history--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-investigation.browser.test.tsx/baseHistory-back---navigation--avoiding-window-history--2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---authenticated-user-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---authenticated-user-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---unauthenticated-user-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-forward-with-guard-transitions---unauthenticated-user-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---authenticated-user-navigates-back-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---authenticated-user-navigates-back-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---unauthenticated-user-stays-on-public-routes-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Back-with-guard---unauthenticated-user-stays-on-public-routes-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back---unique-history-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back---unique-history-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Forward-button-after-back-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Navigate-forward-then-back---unique-history-entries-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Navigate-forward-then-back---unique-history-entries-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Rapid-back-forward-navigation-doesn-t-cause-duplicate-entries-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Rapid-back-forward-navigation-doesn-t-cause-duplicate-entries-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---about-to-home-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---about-to-home-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---contact-to-about-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--Single-back-navigation---contact-to-about-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-forward-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-forward-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-navigation-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/history-navigation.browser.test.ts/GAP-12--View-syncs-with-URL-after-back-navigation-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/login-flow.browser.test.tsx/E2E--User-can-log-in-and-see-dashboard-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/login-flow.browser.test.tsx/E2E--User-can-log-in-and-see-dashboard-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/navigation.browser.test.tsx/E2E--Navigation-reflects-actor-state-transitions-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/navigation.browser.test.tsx/E2E--Navigation-reflects-actor-state-transitions-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-forward-through-multiple-protected-routes-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-forward-through-multiple-protected-routes-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-navigates-from-dashboard-to-settings--protected-route--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/Browser-back-navigates-from-dashboard-to-settings--protected-route--2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/RED--Browser-back-forward-through-multiple-protected-routes-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/RED--Browser-back-from-dashboard-to-settings--protected-route--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/protected-route-navigation.browser.test.tsx/RED--Browser-back-navigates-from-dashboard-to-settings--protected-route--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--account--section-when-navigating-to--settings-account-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--account--section-when-navigating-to--settings-account-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--general--section-when-navigating-to--settings--no-parameter--1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--general--section-when-navigating-to--settings--no-parameter--2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--profile--section-when-navigating-to--settings-profile-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-display--profile--section-when-navigating-to--settings-profile-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-update-section-display-when-clicking-section-navigation-buttons-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-parameter.browser.test.tsx/Settings-Parameter-Display-should-update-section-display-when-clicking-section-navigation-buttons-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-query-freeze.browser.test.ts/Settings-with-query-parameters-works-correctly-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/settings-query-freeze.browser.test.ts/Settings-with-section-parameter-works-correctly-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04--State-Driven-Reset---Browser-back-sends-event-to-actor-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04--State-Driven-Reset---Browser-back-sends-event-to-actor-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04b--Browser-navigation-with-SignalSyncedHistory-integration-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/state-driven.browser.test.ts/DEMO-04b--Browser-navigation-with-SignalSyncedHistory-integration-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/tanstack-integration.browser.test.tsx/TanStack-Router-Integration-renders-with-RouterProvider-context-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/tanstack-integration.browser.test.tsx/TanStack-Router-Integration-renders-with-RouterProvider-context-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/test-multiple-back.browser.test.tsx/Multiple-back--Navigate-forward-3x-then-back-3x-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-1--Opening-with--someinvalidstate-stays-at-current-state-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-2--Opening-with--about-renders-About-component--not-Login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-2b--Opening-with--home-renders-Home-component--not-Login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-2c--Opening-with--contact-renders-Contact-component--not-Login-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-3--Back-forward-navigation---rendering-syncs-with-URL-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-3--Back-forward-navigation---rendering-syncs-with-URL-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4--Auth-state-preserved-when-navigating-between-authenticated-anonymous-states-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4--Auth-state-preserved-when-navigating-between-authenticated-anonymous-states-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4b--Browser-back-forward-preserves-auth-state-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-4b--Browser-back-forward-preserves-auth-state-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-5--Protected-route-with-play-route-respects-authentication-guard-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/uat-xstate-route-regression.browser.test.ts/UAT-5--Protected-route-with-play-route-respects-authentication-guard-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/user-reported-scenario.browser.test.tsx/User-scenario--home---about---home---contact---home--then-back-3x-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/user-reported-scenario.browser.test.tsx/User-scenario--login---home---about---home---contact---home--then-back-3x-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Browser-back-button-sends-play-route-event-with-correct-state-ID-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Browser-back-button-sends-play-route-event-with-correct-state-ID-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Browser-back-button-sends-xstate-route-event-with-correct-state-ID-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Direct-URL-navigation-sends-play-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Direct-URL-navigation-sends-xstate-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-play-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-play-route-event-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Forward-button-sends-xstate-route-event-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/GAP-12-fix-preserved--No-duplicate-history-entries-with-play-route-1.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/GAP-12-fix-preserved--No-duplicate-history-entries-with-play-route-2.png +0 -0
- package/examples/demo/test/browser/__screenshots__/xstate-route-events.browser.test.ts/Protected-route-sends-xstate-route-with-authentication-guard-1.png +0 -0
- package/examples/demo/test/browser/back-button-duplicate.browser.test.tsx +148 -0
- package/examples/demo/test/browser/back-forward-sync.browser.test.tsx +149 -0
- package/examples/demo/test/browser/direct-navigation.browser.test.ts +146 -0
- package/examples/demo/test/browser/exact-user-scenario.browser.test.tsx +207 -0
- package/examples/demo/test/browser/guard-rejection.browser.test.tsx +52 -0
- package/examples/demo/test/browser/history-investigation.browser.test.tsx +82 -0
- package/examples/demo/test/browser/history-navigation.browser.test.ts +351 -0
- package/examples/demo/test/browser/login-flow.browser.test.tsx +34 -0
- package/examples/demo/test/browser/navigation.browser.test.tsx +34 -0
- package/examples/demo/test/browser/protected-route-navigation.browser.test.tsx +161 -0
- package/examples/demo/test/browser/redirect-url-update.browser.test.tsx +140 -0
- package/examples/demo/test/browser/settings-parameter.browser.test.tsx +164 -0
- package/examples/demo/test/browser/settings-query-freeze.browser.test.ts +141 -0
- package/examples/demo/test/browser/state-driven.browser.test.ts +112 -0
- package/examples/demo/test/browser/tanstack-integration.browser.test.tsx +61 -0
- package/examples/demo/test/browser/uat-xstate-route-regression.browser.test.ts +58 -0
- package/examples/demo/test/browser/xstate-route-events.browser.test.ts +293 -0
- package/examples/demo/test/browser-back-view-rendering.test.ts +104 -0
- package/examples/demo/test/browser-e2e/auth-flow.browser.test.tsx +49 -0
- package/examples/demo/test/invalid-route-redirect.test.ts +40 -0
- package/examples/demo/test/passive-infra.test.ts +35 -0
- package/examples/demo/test/route-parameters.test.ts +539 -0
- package/examples/demo/test/signal-only.test.ts +54 -0
- package/examples/demo/test/strict-separation.test.ts +37 -0
- package/examples/demo/test/test-utils.ts +49 -0
- package/examples/demo/tsconfig.json +21 -0
- package/examples/demo/tsconfig.tsbuildinfo +1 -0
- package/examples/demo/vite.config.ts +13 -0
- package/examples/demo/vitest.browser.config.ts +72 -0
- package/examples/demo/vitest.config.e2e.browser.ts +28 -0
- package/examples/demo/vitest.config.ts +35 -0
- package/package.json +51 -0
- package/src/extract-params.ts +75 -0
- package/src/index.ts +31 -0
- package/src/play-router-provider.tsx +46 -0
- package/src/route-map.ts +158 -0
- package/src/tanstack-router-bridge.ts +135 -0
- package/src/types.ts +26 -0
- package/src/utils.ts +12 -0
- package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/Browser-back-button-sends-route-navigate-event-to-actor-1.png +0 -0
- package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/SignalSyncedHistory-prevents-circular-updates-1.png +0 -0
- package/test/browser/__screenshots__/signal-synced-history.browser.test.ts/SignalSyncedHistory-syncs-actor-route-to-browser-URL-1.png +0 -0
- package/test/browser/signal-synced-history.browser.test.ts +95 -0
- package/test/route-map.test.ts +107 -0
- package/test/tanstack-router-bridge.test.ts +318 -0
- package/test/urlpattern-integration.test.ts +145 -0
- package/tsconfig.json +16 -0
- package/tsconfig.tsbuildinfo +1 -0
- package/vitest.config.ts +35 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
# Demo Application Architecture
|
|
2
|
+
|
|
3
|
+
**Target reading time:** 15 minutes
|
|
4
|
+
|
|
5
|
+
This document provides a line-by-line walkthrough of the demo application's architecture, showing how the Universal Player Architecture implements all 5 architectural invariants.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
1. [Overview](#overview)
|
|
12
|
+
2. [Data Flow](#data-flow)
|
|
13
|
+
3. [File-by-File Explanation](#file-by-file-explanation)
|
|
14
|
+
4. [Key Patterns](#key-patterns)
|
|
15
|
+
5. [Verification](#verification)
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Overview
|
|
20
|
+
|
|
21
|
+
### What This Demo Proves
|
|
22
|
+
|
|
23
|
+
This demo validates **5 architectural invariants** that define the Universal Player Architecture:
|
|
24
|
+
|
|
25
|
+
1. **Actor Authority** - Actor guards control navigation, infrastructure obeys
|
|
26
|
+
2. **Strict Separation** - Business logic has zero UI framework imports
|
|
27
|
+
3. **Signal-Only Reactivity** - React rendering driven by signals, not React state
|
|
28
|
+
4. **Passive Infrastructure** - Components forward events, don't control navigation
|
|
29
|
+
5. **State-Driven Reset** - Browser back button sends event to actor for validation
|
|
30
|
+
|
|
31
|
+
### Tech Stack
|
|
32
|
+
|
|
33
|
+
- **Business Logic:** XState v5 state machines (`@xmachines/play-xstate`)
|
|
34
|
+
- **Reactive Substrate:** TC39 Signals (`@xmachines/play-signals`)
|
|
35
|
+
- **Router Integration:** TanStack Router (`@xmachines/play-tanstack-react-router`)
|
|
36
|
+
- **View Renderer:** React 18 (`@xmachines/play-react`)
|
|
37
|
+
- **UI Schema:** Zod catalog (`@xmachines/play-catalog`)
|
|
38
|
+
|
|
39
|
+
### Project Structure
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
examples/demo-app/
|
|
43
|
+
├── src/
|
|
44
|
+
│ ├── machines/ # Business logic (framework-agnostic)
|
|
45
|
+
│ │ ├── auth-machine.ts # XState machine with guards
|
|
46
|
+
│ │ └── catalog.ts # Type-safe component schemas
|
|
47
|
+
│ ├── components/ # React UI (Passive Infrastructure)
|
|
48
|
+
│ │ ├── LoginForm.tsx # Forwards auth.login event
|
|
49
|
+
│ │ └── Dashboard.tsx # Forwards auth.logout event
|
|
50
|
+
│ └── App.tsx # Integration layer (router + renderer)
|
|
51
|
+
├── test/
|
|
52
|
+
│ └── invariants/ # Automated invariant validation
|
|
53
|
+
└── docs/
|
|
54
|
+
├── ARCHITECTURE.md # This file
|
|
55
|
+
└── SWAP-REACT.md # Framework swapping guide
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Data Flow
|
|
61
|
+
|
|
62
|
+
### User Action → State Transition Flow
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
User clicks "Log In" button
|
|
66
|
+
↓
|
|
67
|
+
LoginForm.tsx forwards event via send() [Passive Infrastructure]
|
|
68
|
+
↓
|
|
69
|
+
actor.send({ type: "auth.login", username, password })
|
|
70
|
+
↓
|
|
71
|
+
XState machine processes event
|
|
72
|
+
↓
|
|
73
|
+
Guard checks if transition is valid [Actor Authority]
|
|
74
|
+
↓
|
|
75
|
+
Context updated: { isAuthenticated: true }
|
|
76
|
+
↓
|
|
77
|
+
State transition: loggedOut → dashboard
|
|
78
|
+
↓
|
|
79
|
+
Signal emits new value
|
|
80
|
+
↓
|
|
81
|
+
useSignalEffect detects change [Signal-Only Reactivity]
|
|
82
|
+
↓
|
|
83
|
+
PlayRenderer triggers React re-render
|
|
84
|
+
↓
|
|
85
|
+
SignalSyncedHistory observes actor.currentRoute signal (derived from state)
|
|
86
|
+
↓
|
|
87
|
+
Browser URL updates to /dashboard [Passive Infrastructure]
|
|
88
|
+
↓
|
|
89
|
+
React renders Dashboard component with new props
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Key Insight:** The actor decides everything. Infrastructure (router, renderer) observes and reacts.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## File-by-File Explanation
|
|
97
|
+
|
|
98
|
+
### 1. `src/machines/auth-machine.ts` - Business Logic Layer
|
|
99
|
+
|
|
100
|
+
**Purpose:** Defines authentication state machine with guards controlling navigation.
|
|
101
|
+
|
|
102
|
+
**Key Code:**
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
// Invariant: Strict Separation
|
|
106
|
+
// ZERO React or TanStack Router imports - business logic is framework-agnostic
|
|
107
|
+
import { createMachine, assign } from "xstate";
|
|
108
|
+
|
|
109
|
+
interface AuthContext {
|
|
110
|
+
isAuthenticated: boolean;
|
|
111
|
+
username: string | null;
|
|
112
|
+
routeParams: Record<string, string>;
|
|
113
|
+
queryParams: Record<string, string>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
type AuthEvent =
|
|
117
|
+
| { type: "play.route"; to: string }
|
|
118
|
+
| { type: "auth.login"; username: string; password: string }
|
|
119
|
+
| { type: "auth.logout" };
|
|
120
|
+
|
|
121
|
+
export const authMachine = createMachine({
|
|
122
|
+
id: "auth",
|
|
123
|
+
initial: "loggedOut",
|
|
124
|
+
context: {
|
|
125
|
+
isAuthenticated: false,
|
|
126
|
+
username: null,
|
|
127
|
+
routeParams: {},
|
|
128
|
+
queryParams: {},
|
|
129
|
+
},
|
|
130
|
+
states: {
|
|
131
|
+
loggedOut: {
|
|
132
|
+
meta: {
|
|
133
|
+
route: "/",
|
|
134
|
+
// Invariant: Strict Separation
|
|
135
|
+
// View structure defined in metadata, not JSX
|
|
136
|
+
view: {
|
|
137
|
+
component: "LoginForm",
|
|
138
|
+
props: { title: "Please Log In" },
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
on: {
|
|
142
|
+
"auth.login": {
|
|
143
|
+
target: "loggedIn",
|
|
144
|
+
actions: assign({
|
|
145
|
+
isAuthenticated: true,
|
|
146
|
+
username: ({ event }) => event.username,
|
|
147
|
+
}),
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
loggedIn: {
|
|
152
|
+
meta: {
|
|
153
|
+
route: "/dashboard",
|
|
154
|
+
view: {
|
|
155
|
+
component: "Dashboard",
|
|
156
|
+
props: { welcome: true },
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
// Invariant: Actor Authority
|
|
160
|
+
// Always-guard validates state entry - can I BE in this state?
|
|
161
|
+
always: [
|
|
162
|
+
{
|
|
163
|
+
target: "loggedOut",
|
|
164
|
+
guard: ({ context }) => !context.isAuthenticated,
|
|
165
|
+
},
|
|
166
|
+
],
|
|
167
|
+
on: {
|
|
168
|
+
"auth.logout": {
|
|
169
|
+
target: "loggedOut",
|
|
170
|
+
actions: assign({
|
|
171
|
+
isAuthenticated: false,
|
|
172
|
+
username: null,
|
|
173
|
+
}),
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**What This Demonstrates:**
|
|
182
|
+
|
|
183
|
+
- **Actor Authority:** Always-guard `({ context }) => !context.isAuthenticated` automatically redirects to loggedOut state when authentication is lost
|
|
184
|
+
- **Strict Separation:** No React imports - state machine is pure business logic
|
|
185
|
+
- **State-Driven Reset:** Machine processes play.route events from browser back/forward for validation
|
|
186
|
+
|
|
187
|
+
**Verification:**
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
# Verify zero React imports
|
|
191
|
+
grep -n "from ['\"']react" src/machines/auth-machine.ts
|
|
192
|
+
# (No output = passes)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### 2. `src/machines/catalog.ts` - Type-Safe UI Schema
|
|
198
|
+
|
|
199
|
+
**Purpose:** Defines component prop schemas using Zod for runtime validation.
|
|
200
|
+
|
|
201
|
+
**Key Code:**
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// Invariant: Strict Separation
|
|
205
|
+
// Catalog is framework-agnostic - no React imports
|
|
206
|
+
import { z } from "zod";
|
|
207
|
+
import { defineCatalog } from "@xmachines/play-catalog";
|
|
208
|
+
|
|
209
|
+
export const catalog = defineCatalog({
|
|
210
|
+
LoginForm: z.object({
|
|
211
|
+
title: z.string(),
|
|
212
|
+
}),
|
|
213
|
+
Dashboard: z.object({
|
|
214
|
+
welcome: z.boolean(),
|
|
215
|
+
}),
|
|
216
|
+
});
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
**What This Demonstrates:**
|
|
220
|
+
|
|
221
|
+
- **Strict Separation:** Catalog has zero React imports - props are inferred by renderer
|
|
222
|
+
- **Type Safety:** Zod schemas provide runtime validation and compile-time type inference
|
|
223
|
+
- **Framework Agnostic:** Same catalog works with React, Vue, Svelte, or any renderer
|
|
224
|
+
|
|
225
|
+
**Verification:**
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
# Verify zero React imports
|
|
229
|
+
grep -n "from ['\"']react" src/machines/catalog.ts
|
|
230
|
+
# (No output = passes)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
### 3. `src/components/LoginForm.tsx` - Passive Infrastructure Layer
|
|
236
|
+
|
|
237
|
+
**Purpose:** React component that forwards credentials to actor without controlling navigation.
|
|
238
|
+
|
|
239
|
+
**Key Code:**
|
|
240
|
+
|
|
241
|
+
```typescript
|
|
242
|
+
import React, { useState, type FormEvent } from "react";
|
|
243
|
+
|
|
244
|
+
interface LoginFormProps {
|
|
245
|
+
send: (event: any) => void;
|
|
246
|
+
title: string;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export const LoginForm: React.FC<LoginFormProps> = ({ send, title }) => {
|
|
250
|
+
// Local UI state ONLY (not business logic)
|
|
251
|
+
const [username, setUsername] = useState("");
|
|
252
|
+
const [password, setPassword] = useState("");
|
|
253
|
+
|
|
254
|
+
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
|
255
|
+
e.preventDefault();
|
|
256
|
+
|
|
257
|
+
// Invariant: Passive Infrastructure
|
|
258
|
+
// Forward event to actor, let actor decide validity
|
|
259
|
+
send({
|
|
260
|
+
type: "auth.login",
|
|
261
|
+
username,
|
|
262
|
+
password,
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
return (
|
|
267
|
+
<div>
|
|
268
|
+
<h1>{title}</h1>
|
|
269
|
+
<form onSubmit={handleSubmit}>
|
|
270
|
+
<input
|
|
271
|
+
type="text"
|
|
272
|
+
value={username}
|
|
273
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
274
|
+
required
|
|
275
|
+
/>
|
|
276
|
+
<input
|
|
277
|
+
type="password"
|
|
278
|
+
value={password}
|
|
279
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
280
|
+
required
|
|
281
|
+
/>
|
|
282
|
+
<button type="submit">Log In</button>
|
|
283
|
+
</form>
|
|
284
|
+
</div>
|
|
285
|
+
);
|
|
286
|
+
};
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**What This Demonstrates:**
|
|
290
|
+
|
|
291
|
+
- **Passive Infrastructure:** Component forwards `auth.login` event via `send()`, doesn't control navigation
|
|
292
|
+
- **Signal-Only Reactivity:** Business state comes from actor, not React state (username/password are ephemeral form inputs)
|
|
293
|
+
- **Strict Separation:** Zero router imports - component doesn't know about TanStack Router
|
|
294
|
+
|
|
295
|
+
**Verification:**
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
# Verify zero router imports
|
|
299
|
+
grep -n "@tanstack/react-router" src/components/LoginForm.tsx
|
|
300
|
+
# (No output = passes)
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
### 4. `src/components/Dashboard.tsx` - Protected Route Component
|
|
306
|
+
|
|
307
|
+
**Purpose:** Displays dashboard content and forwards logout event to actor.
|
|
308
|
+
|
|
309
|
+
**Key Code:**
|
|
310
|
+
|
|
311
|
+
```typescript
|
|
312
|
+
import React from "react";
|
|
313
|
+
|
|
314
|
+
interface DashboardProps {
|
|
315
|
+
send: (event: any) => void;
|
|
316
|
+
welcome: boolean;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export const Dashboard: React.FC<DashboardProps> = ({ send, welcome }) => {
|
|
320
|
+
const handleLogout = () => {
|
|
321
|
+
// Invariant: Passive Infrastructure
|
|
322
|
+
// Forward event to actor
|
|
323
|
+
send({ type: "auth.logout" });
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
return (
|
|
327
|
+
<div>
|
|
328
|
+
{welcome && <h1>Welcome to the Dashboard!</h1>}
|
|
329
|
+
<p>
|
|
330
|
+
This is a protected route - you can only see this because you're logged
|
|
331
|
+
in.
|
|
332
|
+
</p>
|
|
333
|
+
<button onClick={handleLogout}>Logout</button>
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
};
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
**What This Demonstrates:**
|
|
340
|
+
|
|
341
|
+
- **Passive Infrastructure:** Component forwards `auth.logout` event, doesn't navigate directly
|
|
342
|
+
- **Actor Authority:** Access control happens in actor guard, not component logic
|
|
343
|
+
|
|
344
|
+
**Verification:**
|
|
345
|
+
|
|
346
|
+
```bash
|
|
347
|
+
# Verify zero router imports
|
|
348
|
+
grep -n "@tanstack/react-router" src/components/Dashboard.tsx
|
|
349
|
+
# (No output = passes)
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
---
|
|
353
|
+
|
|
354
|
+
### 5. `src/App.tsx` - Integration Layer
|
|
355
|
+
|
|
356
|
+
**Purpose:** Wires together actor, router, and renderer.
|
|
357
|
+
|
|
358
|
+
**Key Code:**
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
import { definePlayer } from "@xmachines/play-xstate";
|
|
362
|
+
import { createPlayRouter } from "@xmachines/play-tanstack-react-router";
|
|
363
|
+
import { PlayRenderer } from "@xmachines/play-react";
|
|
364
|
+
import { defineComponents } from "@xmachines/play-catalog";
|
|
365
|
+
import { authMachine } from "./machines/auth-machine";
|
|
366
|
+
import { catalog } from "./machines/catalog";
|
|
367
|
+
import { LoginForm } from "./components/LoginForm";
|
|
368
|
+
import { Dashboard } from "./components/Dashboard";
|
|
369
|
+
|
|
370
|
+
// Define component implementations
|
|
371
|
+
const components = defineComponents(catalog, {
|
|
372
|
+
LoginForm,
|
|
373
|
+
Dashboard,
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
// Create player factory
|
|
377
|
+
const createPlayer = definePlayer({
|
|
378
|
+
machine: authMachine,
|
|
379
|
+
catalog,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
function App() {
|
|
383
|
+
// Create actor instance
|
|
384
|
+
const actor = createPlayer();
|
|
385
|
+
|
|
386
|
+
// Start actor
|
|
387
|
+
actor.start();
|
|
388
|
+
|
|
389
|
+
// Create router that syncs with actor state
|
|
390
|
+
// Invariant: Passive Infrastructure
|
|
391
|
+
// Router observes actor state, sends play.route events on browser back/forward
|
|
392
|
+
const router = createPlayRouter({
|
|
393
|
+
actor,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
return (
|
|
397
|
+
<>
|
|
398
|
+
{/* Invariant: Signal-Only Reactivity */}
|
|
399
|
+
{/* PlayRenderer uses useSignalEffect to observe actor signals */}
|
|
400
|
+
<PlayRenderer actor={actor} components={components} />
|
|
401
|
+
</>
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export default App;
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**What This Demonstrates:**
|
|
409
|
+
|
|
410
|
+
- **Signal-Only Reactivity:** `PlayRenderer` uses `useSignalEffect` to observe actor signals
|
|
411
|
+
- **Passive Infrastructure:** Router created with `createPlayRouter({ actor })` - router observes, doesn't control
|
|
412
|
+
- **Actor Authority:** Actor is the single source of truth - router and renderer react to actor state
|
|
413
|
+
|
|
414
|
+
**Flow:**
|
|
415
|
+
|
|
416
|
+
1. `createPlayer()` creates actor factory from machine + catalog
|
|
417
|
+
2. `actor.start()` initializes state machine
|
|
418
|
+
3. `createPlayRouter({ actor })` creates `SignalSyncedHistory` that:
|
|
419
|
+
- Observes `actor.state` signal via `Signal.subtle.Watcher`
|
|
420
|
+
- Updates browser URL when actor changes state
|
|
421
|
+
- Sends `play.route` event to actor when browser back/forward pressed
|
|
422
|
+
4. `PlayRenderer` observes `actor.state` signal and renders matching component
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Key Patterns
|
|
427
|
+
|
|
428
|
+
### Pattern 1: Actor Authority (Guard-Based Navigation)
|
|
429
|
+
|
|
430
|
+
**Code Location:** `src/machines/auth-machine.ts`
|
|
431
|
+
|
|
432
|
+
```typescript
|
|
433
|
+
loggedIn: {
|
|
434
|
+
meta: { route: "/dashboard" },
|
|
435
|
+
// Always-guard validates state entry
|
|
436
|
+
always: [
|
|
437
|
+
{
|
|
438
|
+
target: "loggedOut",
|
|
439
|
+
guard: ({ context }) => !context.isAuthenticated
|
|
440
|
+
}
|
|
441
|
+
]
|
|
442
|
+
}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**How It Works:**
|
|
446
|
+
|
|
447
|
+
- Always-guard checks: "Can I BE in this state given current context?"
|
|
448
|
+
- If `!isAuthenticated`, automatically transitions to loggedOut
|
|
449
|
+
- Guards validate state invariants, not event properties
|
|
450
|
+
- See [demo-app/auth-machine.ts](../src/machines/auth-machine.ts) for full implementation using `formatPlayRouteTransitions`
|
|
451
|
+
|
|
452
|
+
**Test:** `test/invariants/actor-authority.test.ts` - Attempts to navigate to `/dashboard` while logged out, verifies guard rejects
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
### Pattern 2: meta.view Definition (Strict Separation)
|
|
457
|
+
|
|
458
|
+
**Code Location:** `src/machines/auth-machine.ts` lines 55-63
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
meta: {
|
|
462
|
+
route: "/",
|
|
463
|
+
view: {
|
|
464
|
+
component: "LoginForm",
|
|
465
|
+
props: { title: "Please Log In" },
|
|
466
|
+
},
|
|
467
|
+
}
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**How It Works:**
|
|
471
|
+
|
|
472
|
+
- View structure defined declaratively in state machine metadata
|
|
473
|
+
- Component name is a string reference (not JSX import)
|
|
474
|
+
- Props are plain data (not React elements)
|
|
475
|
+
- Renderer resolves component name to actual component via catalog
|
|
476
|
+
|
|
477
|
+
**Test:** `test/invariants/strict-separation.test.ts` - Verifies zero React imports in `auth-machine.ts` and `catalog.ts`
|
|
478
|
+
|
|
479
|
+
---
|
|
480
|
+
|
|
481
|
+
### Pattern 3: Signal Observation (Signal-Only Reactivity)
|
|
482
|
+
|
|
483
|
+
**Code Location:** `packages/play-react/src/PlayRenderer.tsx` (simplified)
|
|
484
|
+
|
|
485
|
+
```typescript
|
|
486
|
+
function PlayRenderer({ actor, components }) {
|
|
487
|
+
const [, setTick] = useState(0);
|
|
488
|
+
|
|
489
|
+
useSignalEffect(() => {
|
|
490
|
+
// Observe actor.state signal
|
|
491
|
+
const currentState = actor.state.get();
|
|
492
|
+
// Trigger React re-render
|
|
493
|
+
setTick((t) => t + 1);
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
const currentState = actor.state.get();
|
|
497
|
+
const view = currentState.meta?.view;
|
|
498
|
+
const Component = components[view.component];
|
|
499
|
+
|
|
500
|
+
return <Component send={actor.send.bind(actor)} {...view.props} />;
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
**How It Works:**
|
|
505
|
+
|
|
506
|
+
- `useSignalEffect` uses `Signal.subtle.Watcher` to observe `actor.state` signal
|
|
507
|
+
- When signal changes, React is triggered via `setTick`
|
|
508
|
+
- Business state lives in actor, not React state
|
|
509
|
+
|
|
510
|
+
**Test:** `test/invariants/signal-only.test.ts` - Verifies `PlayRenderer` uses `useSignalEffect`
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
### Pattern 4: Event Forwarding (Passive Infrastructure)
|
|
515
|
+
|
|
516
|
+
**Code Location:** `src/components/LoginForm.tsx` lines 54-64
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
|
520
|
+
e.preventDefault();
|
|
521
|
+
send({ type: "auth.login", username, password });
|
|
522
|
+
};
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
**How It Works:**
|
|
526
|
+
|
|
527
|
+
- Component receives `send` function via props (bound to actor)
|
|
528
|
+
- Component forwards event to actor without knowing actor internals
|
|
529
|
+
- Actor processes event and decides outcome
|
|
530
|
+
|
|
531
|
+
**Test:** `test/invariants/passive-infra.test.ts` - Verifies components have zero router imports, use `send()` for events
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
### Pattern 5: Navigation Handling (State-Driven Reset)
|
|
536
|
+
|
|
537
|
+
**Code Location:** `packages/play-tanstack-react-router/src/SignalSyncedHistory.ts` (simplified)
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
class SignalSyncedHistory {
|
|
541
|
+
constructor(actor) {
|
|
542
|
+
// Listen to browser popstate (back/forward)
|
|
543
|
+
window.addEventListener("popstate", () => {
|
|
544
|
+
const newPath = window.location.pathname;
|
|
545
|
+
// Send event to actor for validation
|
|
546
|
+
actor.send({ type: "play.route", to: newPath });
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
// Observe actor's currentRoute signal (derived from state)
|
|
550
|
+
const watcher = new Signal.subtle.Watcher(() => {
|
|
551
|
+
const route = actor.currentRoute.get();
|
|
552
|
+
// Update browser URL if actor changed route
|
|
553
|
+
if (route) window.history.pushState({}, "", route);
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
**How It Works:**
|
|
560
|
+
|
|
561
|
+
- Browser back/forward triggers `popstate` event
|
|
562
|
+
- SignalSyncedHistory sends `play.route` event to actor
|
|
563
|
+
- Actor processes event - always-guards validate state entry
|
|
564
|
+
- If guards allow, state updates and signal emits
|
|
565
|
+
- SignalSyncedHistory observes signal and updates browser URL
|
|
566
|
+
|
|
567
|
+
**Test:** `test/invariants/state-driven.test.ts` - Sends `play.route` event to simulate browser back, verifies actor processes it
|
|
568
|
+
|
|
569
|
+
---
|
|
570
|
+
|
|
571
|
+
## Verification
|
|
572
|
+
|
|
573
|
+
### Automated Tests
|
|
574
|
+
|
|
575
|
+
Run all invariant validation tests:
|
|
576
|
+
|
|
577
|
+
```bash
|
|
578
|
+
cd examples/demo-app
|
|
579
|
+
npm test
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
Expected output:
|
|
583
|
+
|
|
584
|
+
```
|
|
585
|
+
✔ DEMO-02: Actor Authority - Guards reject invalid navigation
|
|
586
|
+
✔ DEMO-03: Strict Separation - Business logic has zero React imports
|
|
587
|
+
✔ DEMO-04: State-Driven Reset - Browser back sends event to actor
|
|
588
|
+
✔ DEMO-05: Passive Infrastructure - Components never import TanStack Router
|
|
589
|
+
✔ DEMO-06: Signal-Only Reactivity - PlayRenderer uses signals not React state
|
|
590
|
+
|
|
591
|
+
ℹ tests 5
|
|
592
|
+
ℹ pass 5
|
|
593
|
+
ℹ fail 0
|
|
594
|
+
```
|
|
595
|
+
|
|
596
|
+
### Manual Verification
|
|
597
|
+
|
|
598
|
+
1. **Actor Authority:**
|
|
599
|
+
|
|
600
|
+
```bash
|
|
601
|
+
npm run dev
|
|
602
|
+
# Visit http://localhost:3000/dashboard directly (while logged out)
|
|
603
|
+
# Verify: URL stays at / or redirects (guard rejected)
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
2. **Strict Separation:**
|
|
607
|
+
|
|
608
|
+
```bash
|
|
609
|
+
grep -rn "from ['\"']react" src/machines/
|
|
610
|
+
# Expected: no output (zero React imports)
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
3. **Passive Infrastructure:**
|
|
614
|
+
|
|
615
|
+
```bash
|
|
616
|
+
grep -rn "@tanstack/react-router" src/components/
|
|
617
|
+
# Expected: no output (zero router imports)
|
|
618
|
+
```
|
|
619
|
+
|
|
620
|
+
4. **Signal-Only Reactivity:**
|
|
621
|
+
|
|
622
|
+
```bash
|
|
623
|
+
grep -n "useSignalEffect" packages/play-react/src/PlayRenderer.tsx
|
|
624
|
+
# Expected: line number showing useSignalEffect usage
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
5. **State-Driven Reset:**
|
|
628
|
+
```bash
|
|
629
|
+
# Run demo, login, click browser back button
|
|
630
|
+
# Observe: actor.send({ type: "play.route", to: "/" }) logged
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## Next Steps
|
|
636
|
+
|
|
637
|
+
- **Understand Invariants:** Read the RFC specification for detailed explanations
|
|
638
|
+
- **Swap Frameworks:** Read [SWAP-REACT.md](./SWAP-REACT.md) to see how to replace React with Vue/Svelte
|
|
639
|
+
- **Build Your Own:** Use this demo as a template for your application
|
|
640
|
+
|
|
641
|
+
---
|
|
642
|
+
|
|
643
|
+
**Questions?** See the RFC specification for deeper invariant explanations and rationale.
|