-
-
Notifications
You must be signed in to change notification settings - Fork 124
Add SSR snapshot support for web adapter SSR flows #782
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1,9 @@ | ||
| /// <reference types="vite/client" /> | ||
|
|
||
| import type { WebAdapterSsrEncodedSnapshot } from '@livestore/adapter-web' | ||
|
|
||
| declare global { | ||
| interface Window { | ||
| __LIVESTORE_SSR__?: Record<string, WebAdapterSsrEncodedSnapshot> | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,26 @@ | ||
| import { createWebAdapterSsrSnapshot, encodeWebAdapterSsrSnapshot } from '@livestore/adapter-web' | ||
|
|
||
| import { getRequestInfo } from 'rwsdk/worker' | ||
| import { schema } from '../todomvc/livestore/schema.js' | ||
| import { TodoApp } from '../todomvc/TodoApp.js' | ||
|
|
||
| export const Home = () => { | ||
| export const Home = async () => { | ||
| const requestInfo = getRequestInfo() | ||
|
|
||
| if (requestInfo.rw.ssr) { | ||
| try { | ||
| const snapshot = await createWebAdapterSsrSnapshot({ schema }) | ||
| const encodedSnapshot = encodeWebAdapterSsrSnapshot(snapshot) | ||
|
|
||
| requestInfo.rw.inlineScripts.add( | ||
| `window.__LIVESTORE_SSR__=window.__LIVESTORE_SSR__??{};window.__LIVESTORE_SSR__['${snapshot.storeId}']=${JSON.stringify( | ||
| encodedSnapshot, | ||
| )};`, | ||
| ) | ||
| } catch (error) { | ||
| console.error('[TodoMVC] Failed to prepare LiveStore SSR snapshot', error) | ||
| } | ||
| } | ||
|
|
||
| return <TodoApp /> | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,11 @@ | ||
| export { makeInMemoryAdapter } from './in-memory/in-memory-adapter.ts' | ||
| export { | ||
| type CreateWebAdapterSsrSnapshotOptions, | ||
| createWebAdapterSsrSnapshot, | ||
| decodeWebAdapterSsrSnapshot, | ||
| encodeWebAdapterSsrSnapshot, | ||
| type WebAdapterSsrEncodedSnapshot, | ||
| type WebAdapterSsrSnapshot, | ||
| } from './ssr.ts' | ||
| export { makePersistedAdapter, type WebAdapterOptions } from './web-worker/client-session/persisted-adapter.ts' | ||
| export * as WorkerSchema from './web-worker/common/worker-schema.ts' |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| import { makeAdapter as makeNodeAdapter } from '@livestore/adapter-node' | ||
|
Check failure on line 1 in packages/@livestore/adapter-web/src/ssr.ts
|
||
| import type { SyncOptions } from '@livestore/common' | ||
| import { liveStoreVersion, type MigrationsReport } from '@livestore/common' | ||
| import type { LiveStoreSchema } from '@livestore/common/schema' | ||
| import { createStorePromise } from '@livestore/livestore' | ||
| import { omitUndefineds } from '@livestore/utils' | ||
| import type { Schema } from '@livestore/utils/effect' | ||
|
|
||
| const DEFAULT_INITIAL_SYNC_TIMEOUT = 5_000 | ||
|
|
||
| export interface WebAdapterSsrSnapshot { | ||
| storeId: string | ||
| snapshot: Uint8Array<ArrayBuffer> | ||
| migrationsReport: MigrationsReport | ||
| liveStoreVersion: string | ||
| } | ||
|
|
||
| export interface WebAdapterSsrEncodedSnapshot { | ||
| storeId: string | ||
| snapshotBase64: string | ||
| migrationsReport: MigrationsReport | ||
| liveStoreVersion: string | ||
| } | ||
|
|
||
| export interface CreateWebAdapterSsrSnapshotOptions { | ||
| schema: LiveStoreSchema | ||
| storeId?: string | ||
| sync?: SyncOptions | ||
| syncPayload?: Schema.JsonValue | ||
| storage?: Parameters<typeof makeNodeAdapter>[0]['storage'] | ||
| } | ||
|
|
||
| export const createWebAdapterSsrSnapshot = async ({ | ||
| schema, | ||
| storeId = 'default', | ||
| sync, | ||
| syncPayload, | ||
| storage, | ||
| }: CreateWebAdapterSsrSnapshotOptions): Promise<WebAdapterSsrSnapshot> => { | ||
| const adapter = makeNodeAdapter({ | ||
| storage: storage ?? { type: 'in-memory' }, | ||
| ...omitUndefineds({ | ||
| sync: | ||
| sync ?? | ||
| ({ | ||
| initialSyncOptions: { _tag: 'Blocking' as const, timeout: DEFAULT_INITIAL_SYNC_TIMEOUT }, | ||
| } satisfies SyncOptions), | ||
| }), | ||
| }) | ||
|
|
||
| const store = await createStorePromise({ | ||
| schema, | ||
| storeId, | ||
| adapter, | ||
| disableDevtools: true, | ||
| batchUpdates: (run) => run(), | ||
| syncPayload, | ||
| }) | ||
|
|
||
| try { | ||
| return { | ||
| storeId, | ||
| snapshot: store.sqliteDbWrapper.export(), | ||
| migrationsReport: store.clientSession.leaderThread.initialState.migrationsReport, | ||
| liveStoreVersion, | ||
| } | ||
| } finally { | ||
| await store.shutdownPromise().catch(() => {}) | ||
| } | ||
| } | ||
|
|
||
| export const encodeWebAdapterSsrSnapshot = (snapshot: WebAdapterSsrSnapshot): WebAdapterSsrEncodedSnapshot => ({ | ||
| storeId: snapshot.storeId, | ||
| snapshotBase64: encodeBase64(snapshot.snapshot), | ||
| migrationsReport: snapshot.migrationsReport, | ||
| liveStoreVersion: snapshot.liveStoreVersion, | ||
| }) | ||
|
|
||
| export const decodeWebAdapterSsrSnapshot = (encoded: WebAdapterSsrEncodedSnapshot): WebAdapterSsrSnapshot => ({ | ||
| storeId: encoded.storeId, | ||
| snapshot: decodeBase64(encoded.snapshotBase64), | ||
| migrationsReport: encoded.migrationsReport, | ||
| liveStoreVersion: encoded.liveStoreVersion, | ||
| }) | ||
|
|
||
| const encodeBase64 = (bytes: Uint8Array<ArrayBuffer>): string => { | ||
| if (typeof Buffer !== 'undefined') { | ||
| return Buffer.from(bytes).toString('base64') | ||
| } | ||
|
|
||
| if (typeof globalThis.btoa === 'function') { | ||
| let binary = '' | ||
| for (const byte of bytes) { | ||
| binary += String.fromCharCode(byte) | ||
| } | ||
|
|
||
| return globalThis.btoa(binary) | ||
| } | ||
|
|
||
| throw new Error('Base64 encoding is not supported in this environment') | ||
| } | ||
|
|
||
| const decodeBase64 = (value: string): Uint8Array<ArrayBuffer> => { | ||
| if (typeof Buffer !== 'undefined') { | ||
| return new Uint8Array(Buffer.from(value, 'base64')) | ||
| } | ||
|
|
||
| if (typeof globalThis.atob === 'function') { | ||
| const binary = globalThis.atob(value) | ||
|
|
||
| const bytes = new Uint8Array(binary.length) | ||
| for (let i = 0; i < binary.length; i++) { | ||
| bytes[i] = binary.charCodeAt(i) | ||
| } | ||
|
|
||
| return bytes | ||
| } | ||
|
|
||
| throw new Error('Base64 decoding is not supported in this environment') | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.