@trestleinc/replicate 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.
Files changed (39) hide show
  1. package/LICENSE +201 -0
  2. package/LICENSE.package +201 -0
  3. package/README.md +871 -0
  4. package/dist/client/collection.d.ts +94 -0
  5. package/dist/client/index.d.ts +18 -0
  6. package/dist/client/logger.d.ts +3 -0
  7. package/dist/client/storage.d.ts +143 -0
  8. package/dist/component/_generated/api.js +5 -0
  9. package/dist/component/_generated/server.js +9 -0
  10. package/dist/component/convex.config.d.ts +2 -0
  11. package/dist/component/convex.config.js +3 -0
  12. package/dist/component/public.d.ts +99 -0
  13. package/dist/component/public.js +135 -0
  14. package/dist/component/schema.d.ts +22 -0
  15. package/dist/component/schema.js +22 -0
  16. package/dist/index.js +375 -0
  17. package/dist/server/index.d.ts +17 -0
  18. package/dist/server/replication.d.ts +122 -0
  19. package/dist/server/schema.d.ts +73 -0
  20. package/dist/server/ssr.d.ts +79 -0
  21. package/dist/server.js +96 -0
  22. package/dist/ssr.js +19 -0
  23. package/package.json +108 -0
  24. package/src/client/collection.ts +550 -0
  25. package/src/client/index.ts +31 -0
  26. package/src/client/logger.ts +31 -0
  27. package/src/client/storage.ts +206 -0
  28. package/src/component/_generated/api.d.ts +95 -0
  29. package/src/component/_generated/api.js +23 -0
  30. package/src/component/_generated/dataModel.d.ts +60 -0
  31. package/src/component/_generated/server.d.ts +149 -0
  32. package/src/component/_generated/server.js +90 -0
  33. package/src/component/convex.config.ts +3 -0
  34. package/src/component/public.ts +212 -0
  35. package/src/component/schema.ts +16 -0
  36. package/src/server/index.ts +26 -0
  37. package/src/server/replication.ts +244 -0
  38. package/src/server/schema.ts +97 -0
  39. package/src/server/ssr.ts +106 -0
package/dist/index.js ADDED
@@ -0,0 +1,375 @@
1
+ import { componentsGeneric } from "convex/server";
2
+ import { NonRetriableError, startOfflineExecutor } from "@tanstack/offline-transactions";
3
+ import { getLogger } from "@logtape/logtape";
4
+ import * as __WEBPACK_EXTERNAL_MODULE_yjs__ from "yjs";
5
+ componentsGeneric();
6
+ class ReplicateStorage {
7
+ component;
8
+ collectionName;
9
+ constructor(component, collectionName){
10
+ this.component = component;
11
+ this.collectionName = collectionName;
12
+ }
13
+ async insertDocument(ctx, documentId, crdtBytes, version) {
14
+ return ctx.runMutation(this.component.public.insertDocument, {
15
+ collectionName: this.collectionName,
16
+ documentId,
17
+ crdtBytes,
18
+ version
19
+ });
20
+ }
21
+ async updateDocument(ctx, documentId, crdtBytes, version) {
22
+ return ctx.runMutation(this.component.public.updateDocument, {
23
+ collectionName: this.collectionName,
24
+ documentId,
25
+ crdtBytes,
26
+ version
27
+ });
28
+ }
29
+ async deleteDocument(ctx, documentId, crdtBytes, version) {
30
+ return ctx.runMutation(this.component.public.deleteDocument, {
31
+ collectionName: this.collectionName,
32
+ documentId,
33
+ crdtBytes,
34
+ version
35
+ });
36
+ }
37
+ async stream(ctx, checkpoint, limit) {
38
+ return ctx.runQuery(this.component.public.stream, {
39
+ collectionName: this.collectionName,
40
+ checkpoint,
41
+ limit
42
+ });
43
+ }
44
+ }
45
+ function logger_getLogger(category) {
46
+ return getLogger([
47
+ 'convex-replicate',
48
+ ...category
49
+ ]);
50
+ }
51
+ const logger = logger_getLogger([
52
+ 'convex-replicate',
53
+ 'collection'
54
+ ]);
55
+ function convexCollectionOptions({ getKey, initialData, convexClient, api, collectionName }) {
56
+ const ydoc = new __WEBPACK_EXTERNAL_MODULE_yjs__.Doc({
57
+ guid: collectionName
58
+ });
59
+ const ymap = ydoc.getMap(collectionName);
60
+ let pendingUpdate = null;
61
+ ydoc.on('update', (update, origin)=>{
62
+ pendingUpdate = update;
63
+ logger.debug('Yjs update event fired', {
64
+ collectionName,
65
+ updateSize: update.length,
66
+ origin
67
+ });
68
+ });
69
+ return {
70
+ id: collectionName,
71
+ getKey,
72
+ _convexClient: convexClient,
73
+ _collectionName: collectionName,
74
+ onInsert: async ({ transaction })=>{
75
+ logger.debug('onInsert handler called', {
76
+ collectionName,
77
+ mutationCount: transaction.mutations.length
78
+ });
79
+ try {
80
+ ydoc.transact(()=>{
81
+ transaction.mutations.forEach((mut)=>{
82
+ const itemYMap = new __WEBPACK_EXTERNAL_MODULE_yjs__.Map();
83
+ Object.entries(mut.modified).forEach(([k, v])=>{
84
+ itemYMap.set(k, v);
85
+ });
86
+ ymap.set(String(mut.key), itemYMap);
87
+ });
88
+ }, 'insert');
89
+ if (pendingUpdate) {
90
+ logger.debug('Sending insert delta to Convex', {
91
+ collectionName,
92
+ documentId: String(transaction.mutations[0].key),
93
+ deltaSize: pendingUpdate.length
94
+ });
95
+ await convexClient.mutation(api.insertDocument, {
96
+ collectionName,
97
+ documentId: String(transaction.mutations[0].key),
98
+ crdtBytes: pendingUpdate.buffer,
99
+ materializedDoc: transaction.mutations[0].modified,
100
+ version: Date.now()
101
+ });
102
+ pendingUpdate = null;
103
+ logger.info('Insert persisted to Convex', {
104
+ collectionName,
105
+ documentId: String(transaction.mutations[0].key)
106
+ });
107
+ }
108
+ } catch (error) {
109
+ logger.error('Insert failed', {
110
+ collectionName,
111
+ error: error?.message,
112
+ status: error?.status
113
+ });
114
+ if (error?.status === 401 || error?.status === 403) throw new NonRetriableError('Authentication failed');
115
+ if (error?.status === 422) throw new NonRetriableError('Validation error');
116
+ throw error;
117
+ }
118
+ },
119
+ onUpdate: async ({ transaction })=>{
120
+ logger.debug('onUpdate handler called', {
121
+ collectionName,
122
+ mutationCount: transaction.mutations.length
123
+ });
124
+ try {
125
+ ydoc.transact(()=>{
126
+ transaction.mutations.forEach((mut)=>{
127
+ const itemYMap = ymap.get(String(mut.key));
128
+ if (itemYMap) Object.entries(mut.modified || {}).forEach(([k, v])=>{
129
+ itemYMap.set(k, v);
130
+ });
131
+ else {
132
+ const newYMap = new __WEBPACK_EXTERNAL_MODULE_yjs__.Map();
133
+ Object.entries(mut.modified).forEach(([k, v])=>{
134
+ newYMap.set(k, v);
135
+ });
136
+ ymap.set(String(mut.key), newYMap);
137
+ }
138
+ });
139
+ }, 'update');
140
+ if (pendingUpdate) {
141
+ logger.debug('Sending update delta to Convex', {
142
+ collectionName,
143
+ documentId: String(transaction.mutations[0].key),
144
+ deltaSize: pendingUpdate.length
145
+ });
146
+ await convexClient.mutation(api.updateDocument, {
147
+ collectionName,
148
+ documentId: String(transaction.mutations[0].key),
149
+ crdtBytes: pendingUpdate.buffer,
150
+ materializedDoc: transaction.mutations[0].modified,
151
+ version: Date.now()
152
+ });
153
+ pendingUpdate = null;
154
+ logger.info('Update persisted to Convex', {
155
+ collectionName,
156
+ documentId: String(transaction.mutations[0].key)
157
+ });
158
+ }
159
+ } catch (error) {
160
+ logger.error('Update failed', {
161
+ collectionName,
162
+ error: error?.message,
163
+ status: error?.status
164
+ });
165
+ if (error?.status === 401 || error?.status === 403) throw new NonRetriableError('Authentication failed');
166
+ if (error?.status === 422) throw new NonRetriableError('Validation error');
167
+ throw error;
168
+ }
169
+ },
170
+ onDelete: async ({ transaction })=>{
171
+ logger.debug('onDelete handler called', {
172
+ collectionName,
173
+ mutationCount: transaction.mutations.length
174
+ });
175
+ try {
176
+ ydoc.transact(()=>{
177
+ transaction.mutations.forEach((mut)=>{
178
+ ymap.delete(String(mut.key));
179
+ });
180
+ }, 'delete');
181
+ if (pendingUpdate) {
182
+ logger.debug('Sending delete delta to Convex', {
183
+ collectionName,
184
+ documentId: String(transaction.mutations[0].key),
185
+ deltaSize: pendingUpdate.length
186
+ });
187
+ await convexClient.mutation(api.deleteDocument, {
188
+ collectionName,
189
+ documentId: String(transaction.mutations[0].key),
190
+ crdtBytes: pendingUpdate.buffer,
191
+ version: Date.now()
192
+ });
193
+ pendingUpdate = null;
194
+ logger.info('Delete persisted to Convex', {
195
+ collectionName,
196
+ documentId: String(transaction.mutations[0].key)
197
+ });
198
+ }
199
+ } catch (error) {
200
+ logger.error('Delete failed', {
201
+ collectionName,
202
+ error: error?.message,
203
+ status: error?.status
204
+ });
205
+ if (error?.status === 401 || error?.status === 403) throw new NonRetriableError('Authentication failed');
206
+ if (error?.status === 422) throw new NonRetriableError('Validation error');
207
+ throw error;
208
+ }
209
+ },
210
+ sync: {
211
+ sync: (params)=>{
212
+ const { begin, write, commit, markReady } = params;
213
+ if (initialData && initialData.length > 0) {
214
+ ydoc.transact(()=>{
215
+ for (const item of initialData){
216
+ const key = getKey(item);
217
+ const itemYMap = new __WEBPACK_EXTERNAL_MODULE_yjs__.Map();
218
+ Object.entries(item).forEach(([k, v])=>{
219
+ itemYMap.set(k, v);
220
+ });
221
+ ymap.set(String(key), itemYMap);
222
+ }
223
+ }, 'ssr-init');
224
+ begin();
225
+ for (const item of initialData)write({
226
+ type: 'insert',
227
+ value: item
228
+ });
229
+ commit();
230
+ logger.debug('Initialized with SSR data', {
231
+ collectionName,
232
+ count: initialData.length
233
+ });
234
+ }
235
+ logger.debug("Setting up Convex subscription", {
236
+ collectionName
237
+ });
238
+ let previousItems = new Map();
239
+ const subscription = convexClient.onUpdate(api.stream, {}, async (items)=>{
240
+ try {
241
+ logger.debug("Subscription update received", {
242
+ collectionName,
243
+ itemCount: items.length
244
+ });
245
+ const currentItems = new Map();
246
+ for (const item of items){
247
+ const key = getKey(item);
248
+ currentItems.set(key, item);
249
+ }
250
+ const deletedItems = [];
251
+ for (const [prevId, prevItem] of previousItems)if (!currentItems.has(prevId)) deletedItems.push(prevItem);
252
+ if (deletedItems.length > 0) logger.info('Detected remote hard deletes', {
253
+ collectionName,
254
+ deletedCount: deletedItems.length,
255
+ deletedIds: deletedItems.map((item)=>getKey(item))
256
+ });
257
+ begin();
258
+ for (const deletedItem of deletedItems){
259
+ const deletedId = getKey(deletedItem);
260
+ ydoc.transact(()=>{
261
+ ymap.delete(String(deletedId));
262
+ }, 'remote-delete');
263
+ write({
264
+ type: 'delete',
265
+ value: deletedItem
266
+ });
267
+ }
268
+ ydoc.transact(()=>{
269
+ for (const item of items){
270
+ const key = getKey(item);
271
+ const itemYMap = new __WEBPACK_EXTERNAL_MODULE_yjs__.Map();
272
+ Object.entries(item).forEach(([k, v])=>{
273
+ itemYMap.set(k, v);
274
+ });
275
+ ymap.set(String(key), itemYMap);
276
+ }
277
+ }, "subscription-sync");
278
+ for (const item of items){
279
+ const key = getKey(item);
280
+ params.collection.has(key) ? write({
281
+ type: 'update',
282
+ value: item
283
+ }) : write({
284
+ type: 'insert',
285
+ value: item
286
+ });
287
+ }
288
+ commit();
289
+ previousItems = currentItems;
290
+ logger.debug('Successfully synced items to collection', {
291
+ count: items.length,
292
+ deletedCount: deletedItems.length
293
+ });
294
+ } catch (error) {
295
+ logger.error("Failed to sync items from subscription", {
296
+ error: error.message,
297
+ errorName: error.name,
298
+ stack: error?.stack,
299
+ collectionName,
300
+ itemCount: items.length
301
+ });
302
+ throw error;
303
+ }
304
+ });
305
+ markReady();
306
+ return ()=>{
307
+ logger.debug("Cleaning up Convex subscription", {
308
+ collectionName
309
+ });
310
+ subscription();
311
+ };
312
+ }
313
+ }
314
+ };
315
+ }
316
+ function createConvexCollection(rawCollection) {
317
+ const config = rawCollection.config;
318
+ const convexClient = config._convexClient;
319
+ const collectionName = config._collectionName;
320
+ if (!convexClient || !collectionName) throw new Error("createConvexCollection requires a collection created with convexCollectionOptions. Make sure you pass convexClient and collectionName to convexCollectionOptions.");
321
+ logger.info('Creating Convex collection with offline support', {
322
+ collectionName
323
+ });
324
+ const offline = startOfflineExecutor({
325
+ collections: {
326
+ [collectionName]: rawCollection
327
+ },
328
+ mutationFns: {},
329
+ beforeRetry: (transactions)=>{
330
+ const cutoff = Date.now() - 86400000;
331
+ const filtered = transactions.filter((tx)=>{
332
+ const isRecent = tx.createdAt.getTime() > cutoff;
333
+ const notExhausted = tx.retryCount < 10;
334
+ return isRecent && notExhausted;
335
+ });
336
+ if (filtered.length < transactions.length) logger.warn('Filtered stale transactions', {
337
+ collectionName,
338
+ before: transactions.length,
339
+ after: filtered.length
340
+ });
341
+ return filtered;
342
+ },
343
+ onLeadershipChange: (isLeader)=>{
344
+ logger.info(isLeader ? 'Offline mode active' : 'Online-only mode', {
345
+ collectionName
346
+ });
347
+ },
348
+ onStorageFailure: (diagnostic)=>{
349
+ logger.warn('Storage failed - online-only mode', {
350
+ collectionName,
351
+ code: diagnostic.code,
352
+ message: diagnostic.message
353
+ });
354
+ }
355
+ });
356
+ if (convexClient.connectionState) {
357
+ const connectionState = convexClient.connectionState();
358
+ logger.debug('Initial connection state', {
359
+ collectionName,
360
+ isConnected: connectionState.isWebSocketConnected
361
+ });
362
+ }
363
+ if ('undefined' != typeof window) window.addEventListener('online', ()=>{
364
+ logger.info('Network online - notifying offline executor', {
365
+ collectionName
366
+ });
367
+ offline.notifyOnline();
368
+ });
369
+ logger.info('Offline support initialized', {
370
+ collectionName,
371
+ mode: offline.mode
372
+ });
373
+ return rawCollection;
374
+ }
375
+ export { NonRetriableError, ReplicateStorage, __WEBPACK_EXTERNAL_MODULE_yjs__ as Y, convexCollectionOptions, createConvexCollection };
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Server-side utilities for Convex backend.
3
+ * Import this in your Convex functions (convex/*.ts files).
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * // convex/tasks.ts
8
+ * import {
9
+ * insertDocumentHelper,
10
+ * updateDocumentHelper,
11
+ * deleteDocumentHelper,
12
+ * streamHelper,
13
+ * } from '@trestleinc/replicate/server';
14
+ * ```
15
+ */
16
+ export { insertDocumentHelper, updateDocumentHelper, deleteDocumentHelper, streamHelper, } from './replication.js';
17
+ export { replicatedTable, type ReplicationFields } from './schema.js';
@@ -0,0 +1,122 @@
1
+ import type { GenericDataModel } from 'convex/server';
2
+ /**
3
+ * Insert a document into both the CRDT component and the main application table.
4
+ *
5
+ * DUAL-STORAGE ARCHITECTURE:
6
+ * This helper implements a dual-storage pattern where documents are stored in two places:
7
+ *
8
+ * 1. Component Storage (CRDT Layer):
9
+ * - Stores CRDT bytes (from Yjs) for offline-first conflict resolution
10
+ * - Handles concurrent updates with automatic merging
11
+ * - Provides the source of truth for offline changes
12
+ *
13
+ * 2. Main Application Table:
14
+ * - Stores materialized documents for efficient querying
15
+ * - Used by server-side Convex functions that need to query/join data
16
+ * - Optimized for reactive subscriptions and complex queries
17
+ *
18
+ * WHY BOTH?
19
+ * - Component: Handles conflict resolution and offline replication (CRDT bytes)
20
+ * - Main table: Enables efficient server-side queries (materialized docs)
21
+ * - Similar to event sourcing: component = event log, main table = read model
22
+ *
23
+ * @param ctx - Convex mutation context
24
+ * @param components - Generated components from Convex
25
+ * @param tableName - Name of the main application table
26
+ * @param args - Document data with id, crdtBytes, materializedDoc, and version
27
+ * @returns Success indicator
28
+ */
29
+ export declare function insertDocumentHelper<_DataModel extends GenericDataModel>(ctx: unknown, components: unknown, tableName: string, args: {
30
+ id: string;
31
+ crdtBytes: ArrayBuffer;
32
+ materializedDoc: unknown;
33
+ version: number;
34
+ }): Promise<{
35
+ success: boolean;
36
+ metadata: {
37
+ documentId: string;
38
+ timestamp: number;
39
+ version: number;
40
+ collectionName: string;
41
+ };
42
+ }>;
43
+ /**
44
+ * Update a document in both the CRDT component and the main application table.
45
+ *
46
+ * @param ctx - Convex mutation context
47
+ * @param components - Generated components from Convex
48
+ * @param tableName - Name of the main application table
49
+ * @param args - Document data with id, crdtBytes, materializedDoc, and version
50
+ * @returns Success indicator
51
+ */
52
+ export declare function updateDocumentHelper<_DataModel extends GenericDataModel>(ctx: unknown, components: unknown, tableName: string, args: {
53
+ id: string;
54
+ crdtBytes: ArrayBuffer;
55
+ materializedDoc: unknown;
56
+ version: number;
57
+ }): Promise<{
58
+ success: boolean;
59
+ metadata: {
60
+ documentId: string;
61
+ timestamp: number;
62
+ version: number;
63
+ collectionName: string;
64
+ };
65
+ }>;
66
+ /**
67
+ * HARD delete a document from main table, APPEND deletion delta to component.
68
+ *
69
+ * NEW BEHAVIOR (v0.3.0):
70
+ * - Appends deletion delta to component event log (preserves history)
71
+ * - Physically removes document from main table (hard delete)
72
+ * - CRDT history preserved for future recovery features
73
+ *
74
+ * @param ctx - Convex mutation context
75
+ * @param components - Generated components from Convex
76
+ * @param tableName - Name of the main application table
77
+ * @param args - Document data with id, crdtBytes (deletion delta), and version
78
+ * @returns Success indicator with metadata
79
+ */
80
+ export declare function deleteDocumentHelper<_DataModel extends GenericDataModel>(ctx: unknown, components: unknown, tableName: string, args: {
81
+ id: string;
82
+ crdtBytes: ArrayBuffer;
83
+ version: number;
84
+ }): Promise<{
85
+ success: boolean;
86
+ metadata: {
87
+ documentId: string;
88
+ timestamp: number;
89
+ version: number;
90
+ collectionName: string;
91
+ };
92
+ }>;
93
+ /**
94
+ * Stream document changes from the CRDT component storage.
95
+ *
96
+ * This reads CRDT bytes from the component (not the main table) to enable
97
+ * true Y.applyUpdate() conflict resolution on the client.
98
+ * Can be used for both polling (awaitReplication) and subscriptions (live updates).
99
+ *
100
+ * @param ctx - Convex query context
101
+ * @param components - Generated components from Convex
102
+ * @param tableName - Name of the collection
103
+ * @param args - Checkpoint and limit for pagination
104
+ * @returns Array of changes with CRDT bytes
105
+ */
106
+ export declare function streamHelper<_DataModel extends GenericDataModel>(ctx: unknown, components: unknown, tableName: string, args: {
107
+ checkpoint: {
108
+ lastModified: number;
109
+ };
110
+ limit?: number;
111
+ }): Promise<{
112
+ changes: Array<{
113
+ documentId: string;
114
+ crdtBytes: ArrayBuffer;
115
+ version: number;
116
+ timestamp: number;
117
+ }>;
118
+ checkpoint: {
119
+ lastModified: number;
120
+ };
121
+ hasMore: boolean;
122
+ }>;
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Schema utilities for defining replicated tables.
3
+ * Automatically adds replication metadata fields so users don't have to.
4
+ *
5
+ * @example
6
+ * ```typescript
7
+ * // convex/schema.ts
8
+ * import { defineSchema } from 'convex/server';
9
+ * import { v } from 'convex/values';
10
+ * import { replicatedTable } from '@trestleinc/replicate/server';
11
+ *
12
+ * export default defineSchema({
13
+ * tasks: replicatedTable(
14
+ * {
15
+ * id: v.string(),
16
+ * text: v.string(),
17
+ * isCompleted: v.boolean(),
18
+ * },
19
+ * (table) => table
20
+ * .index('by_id', ['id'])
21
+ * .index('by_timestamp', ['timestamp'])
22
+ * ),
23
+ * });
24
+ * ```
25
+ */
26
+ /**
27
+ * Internal replication metadata fields added to every replicated table.
28
+ * These are managed automatically by the replication layer.
29
+ */
30
+ export type ReplicationFields = {
31
+ /** Version number for conflict resolution */
32
+ version: number;
33
+ /** Last modification timestamp (Unix ms) */
34
+ timestamp: number;
35
+ };
36
+ /**
37
+ * Wraps a table definition to automatically add replication metadata fields.
38
+ *
39
+ * Users define their business logic fields, and we inject:
40
+ * - `version` - For conflict resolution and CRDT versioning
41
+ * - `timestamp` - For incremental sync and change tracking
42
+ *
43
+ * Enables:
44
+ * - Dual-storage architecture (CRDT component + main table)
45
+ * - Conflict-free replication across clients
46
+ * - Hard delete support with CRDT history preservation
47
+ * - Event sourcing via component storage
48
+ *
49
+ * @param userFields - User's business logic fields (id, text, etc.)
50
+ * @param applyIndexes - Optional callback to add indexes to the table
51
+ * @returns TableDefinition with replication fields injected
52
+ *
53
+ * @example
54
+ * ```typescript
55
+ * // Simple table with hard delete support
56
+ * tasks: replicatedTable({
57
+ * id: v.string(),
58
+ * text: v.string(),
59
+ * })
60
+ *
61
+ * // With indexes
62
+ * tasks: replicatedTable(
63
+ * {
64
+ * id: v.string(),
65
+ * text: v.string(),
66
+ * },
67
+ * (table) => table
68
+ * .index('by_id', ['id'])
69
+ * .index('by_timestamp', ['timestamp'])
70
+ * )
71
+ * ```
72
+ */
73
+ export declare function replicatedTable(userFields: Record<string, any>, applyIndexes?: (table: any) => any): any;
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Server-Side Rendering (SSR) Utilities
3
+ *
4
+ * This module provides utilities for loading collection data during
5
+ * server-side rendering. Use `loadCollection` with an explicit config
6
+ * object for clarity and type safety.
7
+ *
8
+ * @module ssr
9
+ * @example
10
+ * ```typescript
11
+ * import { loadCollection } from '@convex-replicate/core/ssr';
12
+ * import { api } from '../convex/_generated/api';
13
+ *
14
+ * const tasks = await loadCollection<Task>(httpClient, {
15
+ * api: api.tasks,
16
+ * collection: 'tasks',
17
+ * limit: 100,
18
+ * });
19
+ * ```
20
+ */
21
+ import type { ConvexHttpClient } from 'convex/browser';
22
+ import type { FunctionReference } from 'convex/server';
23
+ /**
24
+ * API module shape expected by loadCollection.
25
+ *
26
+ * This should match the generated API module for your collection
27
+ * (e.g., api.tasks, api.users, etc.)
28
+ */
29
+ export type CollectionAPI = {
30
+ stream: FunctionReference<'query', 'public' | 'internal'>;
31
+ };
32
+ /**
33
+ * Configuration for loading collection data during SSR.
34
+ */
35
+ export interface LoadCollectionConfig {
36
+ /** The API module for the collection (e.g., api.tasks) */
37
+ api: CollectionAPI;
38
+ /** The collection name (should match the API module name) */
39
+ collection: string;
40
+ /** Maximum number of items to load (default: 100) */
41
+ limit?: number;
42
+ }
43
+ /**
44
+ * Load collection data for server-side rendering.
45
+ *
46
+ * **IMPORTANT**: This function is currently limited because `stream` only returns
47
+ * CRDT bytes, not materialized documents. For most SSR use cases, it's recommended to
48
+ * create a separate query that reads from your main table instead.
49
+ *
50
+ * @deprecated Consider creating a dedicated SSR query instead. See example below.
51
+ *
52
+ * @param httpClient - Convex HTTP client for server-side queries
53
+ * @param config - Configuration object with api, collection, and options
54
+ * @returns Promise resolving to array of items from the collection
55
+ *
56
+ * @example
57
+ * **Recommended SSR Pattern:**
58
+ * ```typescript
59
+ * // convex/tasks.ts
60
+ * export const list = query({
61
+ * handler: async (ctx) => {
62
+ * return await ctx.db
63
+ * .query('tasks')
64
+ * .filter((q) => q.neq(q.field('deleted'), true))
65
+ * .collect();
66
+ * },
67
+ * });
68
+ *
69
+ * // In your route loader
70
+ * import { ConvexHttpClient } from 'convex/browser';
71
+ * import { api } from '../convex/_generated/api';
72
+ *
73
+ * const httpClient = new ConvexHttpClient(import.meta.env.VITE_CONVEX_URL);
74
+ * const tasks = await httpClient.query(api.tasks.list);
75
+ * ```
76
+ */
77
+ export declare function loadCollection<TItem extends {
78
+ id: string;
79
+ }>(httpClient: ConvexHttpClient, config: LoadCollectionConfig): Promise<ReadonlyArray<TItem>>;