Repository navigation
Server rendering: read snapshots, and a Node backend that writes on a worker thread - #4
Open
sleeper-luke wants to merge 6 commits into
Open
sleeper-luke wants to merge 6 commits into
sleeper-luke wants to merge 6 commits into
Conversation
…an run a native shred in JS Co-authored-by: Cursor <cursoragent@cursor.com>
…ser renders it until its stores have the rows Co-authored-by: Cursor <cursoragent@cursor.com>
…g on a worker thread Co-authored-by: Cursor <cursoragent@cursor.com>
…he Node entry Co-authored-by: Cursor <cursoragent@cursor.com>
…nt can import Cellar under a React with no createContext Co-authored-by: Cursor <cursoragent@cursor.com>
…'t know the module yet can still bundle it Co-authored-by: Cursor <cursoragent@cursor.com>
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.
Before: Cellar only runs where its stores bind in the same process that renders: a device, or a browser page. A server that renders with React, such as sleeper-web on Next.js, has no way to render a store's reads. Its hooks answer
emptythere, so the server's HTML and the browser's first render can't carry players, stats or schedules. There is also no SQLite binding for Node.After:
await dehydrateReads([readCall(PlayerService.Hooks.usePlayerDetail, { sport, playerId }), …]). It hands the snapshot to<CellarHydrate snapshot={…}>.readCalltakes a service's existing hook or getter, so a service needs no changes.CellarHydrate, a read answers from the snapshot while its store has no rows for the partition, and doesn't fetch the partition.readCall(…, { live: true })makes the browser fetch it and move to the store's value once it lands, for data that keeps changing.configureCellar({ serverRendering: true })makes hooks answer only from the snapshot and never fetch, so the HTML is what the browser renders first, before its own stores are bound. A call the snapshot doesn't hold rendersempty, with the status the browser's first render reports.getValueand a store's lifecycle still read and fetch, which is how a server fills a snapshot.@sleeperhq/react-native-cellar/nodeentry:bindNodeStore(label, store)binds a store to anode:sqlitedatabase of its own. A worker thread parses each fetched body and writes its rows; the main thread reads through its own connection to the same file, which WAL lets run alongside a write.How:
read/hydration.tsholds the snapshot type,dehydrateReads,readCallandCellarHydrate.capture(args, live)that awaits its partitions through the fetch ingest'sprefetch, then runsselect.pairReadattaches that capture to the hook and getter it publishes.store.readplus the read's args key. That key is a pure content string, so the server and the browser derive the same one.node/node_connection.tskeeps an ordered request port to the writer (node/writer.ts):execute,executeBatch) waits on a shared signal and collects its reply withreceiveMessageOnPort;write/js_shred.ts). Both connections cache prepared statements.Symbol.for). A server can load the core through its ESM build and through the CommonJS./nodeentry, and both see one configuration../nodeis CommonJS under both conditions, because the writer is located as a file beside the entry.packaging.test.tspins that exception instead of loosening the one-build rule for the other subpaths.Measured: app-shared's real player and stats stores, against production payloads (NFL players 4.1 MB; NFL season stats 5.3 MB):
A refresh with a tenth of the rows changed blocks the main thread for 1–26 ms.
Limits:
node:sqlitelogs anExperimentalWarningonce per thread on Node 22/24.serverExternalPackagesin Next.js), so the writer file resolves beside the entry.null, while a React server-component prop keepsundefined.CellarHydrateabove it, behavior is unchanged.Tests:
src/tests/read/hydration.test.ts: capture, failure reporting, the browser and server hook paths,useEach, nested providers.src/tests/node/node_connection.test.ts: ordering, temp tables, rollback, shred parity with sql.js, a store over the writer, snapshot capture, and the main thread staying free during a 30,000-row ingest (22 ms against 325 ms).yarn test(815 passed),yarn test:prod(797 passed),yarn build, and the rozenite plugin's typecheck and tests all pass locally../nodeentry, the writer at its default path.