@trestleinc/replicate 0.1.0 → 1.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 (94) hide show
  1. package/README.md +356 -420
  2. package/dist/client/collection.d.ts +78 -76
  3. package/dist/client/errors.d.ts +59 -0
  4. package/dist/client/index.d.ts +22 -18
  5. package/dist/client/logger.d.ts +0 -1
  6. package/dist/client/merge.d.ts +77 -0
  7. package/dist/client/persistence/adapters/index.d.ts +8 -0
  8. package/dist/client/persistence/adapters/opsqlite.d.ts +46 -0
  9. package/dist/client/persistence/adapters/sqljs.d.ts +83 -0
  10. package/dist/client/persistence/index.d.ts +49 -0
  11. package/dist/client/persistence/indexeddb.d.ts +17 -0
  12. package/dist/client/persistence/memory.d.ts +16 -0
  13. package/dist/client/persistence/sqlite-browser.d.ts +51 -0
  14. package/dist/client/persistence/sqlite-level.d.ts +63 -0
  15. package/dist/client/persistence/sqlite-rn.d.ts +36 -0
  16. package/dist/client/persistence/sqlite.d.ts +47 -0
  17. package/dist/client/persistence/types.d.ts +42 -0
  18. package/dist/client/prose.d.ts +56 -0
  19. package/dist/client/replicate.d.ts +40 -0
  20. package/dist/client/services/checkpoint.d.ts +18 -0
  21. package/dist/client/services/reconciliation.d.ts +24 -0
  22. package/dist/component/_generated/api.d.ts +35 -0
  23. package/dist/component/_generated/api.js +3 -3
  24. package/dist/component/_generated/component.d.ts +89 -0
  25. package/dist/component/_generated/component.js +0 -0
  26. package/dist/component/_generated/dataModel.d.ts +45 -0
  27. package/dist/component/_generated/dataModel.js +0 -0
  28. package/{src → dist}/component/_generated/server.d.ts +9 -38
  29. package/dist/component/convex.config.d.ts +2 -2
  30. package/dist/component/convex.config.js +2 -1
  31. package/dist/component/logger.d.ts +8 -0
  32. package/dist/component/logger.js +30 -0
  33. package/dist/component/public.d.ts +36 -61
  34. package/dist/component/public.js +232 -58
  35. package/dist/component/schema.d.ts +32 -8
  36. package/dist/component/schema.js +19 -6
  37. package/dist/index.js +1553 -308
  38. package/dist/server/builder.d.ts +94 -0
  39. package/dist/server/index.d.ts +14 -17
  40. package/dist/server/schema.d.ts +17 -63
  41. package/dist/server/storage.d.ts +80 -0
  42. package/dist/server.js +268 -83
  43. package/dist/shared/index.d.ts +5 -0
  44. package/dist/shared/index.js +2 -0
  45. package/dist/shared/types.d.ts +50 -0
  46. package/dist/shared/types.js +6 -0
  47. package/dist/shared.js +6 -0
  48. package/package.json +59 -49
  49. package/src/client/collection.ts +877 -450
  50. package/src/client/errors.ts +45 -0
  51. package/src/client/index.ts +52 -26
  52. package/src/client/logger.ts +2 -28
  53. package/src/client/merge.ts +374 -0
  54. package/src/client/persistence/adapters/index.ts +8 -0
  55. package/src/client/persistence/adapters/opsqlite.ts +54 -0
  56. package/src/client/persistence/adapters/sqljs.ts +128 -0
  57. package/src/client/persistence/index.ts +54 -0
  58. package/src/client/persistence/indexeddb.ts +110 -0
  59. package/src/client/persistence/memory.ts +61 -0
  60. package/src/client/persistence/sqlite-browser.ts +107 -0
  61. package/src/client/persistence/sqlite-level.ts +407 -0
  62. package/src/client/persistence/sqlite-rn.ts +44 -0
  63. package/src/client/persistence/sqlite.ts +161 -0
  64. package/src/client/persistence/types.ts +49 -0
  65. package/src/client/prose.ts +369 -0
  66. package/src/client/replicate.ts +80 -0
  67. package/src/client/services/checkpoint.ts +86 -0
  68. package/src/client/services/reconciliation.ts +108 -0
  69. package/src/component/_generated/api.ts +52 -0
  70. package/src/component/_generated/component.ts +103 -0
  71. package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +1 -1
  72. package/src/component/_generated/server.ts +161 -0
  73. package/src/component/convex.config.ts +3 -1
  74. package/src/component/logger.ts +36 -0
  75. package/src/component/public.ts +364 -111
  76. package/src/component/schema.ts +18 -5
  77. package/src/env.d.ts +31 -0
  78. package/src/server/builder.ts +85 -0
  79. package/src/server/index.ts +9 -24
  80. package/src/server/schema.ts +20 -76
  81. package/src/server/storage.ts +313 -0
  82. package/src/shared/index.ts +5 -0
  83. package/src/shared/types.ts +52 -0
  84. package/LICENSE.package +0 -201
  85. package/dist/client/storage.d.ts +0 -143
  86. package/dist/server/replication.d.ts +0 -122
  87. package/dist/server/ssr.d.ts +0 -79
  88. package/dist/ssr.js +0 -19
  89. package/src/client/storage.ts +0 -206
  90. package/src/component/_generated/api.d.ts +0 -95
  91. package/src/component/_generated/api.js +0 -23
  92. package/src/component/_generated/server.js +0 -90
  93. package/src/server/replication.ts +0 -244
  94. package/src/server/ssr.ts +0 -106
@@ -0,0 +1,128 @@
1
+ /**
2
+ * sql.js adapter wrapper for browser SQLite.
3
+ *
4
+ * The consuming app imports sql.js and creates the database,
5
+ * then passes it to this wrapper.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * import initSqlJs from 'sql.js';
10
+ * import { SqlJsAdapter } from '@trestleinc/replicate/client';
11
+ *
12
+ * const SQL = await initSqlJs({ locateFile: f => `/wasm/${f}` });
13
+ * const db = new SQL.Database();
14
+ * const adapter = new SqlJsAdapter(db, {
15
+ * onPersist: async (data) => {
16
+ * // Persist to OPFS, localStorage, etc.
17
+ * }
18
+ * });
19
+ * ```
20
+ */
21
+ import type { SqliteAdapter } from '../sqlite-level.js';
22
+
23
+ /**
24
+ * Interface for sql.js Database.
25
+ * Consumer must install sql.js and pass a Database instance.
26
+ */
27
+ export interface SqlJsDatabase {
28
+ run(sql: string, params?: unknown[]): void;
29
+ prepare(sql: string): {
30
+ bind(params?: unknown[]): void;
31
+ step(): boolean;
32
+ getAsObject(): Record<string, unknown>;
33
+ free(): void;
34
+ };
35
+ export(): Uint8Array;
36
+ close(): void;
37
+ }
38
+
39
+ /**
40
+ * Options for the SqlJsAdapter.
41
+ */
42
+ export interface SqlJsAdapterOptions {
43
+ /**
44
+ * Callback to persist database after write operations.
45
+ * Called with the exported database bytes.
46
+ *
47
+ * @example OPFS persistence
48
+ * ```typescript
49
+ * onPersist: async (data) => {
50
+ * const root = await navigator.storage.getDirectory();
51
+ * const handle = await root.getFileHandle('myapp.sqlite', { create: true });
52
+ * const writable = await handle.createWritable();
53
+ * await writable.write(data.buffer);
54
+ * await writable.close();
55
+ * }
56
+ * ```
57
+ */
58
+ onPersist?: (data: Uint8Array) => Promise<void>;
59
+ }
60
+
61
+ /**
62
+ * Wraps a sql.js Database as a SqliteAdapter.
63
+ *
64
+ * @example
65
+ * ```typescript
66
+ * import initSqlJs from 'sql.js';
67
+ * import { SqlJsAdapter } from '@trestleinc/replicate/client';
68
+ *
69
+ * const SQL = await initSqlJs();
70
+ * const db = new SQL.Database();
71
+ * const adapter = new SqlJsAdapter(db);
72
+ * ```
73
+ */
74
+ export class SqlJsAdapter implements SqliteAdapter {
75
+ private db: SqlJsDatabase;
76
+ private onPersist?: (data: Uint8Array) => Promise<void>;
77
+
78
+ constructor(db: SqlJsDatabase, options: SqlJsAdapterOptions = {}) {
79
+ this.db = db;
80
+ this.onPersist = options.onPersist;
81
+ }
82
+
83
+ async execute(sql: string, params?: unknown[]): Promise<{ rows: Record<string, unknown>[] }> {
84
+ const rows: Record<string, unknown>[] = [];
85
+
86
+ // Handle statements that don't return data
87
+ if (
88
+ sql.trim().toUpperCase().startsWith('CREATE') ||
89
+ sql.trim().toUpperCase().startsWith('INSERT') ||
90
+ sql.trim().toUpperCase().startsWith('UPDATE') ||
91
+ sql.trim().toUpperCase().startsWith('DELETE') ||
92
+ sql.trim().toUpperCase().startsWith('BEGIN') ||
93
+ sql.trim().toUpperCase().startsWith('COMMIT') ||
94
+ sql.trim().toUpperCase().startsWith('ROLLBACK')
95
+ ) {
96
+ this.db.run(sql, params);
97
+ await this.persist();
98
+ return { rows };
99
+ }
100
+
101
+ // Handle SELECT statements
102
+ const stmt = this.db.prepare(sql);
103
+ if (params && params.length > 0) {
104
+ stmt.bind(params);
105
+ }
106
+
107
+ while (stmt.step()) {
108
+ rows.push(stmt.getAsObject());
109
+ }
110
+ stmt.free();
111
+
112
+ return { rows };
113
+ }
114
+
115
+ close(): void {
116
+ this.db.close();
117
+ }
118
+
119
+ /**
120
+ * Persist database using the onPersist callback if provided.
121
+ */
122
+ private async persist(): Promise<void> {
123
+ if (this.onPersist) {
124
+ const data = this.db.export();
125
+ await this.onPersist(new Uint8Array(data));
126
+ }
127
+ }
128
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Persistence layer exports.
3
+ *
4
+ * Provides swappable storage backends for Y.Doc and key-value data.
5
+ */
6
+ export type { Persistence, PersistenceProvider, KeyValueStore } from './types.js';
7
+ export type { SqlitePersistenceOptions } from './sqlite.js';
8
+ export type { SqlJsStatic } from './sqlite-browser.js';
9
+ export type { SqliteAdapter } from './sqlite-level.js';
10
+
11
+ // Internal imports for the persistence object
12
+ import { indexeddbPersistence } from './indexeddb.js';
13
+ import { memoryPersistence } from './memory.js';
14
+ import { sqlitePersistence } from './sqlite.js';
15
+ import { createBrowserSqlitePersistence } from './sqlite-browser.js';
16
+ import { createReactNativeSqlitePersistence } from './sqlite-rn.js';
17
+
18
+ /**
19
+ * Persistence API - nested object pattern for ergonomic access.
20
+ *
21
+ * @example
22
+ * ```typescript
23
+ * import { persistence } from '@trestleinc/replicate/client';
24
+ *
25
+ * // Browser SQLite (recommended for web)
26
+ * const p = await persistence.sqlite.browser(SQL, 'myapp');
27
+ *
28
+ * // React Native SQLite
29
+ * const p = await persistence.sqlite.native(db, 'myapp');
30
+ *
31
+ * // IndexedDB fallback
32
+ * const p = persistence.indexeddb('myapp');
33
+ *
34
+ * // In-memory (testing)
35
+ * const p = persistence.memory();
36
+ * ```
37
+ */
38
+ export const persistence = {
39
+ /** IndexedDB-backed persistence (browser) */
40
+ indexeddb: indexeddbPersistence,
41
+
42
+ /** In-memory persistence (testing/ephemeral) */
43
+ memory: memoryPersistence,
44
+
45
+ /** SQLite persistence variants */
46
+ sqlite: {
47
+ /** Browser SQLite with OPFS (sql.js) */
48
+ browser: createBrowserSqlitePersistence,
49
+ /** React Native SQLite (op-sqlite) */
50
+ native: createReactNativeSqlitePersistence,
51
+ /** Custom SQLite adapter */
52
+ create: sqlitePersistence,
53
+ },
54
+ } as const;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * IndexedDB persistence implementation for browser environments.
3
+ *
4
+ * Uses y-indexeddb for Y.Doc persistence and browser-level for key-value storage.
5
+ * browser-level is an abstract-level database backed by IndexedDB.
6
+ */
7
+ import type * as Y from 'yjs';
8
+ import { IndexeddbPersistence } from 'y-indexeddb';
9
+ import { BrowserLevel } from 'browser-level';
10
+ import type { Persistence, PersistenceProvider, KeyValueStore } from './types.js';
11
+
12
+ /**
13
+ * browser-level backed key-value store.
14
+ *
15
+ * Uses the Level ecosystem for consistent API across browser and React Native.
16
+ */
17
+ class BrowserLevelKeyValueStore implements KeyValueStore {
18
+ private db: BrowserLevel<string, string>;
19
+
20
+ constructor(dbName: string) {
21
+ this.db = new BrowserLevel(dbName);
22
+ }
23
+
24
+ async get<T>(key: string): Promise<T | undefined> {
25
+ try {
26
+ const value = await this.db.get(key);
27
+ if (value === undefined) {
28
+ return undefined;
29
+ }
30
+ return JSON.parse(value) as T;
31
+ } catch (err: any) {
32
+ // Level throws LEVEL_NOT_FOUND error for missing keys
33
+ if (err.code === 'LEVEL_NOT_FOUND') {
34
+ return undefined;
35
+ }
36
+ throw err;
37
+ }
38
+ }
39
+
40
+ async set<T>(key: string, value: T): Promise<void> {
41
+ await this.db.put(key, JSON.stringify(value));
42
+ }
43
+
44
+ async del(key: string): Promise<void> {
45
+ try {
46
+ await this.db.del(key);
47
+ } catch (err: any) {
48
+ // Ignore not found errors on delete
49
+ if (err.code !== 'LEVEL_NOT_FOUND') {
50
+ throw err;
51
+ }
52
+ }
53
+ }
54
+
55
+ async close(): Promise<void> {
56
+ await this.db.close();
57
+ }
58
+ }
59
+
60
+ /**
61
+ * IndexedDB persistence provider wrapping y-indexeddb.
62
+ */
63
+ class IndexedDBPersistenceProvider implements PersistenceProvider {
64
+ private persistence: IndexeddbPersistence;
65
+ readonly whenSynced: Promise<void>;
66
+
67
+ constructor(collection: string, ydoc: Y.Doc) {
68
+ this.persistence = new IndexeddbPersistence(collection, ydoc);
69
+
70
+ // Handle race: check synced state before attaching listener
71
+ // If database is empty or fast, synced event may fire before we attach
72
+ this.whenSynced = new Promise((resolve) => {
73
+ if (this.persistence.synced) {
74
+ // Already synced - resolve immediately
75
+ resolve();
76
+ } else {
77
+ // Not yet synced - wait for event (use once to prevent listener accumulation)
78
+ this.persistence.once('synced', () => resolve());
79
+ }
80
+ });
81
+ }
82
+
83
+ destroy(): void {
84
+ this.persistence.destroy();
85
+ }
86
+ }
87
+
88
+ /**
89
+ * Create an IndexedDB persistence factory.
90
+ *
91
+ * Uses y-indexeddb for Y.Doc persistence and browser-level for metadata storage.
92
+ *
93
+ * @param dbName - Name for the LevelDB database (default: 'replicate-kv')
94
+ *
95
+ * @example
96
+ * ```typescript
97
+ * convexCollectionOptions<Task>({
98
+ * // ... other options
99
+ * persistence: indexeddbPersistence(),
100
+ * });
101
+ * ```
102
+ */
103
+ export function indexeddbPersistence(dbName = 'replicate-kv'): Persistence {
104
+ const kv = new BrowserLevelKeyValueStore(dbName);
105
+ return {
106
+ createDocPersistence: (collection: string, ydoc: Y.Doc) =>
107
+ new IndexedDBPersistenceProvider(collection, ydoc),
108
+ kv,
109
+ };
110
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * In-memory persistence implementation for testing.
3
+ *
4
+ * State is not persisted across sessions - useful for tests and development.
5
+ */
6
+ import type * as Y from 'yjs';
7
+ import type { Persistence, PersistenceProvider, KeyValueStore } from './types.js';
8
+
9
+ /**
10
+ * In-memory key-value store.
11
+ */
12
+ class MemoryKeyValueStore implements KeyValueStore {
13
+ private store = new Map<string, unknown>();
14
+
15
+ async get<T>(key: string): Promise<T | undefined> {
16
+ return this.store.get(key) as T | undefined;
17
+ }
18
+
19
+ async set<T>(key: string, value: T): Promise<void> {
20
+ this.store.set(key, value);
21
+ }
22
+
23
+ async del(key: string): Promise<void> {
24
+ this.store.delete(key);
25
+ }
26
+ }
27
+
28
+ /**
29
+ * No-op persistence provider for in-memory usage.
30
+ *
31
+ * The Y.Doc is kept in memory without persistence.
32
+ */
33
+ class MemoryPersistenceProvider implements PersistenceProvider {
34
+ readonly whenSynced = Promise.resolve();
35
+
36
+ destroy(): void {
37
+ // No resources to clean up
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Create an in-memory persistence factory.
43
+ *
44
+ * Useful for testing where you don't want IndexedDB side effects.
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * // In tests
49
+ * convexCollectionOptions<Task>({
50
+ * // ... other options
51
+ * persistence: memoryPersistence(),
52
+ * });
53
+ * ```
54
+ */
55
+ export function memoryPersistence(): Persistence {
56
+ const kv = new MemoryKeyValueStore();
57
+ return {
58
+ createDocPersistence: (_collection: string, _ydoc: Y.Doc) => new MemoryPersistenceProvider(),
59
+ kv,
60
+ };
61
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Browser SQLite persistence helper using sql.js and OPFS.
3
+ *
4
+ * Handles all the boilerplate for browser SQLite:
5
+ * - Loading existing database from OPFS
6
+ * - Persisting to OPFS on every write
7
+ * - Creating the SqlJsAdapter
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { createBrowserSqlitePersistence } from '@trestleinc/replicate/client';
12
+ * import initSqlJs from 'sql.js';
13
+ *
14
+ * const SQL = await initSqlJs({ locateFile: f => `https://sql.js.org/dist/${f}` });
15
+ * const persistence = await createBrowserSqlitePersistence(SQL, 'myapp');
16
+ * ```
17
+ */
18
+ import { SqlJsAdapter, type SqlJsDatabase } from './adapters/sqljs.js';
19
+ import { sqlitePersistence } from './sqlite.js';
20
+ import type { Persistence } from './types.js';
21
+
22
+ /**
23
+ * Interface for the sql.js module (the result of initSqlJs).
24
+ */
25
+ export interface SqlJsStatic {
26
+ Database: new (data?: ArrayLike<number>) => SqlJsDatabase;
27
+ }
28
+
29
+ /**
30
+ * Load existing database from OPFS if available.
31
+ */
32
+ async function loadFromOPFS(dbName: string): Promise<Uint8Array | null> {
33
+ try {
34
+ const root = await navigator.storage.getDirectory();
35
+ const handle = await root.getFileHandle(`${dbName}.sqlite`);
36
+ const file = await handle.getFile();
37
+ const buffer = await file.arrayBuffer();
38
+ return new Uint8Array(buffer);
39
+ } catch {
40
+ // File doesn't exist yet
41
+ return null;
42
+ }
43
+ }
44
+
45
+ /**
46
+ * Save database to OPFS for durable storage.
47
+ */
48
+ function createOPFSSaver(dbName: string): (data: Uint8Array) => Promise<void> {
49
+ return async (data: Uint8Array): Promise<void> => {
50
+ try {
51
+ const root = await navigator.storage.getDirectory();
52
+ const handle = await root.getFileHandle(`${dbName}.sqlite`, { create: true });
53
+ const writable = await handle.createWritable();
54
+ // Copy to a new ArrayBuffer to satisfy TypeScript's strict ArrayBuffer type
55
+ const buffer = new ArrayBuffer(data.length);
56
+ new Uint8Array(buffer).set(data);
57
+ await writable.write(buffer);
58
+ await writable.close();
59
+ } catch {
60
+ // Silently fail - OPFS may not be available
61
+ }
62
+ };
63
+ }
64
+
65
+ /**
66
+ * Create browser SQLite persistence with OPFS storage.
67
+ *
68
+ * This helper handles all the OPFS boilerplate:
69
+ * - Loads existing database from OPFS on init
70
+ * - Persists to OPFS after every write operation
71
+ *
72
+ * @param SQL - The initialized sql.js module (from `await initSqlJs()`)
73
+ * @param dbName - Name for the database (used for OPFS filename: `{dbName}.sqlite`)
74
+ *
75
+ * @example
76
+ * ```typescript
77
+ * import { createBrowserSqlitePersistence } from '@trestleinc/replicate/client';
78
+ * import initSqlJs from 'sql.js';
79
+ *
80
+ * const SQL = await initSqlJs({ locateFile: f => `https://sql.js.org/dist/${f}` });
81
+ * const persistence = await createBrowserSqlitePersistence(SQL, 'intervals');
82
+ *
83
+ * // Use in collection options
84
+ * convexCollectionOptions<Task>({
85
+ * // ...
86
+ * persistence,
87
+ * });
88
+ * ```
89
+ */
90
+ export async function createBrowserSqlitePersistence(
91
+ SQL: SqlJsStatic,
92
+ dbName: string
93
+ ): Promise<Persistence> {
94
+ // Load existing database from OPFS if available
95
+ const existingData = await loadFromOPFS(dbName);
96
+
97
+ // Create database (with existing data if found)
98
+ const db = existingData ? new SQL.Database(existingData) : new SQL.Database();
99
+
100
+ // Create adapter with OPFS persistence
101
+ const adapter = new SqlJsAdapter(db, {
102
+ onPersist: createOPFSSaver(dbName),
103
+ });
104
+
105
+ // Create and return persistence
106
+ return sqlitePersistence({ adapter, dbName });
107
+ }