@strata-sync/next 0.1.0

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.
@@ -0,0 +1,624 @@
1
+ import type { StorageAdapter } from "@strata-sync/client";
2
+ import type {
3
+ BootstrapMetadata,
4
+ ModelRegistrySnapshot,
5
+ ModelRow,
6
+ SchemaDefinition,
7
+ } from "@strata-sync/core";
8
+ import {
9
+ computeSchemaHash,
10
+ getOrCreateClientId,
11
+ ModelRegistry,
12
+ } from "@strata-sync/core";
13
+
14
+ const DEFAULT_PREFETCH_TIMEOUT_MS = 10_000;
15
+ const NDJSON_ACCEPT_HEADER = "application/x-ndjson";
16
+ const TRAILING_SLASH_RE = /\/+$/;
17
+ const KNOWN_SYNC_SUFFIXES = ["/bootstrap", "/batch", "/deltas"];
18
+
19
+ export interface BootstrapSnapshot {
20
+ version: 1;
21
+ schemaHash: string;
22
+ lastSyncId: number;
23
+ firstSyncId?: number;
24
+ groups: string[];
25
+ rows: ModelRow[];
26
+ fetchedAt: number;
27
+ rowCount?: number;
28
+ }
29
+
30
+ export interface PrefetchBootstrapOptions {
31
+ endpoint: string;
32
+ authorization?: string;
33
+ headers?: Record<string, string>;
34
+ models?: string[];
35
+ groups?: string[];
36
+ schemaHash?: string;
37
+ timeout?: number;
38
+ }
39
+
40
+ export async function prefetchBootstrap(
41
+ options: PrefetchBootstrapOptions
42
+ ): Promise<BootstrapSnapshot> {
43
+ const {
44
+ endpoint,
45
+ authorization,
46
+ headers,
47
+ models,
48
+ groups,
49
+ schemaHash,
50
+ timeout = DEFAULT_PREFETCH_TIMEOUT_MS,
51
+ } = options;
52
+
53
+ const requestHeaders = buildRequestHeaders(authorization, headers);
54
+ const params = buildBootstrapParams({ models, groups, schemaHash });
55
+ const url = buildBootstrapUrl(endpoint, params);
56
+
57
+ const response = await fetchBootstrap(url, requestHeaders, timeout);
58
+ await ensureResponseOk(response);
59
+
60
+ const { rows, metadata, rowCount } = await readBootstrapStream(
61
+ getResponseBody(response)
62
+ );
63
+
64
+ const resolvedMetadata = ensureBootstrapMetadata(metadata);
65
+ const snapshotSchemaHash = resolveSnapshotSchemaHash(
66
+ resolvedMetadata,
67
+ schemaHash
68
+ );
69
+
70
+ return {
71
+ version: 1,
72
+ schemaHash: snapshotSchemaHash,
73
+ lastSyncId: resolvedMetadata.lastSyncId,
74
+ firstSyncId: resolveFirstSyncId(resolvedMetadata),
75
+ groups: resolvedMetadata.subscribedSyncGroups,
76
+ rows,
77
+ fetchedAt: Date.now(),
78
+ rowCount,
79
+ };
80
+ }
81
+
82
+ function buildRequestHeaders(
83
+ authorization?: string,
84
+ headers?: Record<string, string>
85
+ ): Record<string, string> {
86
+ const requestHeaders: Record<string, string> = {
87
+ Accept: NDJSON_ACCEPT_HEADER,
88
+ ...headers,
89
+ };
90
+
91
+ if (authorization) {
92
+ requestHeaders.Authorization = authorization;
93
+ }
94
+
95
+ return requestHeaders;
96
+ }
97
+
98
+ function buildBootstrapParams(options: {
99
+ models?: string[];
100
+ groups?: string[];
101
+ schemaHash?: string;
102
+ }): URLSearchParams {
103
+ const params = new URLSearchParams();
104
+ params.set("type", "full");
105
+
106
+ if (options.models?.length) {
107
+ params.set("onlyModels", options.models.join(","));
108
+ }
109
+ if (options.schemaHash) {
110
+ params.set("schemaHash", options.schemaHash);
111
+ }
112
+ if (options.groups?.length) {
113
+ params.set("syncGroups", options.groups.join(","));
114
+ }
115
+
116
+ return params;
117
+ }
118
+
119
+ function buildBootstrapUrl(endpoint: string, params: URLSearchParams): string {
120
+ const baseEndpoint = normalizeSyncEndpoint(endpoint);
121
+ const url = joinSyncUrl(baseEndpoint, "/bootstrap");
122
+ return `${url}?${params.toString()}`;
123
+ }
124
+
125
+ async function fetchBootstrap(
126
+ url: string,
127
+ headers: Record<string, string>,
128
+ timeout: number
129
+ ): Promise<Response> {
130
+ const controller = new AbortController();
131
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
132
+
133
+ try {
134
+ return await fetch(url, {
135
+ method: "GET",
136
+ headers,
137
+ signal: controller.signal,
138
+ });
139
+ } finally {
140
+ clearTimeout(timeoutId);
141
+ }
142
+ }
143
+
144
+ async function ensureResponseOk(response: Response): Promise<void> {
145
+ if (response.ok) {
146
+ return;
147
+ }
148
+
149
+ const text = await response.text();
150
+ throw new Error(`Bootstrap prefetch failed: ${response.status} ${text}`);
151
+ }
152
+
153
+ function getResponseBody(response: Response): ReadableStream<Uint8Array> {
154
+ if (!response.body) {
155
+ throw new Error("Bootstrap prefetch response has no body");
156
+ }
157
+ return response.body;
158
+ }
159
+
160
+ interface BootstrapParseResult {
161
+ rows: ModelRow[];
162
+ metadata: BootstrapMetadata | null;
163
+ rowCount?: number;
164
+ }
165
+
166
+ interface BootstrapParseState extends BootstrapParseResult {
167
+ buffer: string;
168
+ }
169
+
170
+ async function readBootstrapStream(
171
+ stream: ReadableStream<Uint8Array>
172
+ ): Promise<BootstrapParseResult> {
173
+ const reader = stream.getReader();
174
+ const decoder = new TextDecoder();
175
+ const state: BootstrapParseState = {
176
+ rows: [],
177
+ metadata: null,
178
+ rowCount: undefined,
179
+ buffer: "",
180
+ };
181
+
182
+ try {
183
+ while (true) {
184
+ const { done, value } = await reader.read();
185
+ if (done) {
186
+ break;
187
+ }
188
+
189
+ state.buffer += decoder.decode(value, { stream: true });
190
+ state.buffer = drainBootstrapLines(state, state.buffer);
191
+ }
192
+
193
+ flushBootstrapBuffer(state);
194
+ } finally {
195
+ reader.releaseLock();
196
+ }
197
+
198
+ return {
199
+ rows: state.rows,
200
+ metadata: state.metadata,
201
+ rowCount: state.rowCount,
202
+ };
203
+ }
204
+
205
+ function drainBootstrapLines(
206
+ state: BootstrapParseState,
207
+ buffer: string
208
+ ): string {
209
+ const lines = buffer.split("\n");
210
+ const remaining = lines.pop() ?? "";
211
+
212
+ for (const line of lines) {
213
+ applyBootstrapLine(state, line);
214
+ }
215
+
216
+ return remaining;
217
+ }
218
+
219
+ function flushBootstrapBuffer(state: BootstrapParseState): void {
220
+ const trimmed = state.buffer.trim();
221
+ if (!trimmed) {
222
+ return;
223
+ }
224
+ applyBootstrapLine(state, trimmed);
225
+ }
226
+
227
+ function applyBootstrapLine(state: BootstrapParseState, line: string): void {
228
+ const parsed = parseBootstrapLine(line);
229
+ if (!parsed) {
230
+ return;
231
+ }
232
+
233
+ if (parsed.type === "meta") {
234
+ state.metadata = parsed.metadata;
235
+ return;
236
+ }
237
+
238
+ if (parsed.type === "row") {
239
+ state.rows.push(parsed.row);
240
+ return;
241
+ }
242
+
243
+ state.rowCount = parsed.rowCount;
244
+ }
245
+
246
+ interface ValidatedBootstrapMetadata extends BootstrapMetadata {
247
+ lastSyncId: number;
248
+ }
249
+
250
+ function ensureBootstrapMetadata(
251
+ metadata: BootstrapMetadata | null
252
+ ): ValidatedBootstrapMetadata {
253
+ if (!metadata) {
254
+ throw new Error("Bootstrap prefetch did not receive metadata");
255
+ }
256
+ if (metadata.lastSyncId === undefined) {
257
+ throw new Error("Bootstrap metadata is missing lastSyncId");
258
+ }
259
+ return metadata as ValidatedBootstrapMetadata;
260
+ }
261
+
262
+ function resolveSnapshotSchemaHash(
263
+ metadata: BootstrapMetadata,
264
+ fallback?: string
265
+ ): string {
266
+ return metadata.schemaHash ?? fallback ?? "";
267
+ }
268
+
269
+ function resolveFirstSyncId(metadata: ValidatedBootstrapMetadata): number {
270
+ return typeof metadata.raw?.firstSyncId === "number"
271
+ ? metadata.raw.firstSyncId
272
+ : metadata.lastSyncId;
273
+ }
274
+
275
+ export interface BootstrapSnapshotPayload {
276
+ version: 1;
277
+ encoding: "json" | "gzip-base64";
278
+ data: string;
279
+ }
280
+
281
+ export interface SerializeBootstrapOptions {
282
+ compress?: boolean;
283
+ }
284
+
285
+ export async function serializeBootstrapSnapshot(
286
+ snapshot: BootstrapSnapshot,
287
+ options: SerializeBootstrapOptions = {}
288
+ ): Promise<BootstrapSnapshotPayload> {
289
+ const json = JSON.stringify(snapshot);
290
+ const shouldCompress =
291
+ options.compress !== false && canUseCompressionStreams();
292
+
293
+ if (!shouldCompress) {
294
+ return {
295
+ version: snapshot.version,
296
+ encoding: "json",
297
+ data: json,
298
+ };
299
+ }
300
+
301
+ const compressed = await compressToBase64(json);
302
+ return {
303
+ version: snapshot.version,
304
+ encoding: "gzip-base64",
305
+ data: compressed,
306
+ };
307
+ }
308
+
309
+ export async function deserializeBootstrapSnapshot(
310
+ payload: BootstrapSnapshotPayload
311
+ ): Promise<BootstrapSnapshot> {
312
+ if (payload.version !== 1) {
313
+ throw new Error(
314
+ `Unsupported bootstrap payload version: ${payload.version}`
315
+ );
316
+ }
317
+
318
+ if (payload.encoding === "json") {
319
+ return JSON.parse(payload.data) as BootstrapSnapshot;
320
+ }
321
+
322
+ if (!canUseCompressionStreams()) {
323
+ throw new Error("DecompressionStream is not available in this runtime");
324
+ }
325
+
326
+ const json = await decompressFromBase64(payload.data);
327
+ return JSON.parse(json) as BootstrapSnapshot;
328
+ }
329
+
330
+ export async function encodeBootstrapSnapshot(
331
+ snapshot: BootstrapSnapshot,
332
+ options: SerializeBootstrapOptions = {}
333
+ ): Promise<string> {
334
+ const payload = await serializeBootstrapSnapshot(snapshot, options);
335
+ return JSON.stringify(payload);
336
+ }
337
+
338
+ export function decodeBootstrapSnapshot(
339
+ encoded: string
340
+ ): Promise<BootstrapSnapshot> {
341
+ const payload = JSON.parse(encoded) as BootstrapSnapshotPayload;
342
+ return deserializeBootstrapSnapshot(payload);
343
+ }
344
+
345
+ export function isBootstrapSnapshotStale(
346
+ snapshot: BootstrapSnapshot,
347
+ maxAge = 30_000
348
+ ): boolean {
349
+ return Date.now() - snapshot.fetchedAt > maxAge;
350
+ }
351
+
352
+ export interface SeedStorageOptions {
353
+ storage: StorageAdapter;
354
+ snapshot: BootstrapSnapshot | BootstrapSnapshotPayload | string;
355
+ dbName?: string;
356
+ clearExisting?: boolean;
357
+ validateSchemaHash?: boolean;
358
+ batchSize?: number;
359
+ closeAfter?: boolean;
360
+ schema?: SchemaDefinition | ModelRegistrySnapshot;
361
+ }
362
+
363
+ export interface SeedStorageResult {
364
+ applied: boolean;
365
+ rowCount: number;
366
+ reason?: "schema_mismatch";
367
+ }
368
+
369
+ export async function seedStorageFromBootstrap(
370
+ options: SeedStorageOptions
371
+ ): Promise<SeedStorageResult> {
372
+ const {
373
+ storage,
374
+ snapshot,
375
+ dbName = "sync-db",
376
+ clearExisting = true,
377
+ validateSchemaHash = true,
378
+ batchSize = 500,
379
+ closeAfter = true,
380
+ schema,
381
+ } = options;
382
+
383
+ const resolvedSnapshot = await resolveSnapshot(snapshot);
384
+ const localSchemaHash = computeSchemaHash(schema ?? ModelRegistry.snapshot());
385
+
386
+ if (
387
+ validateSchemaHash &&
388
+ resolvedSnapshot.schemaHash &&
389
+ resolvedSnapshot.schemaHash !== localSchemaHash
390
+ ) {
391
+ return { applied: false, rowCount: 0, reason: "schema_mismatch" };
392
+ }
393
+
394
+ let opened = false;
395
+
396
+ try {
397
+ await storage.open({ name: dbName, schema });
398
+ opened = true;
399
+
400
+ const existingMeta = await storage.getMeta();
401
+ const clientId =
402
+ existingMeta.clientId || getOrCreateClientId(`${dbName}_client_id`);
403
+
404
+ if (clearExisting) {
405
+ await storage.clear();
406
+ }
407
+
408
+ let ops: Array<{
409
+ type: "put";
410
+ modelName: string;
411
+ data: Record<string, unknown>;
412
+ }> = [];
413
+
414
+ for (const row of resolvedSnapshot.rows) {
415
+ ops.push({ type: "put", modelName: row.modelName, data: row.data });
416
+
417
+ if (ops.length >= batchSize) {
418
+ await storage.writeBatch(ops);
419
+ ops = [];
420
+ }
421
+ }
422
+
423
+ if (ops.length > 0) {
424
+ await storage.writeBatch(ops);
425
+ }
426
+
427
+ await storage.setMeta({
428
+ schemaHash: localSchemaHash,
429
+ lastSyncId: resolvedSnapshot.lastSyncId,
430
+ firstSyncId: resolvedSnapshot.firstSyncId,
431
+ subscribedSyncGroups: resolvedSnapshot.groups,
432
+ bootstrapComplete: true,
433
+ lastSyncAt: resolvedSnapshot.fetchedAt,
434
+ clientId,
435
+ });
436
+
437
+ return { applied: true, rowCount: resolvedSnapshot.rows.length };
438
+ } finally {
439
+ if (closeAfter && opened) {
440
+ await storage.close();
441
+ }
442
+ }
443
+ }
444
+
445
+ function isPayload(
446
+ value: BootstrapSnapshot | BootstrapSnapshotPayload | string
447
+ ): value is BootstrapSnapshotPayload {
448
+ return typeof value === "object" && value !== null && "encoding" in value;
449
+ }
450
+
451
+ function resolveSnapshot(
452
+ snapshot: BootstrapSnapshot | BootstrapSnapshotPayload | string
453
+ ): Promise<BootstrapSnapshot> {
454
+ if (typeof snapshot === "string") {
455
+ const parsed = JSON.parse(snapshot) as BootstrapSnapshotPayload;
456
+ return deserializeBootstrapSnapshot(parsed);
457
+ }
458
+
459
+ if (isPayload(snapshot)) {
460
+ return deserializeBootstrapSnapshot(snapshot);
461
+ }
462
+
463
+ return Promise.resolve(snapshot);
464
+ }
465
+
466
+ function canUseCompressionStreams(): boolean {
467
+ return (
468
+ typeof CompressionStream !== "undefined" &&
469
+ typeof DecompressionStream !== "undefined"
470
+ );
471
+ }
472
+
473
+ async function compressToBase64(input: string): Promise<string> {
474
+ const compressedStream = new Blob([input])
475
+ .stream()
476
+ .pipeThrough(new CompressionStream("gzip"));
477
+ const buffer = await new Response(compressedStream).arrayBuffer();
478
+ return toBase64(new Uint8Array(buffer));
479
+ }
480
+
481
+ function decompressFromBase64(encoded: string): Promise<string> {
482
+ const bytes = fromBase64(encoded);
483
+ const decompressedStream = new Blob([bytes as BlobPart])
484
+ .stream()
485
+ .pipeThrough(new DecompressionStream("gzip"));
486
+ return new Response(decompressedStream).text();
487
+ }
488
+
489
+ function toBase64(bytes: Uint8Array): string {
490
+ if (typeof Buffer !== "undefined") {
491
+ return Buffer.from(bytes).toString("base64");
492
+ }
493
+
494
+ let binary = "";
495
+ for (const byte of bytes) {
496
+ binary += String.fromCharCode(byte);
497
+ }
498
+ return btoa(binary);
499
+ }
500
+
501
+ function fromBase64(encoded: string): Uint8Array {
502
+ if (typeof Buffer !== "undefined") {
503
+ return new Uint8Array(Buffer.from(encoded, "base64"));
504
+ }
505
+
506
+ const binary = atob(encoded);
507
+ const bytes = new Uint8Array(binary.length);
508
+ for (let i = 0; i < binary.length; i += 1) {
509
+ bytes[i] = binary.charCodeAt(i);
510
+ }
511
+ return bytes;
512
+ }
513
+
514
+ type ParsedBootstrapLine =
515
+ | { type: "meta"; metadata: BootstrapMetadata }
516
+ | { type: "row"; row: ModelRow }
517
+ | { type: "end"; rowCount?: number };
518
+
519
+ function parseBootstrapLine(line: string): ParsedBootstrapLine | null {
520
+ const trimmed = line.trim();
521
+ if (!trimmed) {
522
+ return null;
523
+ }
524
+
525
+ if (trimmed.startsWith("_metadata_=")) {
526
+ const raw = JSON.parse(trimmed.slice("_metadata_=".length)) as Record<
527
+ string,
528
+ unknown
529
+ >;
530
+ return { type: "meta", metadata: normalizeBootstrapMetadata(raw) };
531
+ }
532
+
533
+ const parsed = JSON.parse(trimmed) as Record<string, unknown>;
534
+
535
+ if (typeof parsed._metadata_ === "object" && parsed._metadata_ !== null) {
536
+ return {
537
+ type: "meta",
538
+ metadata: normalizeBootstrapMetadata(
539
+ parsed._metadata_ as Record<string, unknown>
540
+ ),
541
+ };
542
+ }
543
+
544
+ if (isBootstrapMetadata(parsed)) {
545
+ return { type: "meta", metadata: normalizeBootstrapMetadata(parsed) };
546
+ }
547
+
548
+ if (parsed.type === "end") {
549
+ return {
550
+ type: "end",
551
+ rowCount:
552
+ typeof parsed.rowCount === "number" ? parsed.rowCount : undefined,
553
+ };
554
+ }
555
+
556
+ if (typeof parsed.__class !== "string") {
557
+ throw new Error("Bootstrap row is missing __class");
558
+ }
559
+
560
+ const { __class: modelName, ...data } = parsed;
561
+ return {
562
+ type: "row",
563
+ row: { modelName, data },
564
+ };
565
+ }
566
+
567
+ function isBootstrapMetadata(parsed: Record<string, unknown>): boolean {
568
+ return (
569
+ "lastSyncId" in parsed ||
570
+ "subscribedSyncGroups" in parsed ||
571
+ "returnedModelsCount" in parsed
572
+ );
573
+ }
574
+
575
+ function normalizeBootstrapMetadata(
576
+ parsed: Record<string, unknown>
577
+ ): BootstrapMetadata {
578
+ const lastSyncIdRaw = parsed.lastSyncId;
579
+ const subscribedSyncGroupsRaw = parsed.subscribedSyncGroups;
580
+
581
+ const subscribedSyncGroups = Array.isArray(subscribedSyncGroupsRaw)
582
+ ? subscribedSyncGroupsRaw.filter(
583
+ (group): group is string => typeof group === "string"
584
+ )
585
+ : [];
586
+
587
+ const result: BootstrapMetadata = {
588
+ subscribedSyncGroups,
589
+ returnedModelsCount:
590
+ parsed.returnedModelsCount &&
591
+ typeof parsed.returnedModelsCount === "object"
592
+ ? (parsed.returnedModelsCount as Record<string, number>)
593
+ : undefined,
594
+ schemaHash:
595
+ typeof parsed.schemaHash === "string" ? parsed.schemaHash : undefined,
596
+ databaseVersion:
597
+ typeof parsed.databaseVersion === "number"
598
+ ? parsed.databaseVersion
599
+ : undefined,
600
+ raw: parsed,
601
+ };
602
+
603
+ if (typeof lastSyncIdRaw === "string" || typeof lastSyncIdRaw === "number") {
604
+ result.lastSyncId = Number(lastSyncIdRaw);
605
+ }
606
+
607
+ return result;
608
+ }
609
+
610
+ function normalizeSyncEndpoint(endpoint: string): string {
611
+ const trimmed = endpoint.replace(TRAILING_SLASH_RE, "");
612
+ for (const suffix of KNOWN_SYNC_SUFFIXES) {
613
+ if (trimmed.endsWith(suffix)) {
614
+ return trimmed.slice(0, -suffix.length);
615
+ }
616
+ }
617
+ return trimmed;
618
+ }
619
+
620
+ function joinSyncUrl(base: string, path: string): string {
621
+ const normalizedBase = base.replace(TRAILING_SLASH_RE, "");
622
+ const normalizedPath = path.startsWith("/") ? path : `/${path}`;
623
+ return `${normalizedBase}${normalizedPath}`;
624
+ }
package/src/client.ts ADDED
@@ -0,0 +1,38 @@
1
+ "use client";
2
+
3
+ // biome-ignore lint/performance/noBarrelFile: This is the package's public client-side API entry point
4
+ export {
5
+ useConnectionState,
6
+ useIsOffline,
7
+ useModel,
8
+ useModelSuspense,
9
+ usePendingCount,
10
+ useQuery,
11
+ useQueryAll,
12
+ useQueryCount,
13
+ useSync,
14
+ useSyncClient,
15
+ useSyncClientInstance,
16
+ useSyncReady,
17
+ useSyncState,
18
+ } from "@strata-sync/react";
19
+ export type {
20
+ BootstrapSnapshot,
21
+ BootstrapSnapshotPayload,
22
+ SeedStorageOptions,
23
+ SeedStorageResult,
24
+ } from "./bootstrap";
25
+ export {
26
+ decodeBootstrapSnapshot,
27
+ deserializeBootstrapSnapshot,
28
+ isBootstrapSnapshotStale,
29
+ seedStorageFromBootstrap,
30
+ } from "./bootstrap";
31
+ export type {
32
+ PrefetchedData,
33
+ PrefetchOptions,
34
+ PrefetchResult,
35
+ } from "./prefetch";
36
+ export { deserializePrefetchResult, isPrefetchStale } from "./prefetch";
37
+ export type { NextSyncProviderProps } from "./provider";
38
+ export { NextSyncProvider, withSyncProvider } from "./provider";
package/src/index.ts ADDED
@@ -0,0 +1,45 @@
1
+ // biome-ignore-all lint/performance/noBarrelFile: This is the package's main entry point
2
+ export type {
3
+ BootstrapSnapshot,
4
+ BootstrapSnapshotPayload,
5
+ NextSyncProviderProps,
6
+ PrefetchedData,
7
+ PrefetchOptions,
8
+ PrefetchResult,
9
+ SeedStorageOptions,
10
+ SeedStorageResult,
11
+ } from "./client";
12
+ export {
13
+ decodeBootstrapSnapshot,
14
+ deserializeBootstrapSnapshot,
15
+ deserializePrefetchResult,
16
+ isBootstrapSnapshotStale,
17
+ isPrefetchStale,
18
+ NextSyncProvider,
19
+ seedStorageFromBootstrap,
20
+ useConnectionState,
21
+ useIsOffline,
22
+ useModel,
23
+ useModelSuspense,
24
+ usePendingCount,
25
+ useQuery,
26
+ useQueryAll,
27
+ useQueryCount,
28
+ useSync,
29
+ useSyncClient,
30
+ useSyncClientInstance,
31
+ useSyncReady,
32
+ useSyncState,
33
+ withSyncProvider,
34
+ } from "./client";
35
+ export type {
36
+ PrefetchBootstrapOptions,
37
+ SerializeBootstrapOptions,
38
+ } from "./server";
39
+ export {
40
+ encodeBootstrapSnapshot,
41
+ prefetchBootstrap,
42
+ prefetchModels,
43
+ serializeBootstrapSnapshot,
44
+ serializePrefetchResult,
45
+ } from "./server";