@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.
- package/README.md +356 -420
- package/dist/client/collection.d.ts +78 -76
- package/dist/client/errors.d.ts +59 -0
- package/dist/client/index.d.ts +22 -18
- package/dist/client/logger.d.ts +0 -1
- package/dist/client/merge.d.ts +77 -0
- package/dist/client/persistence/adapters/index.d.ts +8 -0
- package/dist/client/persistence/adapters/opsqlite.d.ts +46 -0
- package/dist/client/persistence/adapters/sqljs.d.ts +83 -0
- package/dist/client/persistence/index.d.ts +49 -0
- package/dist/client/persistence/indexeddb.d.ts +17 -0
- package/dist/client/persistence/memory.d.ts +16 -0
- package/dist/client/persistence/sqlite-browser.d.ts +51 -0
- package/dist/client/persistence/sqlite-level.d.ts +63 -0
- package/dist/client/persistence/sqlite-rn.d.ts +36 -0
- package/dist/client/persistence/sqlite.d.ts +47 -0
- package/dist/client/persistence/types.d.ts +42 -0
- package/dist/client/prose.d.ts +56 -0
- package/dist/client/replicate.d.ts +40 -0
- package/dist/client/services/checkpoint.d.ts +18 -0
- package/dist/client/services/reconciliation.d.ts +24 -0
- package/dist/component/_generated/api.d.ts +35 -0
- package/dist/component/_generated/api.js +3 -3
- package/dist/component/_generated/component.d.ts +89 -0
- package/dist/component/_generated/component.js +0 -0
- package/dist/component/_generated/dataModel.d.ts +45 -0
- package/dist/component/_generated/dataModel.js +0 -0
- package/{src → dist}/component/_generated/server.d.ts +9 -38
- package/dist/component/convex.config.d.ts +2 -2
- package/dist/component/convex.config.js +2 -1
- package/dist/component/logger.d.ts +8 -0
- package/dist/component/logger.js +30 -0
- package/dist/component/public.d.ts +36 -61
- package/dist/component/public.js +232 -58
- package/dist/component/schema.d.ts +32 -8
- package/dist/component/schema.js +19 -6
- package/dist/index.js +1553 -308
- package/dist/server/builder.d.ts +94 -0
- package/dist/server/index.d.ts +14 -17
- package/dist/server/schema.d.ts +17 -63
- package/dist/server/storage.d.ts +80 -0
- package/dist/server.js +268 -83
- package/dist/shared/index.d.ts +5 -0
- package/dist/shared/index.js +2 -0
- package/dist/shared/types.d.ts +50 -0
- package/dist/shared/types.js +6 -0
- package/dist/shared.js +6 -0
- package/package.json +59 -49
- package/src/client/collection.ts +877 -450
- package/src/client/errors.ts +45 -0
- package/src/client/index.ts +52 -26
- package/src/client/logger.ts +2 -28
- package/src/client/merge.ts +374 -0
- package/src/client/persistence/adapters/index.ts +8 -0
- package/src/client/persistence/adapters/opsqlite.ts +54 -0
- package/src/client/persistence/adapters/sqljs.ts +128 -0
- package/src/client/persistence/index.ts +54 -0
- package/src/client/persistence/indexeddb.ts +110 -0
- package/src/client/persistence/memory.ts +61 -0
- package/src/client/persistence/sqlite-browser.ts +107 -0
- package/src/client/persistence/sqlite-level.ts +407 -0
- package/src/client/persistence/sqlite-rn.ts +44 -0
- package/src/client/persistence/sqlite.ts +161 -0
- package/src/client/persistence/types.ts +49 -0
- package/src/client/prose.ts +369 -0
- package/src/client/replicate.ts +80 -0
- package/src/client/services/checkpoint.ts +86 -0
- package/src/client/services/reconciliation.ts +108 -0
- package/src/component/_generated/api.ts +52 -0
- package/src/component/_generated/component.ts +103 -0
- package/src/component/_generated/{dataModel.d.ts → dataModel.ts} +1 -1
- package/src/component/_generated/server.ts +161 -0
- package/src/component/convex.config.ts +3 -1
- package/src/component/logger.ts +36 -0
- package/src/component/public.ts +364 -111
- package/src/component/schema.ts +18 -5
- package/src/env.d.ts +31 -0
- package/src/server/builder.ts +85 -0
- package/src/server/index.ts +9 -24
- package/src/server/schema.ts +20 -76
- package/src/server/storage.ts +313 -0
- package/src/shared/index.ts +5 -0
- package/src/shared/types.ts +52 -0
- package/LICENSE.package +0 -201
- package/dist/client/storage.d.ts +0 -143
- package/dist/server/replication.d.ts +0 -122
- package/dist/server/ssr.d.ts +0 -79
- package/dist/ssr.js +0 -19
- package/src/client/storage.ts +0 -206
- package/src/component/_generated/api.d.ts +0 -95
- package/src/component/_generated/api.js +0 -23
- package/src/component/_generated/server.js +0 -90
- package/src/server/replication.ts +0 -244
- package/src/server/ssr.ts +0 -106
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { Data } from 'effect';
|
|
2
|
+
|
|
3
|
+
export class NetworkError extends Data.TaggedError('NetworkError')<{
|
|
4
|
+
readonly cause: unknown;
|
|
5
|
+
readonly retryable: true;
|
|
6
|
+
readonly operation: string;
|
|
7
|
+
}> {}
|
|
8
|
+
|
|
9
|
+
export class IDBError extends Data.TaggedError('IDBError')<{
|
|
10
|
+
readonly operation: 'get' | 'set' | 'delete' | 'clear';
|
|
11
|
+
readonly store?: string;
|
|
12
|
+
readonly key?: string;
|
|
13
|
+
readonly cause: unknown;
|
|
14
|
+
}> {}
|
|
15
|
+
|
|
16
|
+
export class IDBWriteError extends Data.TaggedError('IDBWriteError')<{
|
|
17
|
+
readonly key: string;
|
|
18
|
+
readonly value: unknown;
|
|
19
|
+
readonly cause: unknown;
|
|
20
|
+
}> {}
|
|
21
|
+
|
|
22
|
+
export class ReconciliationError extends Data.TaggedError('ReconciliationError')<{
|
|
23
|
+
readonly collection: string;
|
|
24
|
+
readonly reason: string;
|
|
25
|
+
readonly cause?: unknown;
|
|
26
|
+
}> {}
|
|
27
|
+
|
|
28
|
+
export class ProseError extends Data.TaggedError('ProseError')<{
|
|
29
|
+
readonly documentId: string;
|
|
30
|
+
readonly field: string;
|
|
31
|
+
readonly collection: string;
|
|
32
|
+
}> {}
|
|
33
|
+
|
|
34
|
+
export class CollectionNotReadyError extends Data.TaggedError('CollectionNotReadyError')<{
|
|
35
|
+
readonly collection: string;
|
|
36
|
+
readonly reason: string;
|
|
37
|
+
}> {}
|
|
38
|
+
|
|
39
|
+
/** Error that should not be retried (auth failures, validation errors) */
|
|
40
|
+
export class NonRetriableError extends Error {
|
|
41
|
+
constructor(message: string) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = 'NonRetriableError';
|
|
44
|
+
}
|
|
45
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,31 +1,57 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Client-side utilities for browser/React code.
|
|
3
|
-
* Import this in your frontend components.
|
|
4
|
-
*
|
|
5
|
-
* @example
|
|
6
|
-
* ```typescript
|
|
7
|
-
* // src/useTasks.ts
|
|
8
|
-
* import {
|
|
9
|
-
* convexCollectionOptions,
|
|
10
|
-
* createConvexCollection,
|
|
11
|
-
* type ConvexCollection,
|
|
12
|
-
* } from '@trestleinc/replicate/client';
|
|
13
|
-
* ```
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// Component client (ReplicateStorage class)
|
|
17
|
-
export { ReplicateStorage } from './storage.js';
|
|
18
|
-
|
|
19
|
-
// TanStack DB collection integration
|
|
20
1
|
export {
|
|
21
2
|
convexCollectionOptions,
|
|
22
|
-
createConvexCollection,
|
|
23
3
|
type ConvexCollection,
|
|
24
|
-
type
|
|
25
|
-
} from '
|
|
4
|
+
type EditorBinding,
|
|
5
|
+
} from '$/client/collection.js';
|
|
26
6
|
|
|
27
|
-
|
|
28
|
-
|
|
7
|
+
import {
|
|
8
|
+
NetworkError,
|
|
9
|
+
IDBError,
|
|
10
|
+
IDBWriteError,
|
|
11
|
+
ReconciliationError,
|
|
12
|
+
ProseError,
|
|
13
|
+
CollectionNotReadyError,
|
|
14
|
+
NonRetriableError,
|
|
15
|
+
} from '$/client/errors.js';
|
|
29
16
|
|
|
30
|
-
|
|
31
|
-
|
|
17
|
+
export const errors = {
|
|
18
|
+
Network: NetworkError,
|
|
19
|
+
IDB: IDBError,
|
|
20
|
+
IDBWrite: IDBWriteError,
|
|
21
|
+
Reconciliation: ReconciliationError,
|
|
22
|
+
Prose: ProseError,
|
|
23
|
+
CollectionNotReady: CollectionNotReadyError,
|
|
24
|
+
NonRetriable: NonRetriableError,
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
import { extract } from '$/client/merge.js';
|
|
28
|
+
|
|
29
|
+
export const prose = {
|
|
30
|
+
extract,
|
|
31
|
+
} as const;
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
persistence,
|
|
35
|
+
type Persistence,
|
|
36
|
+
type PersistenceProvider,
|
|
37
|
+
type KeyValueStore,
|
|
38
|
+
type SqlitePersistenceOptions,
|
|
39
|
+
type SqliteAdapter,
|
|
40
|
+
type SqlJsStatic,
|
|
41
|
+
} from '$/client/persistence/index.js';
|
|
42
|
+
|
|
43
|
+
import {
|
|
44
|
+
SqlJsAdapter,
|
|
45
|
+
OPSqliteAdapter,
|
|
46
|
+
} from '$/client/persistence/adapters/index.js';
|
|
47
|
+
|
|
48
|
+
export const adapters = {
|
|
49
|
+
sqljs: SqlJsAdapter,
|
|
50
|
+
opsqlite: OPSqliteAdapter,
|
|
51
|
+
} as const;
|
|
52
|
+
|
|
53
|
+
export type {
|
|
54
|
+
SqlJsDatabase,
|
|
55
|
+
SqlJsAdapterOptions,
|
|
56
|
+
OPSQLiteDatabase,
|
|
57
|
+
} from '$/client/persistence/adapters/index.js';
|
package/src/client/logger.ts
CHANGED
|
@@ -1,31 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type Logger,
|
|
3
|
-
configure,
|
|
4
|
-
getConsoleSink,
|
|
5
|
-
getLogger as getLogTapeLogger,
|
|
6
|
-
} from '@logtape/logtape';
|
|
7
|
-
|
|
8
|
-
let isConfigured = false;
|
|
9
|
-
|
|
10
|
-
export async function configureLogger(enableLogging = false): Promise<void> {
|
|
11
|
-
if (isConfigured) return;
|
|
12
|
-
|
|
13
|
-
await configure({
|
|
14
|
-
sinks: {
|
|
15
|
-
console: getConsoleSink(),
|
|
16
|
-
},
|
|
17
|
-
loggers: [
|
|
18
|
-
{
|
|
19
|
-
category: ['convex-replicate'],
|
|
20
|
-
lowestLevel: enableLogging ? 'debug' : 'warning',
|
|
21
|
-
sinks: ['console'],
|
|
22
|
-
},
|
|
23
|
-
],
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
isConfigured = true;
|
|
27
|
-
}
|
|
1
|
+
import { type Logger, getLogger as getLogTapeLogger } from '@logtape/logtape';
|
|
28
2
|
|
|
29
3
|
export function getLogger(category: string[]): Logger {
|
|
30
|
-
return getLogTapeLogger(['
|
|
4
|
+
return getLogTapeLogger(['replicate', ...category]);
|
|
31
5
|
}
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Merge Helpers - Plain functions for Yjs CRDT operations
|
|
3
|
+
*
|
|
4
|
+
* Provides document creation, state encoding, and merge operations.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as Y from 'yjs';
|
|
8
|
+
import { getLogger } from '$/client/logger.js';
|
|
9
|
+
import type { KeyValueStore } from '$/client/persistence/types.js';
|
|
10
|
+
|
|
11
|
+
const logger = getLogger(['replicate', 'merge']);
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Create a Yjs document with a persistent clientId.
|
|
15
|
+
* The clientId ensures consistent identity across sessions for CRDT merging.
|
|
16
|
+
*
|
|
17
|
+
* @param collection - The collection name
|
|
18
|
+
* @param kv - Key-value store for persisting the clientId
|
|
19
|
+
*/
|
|
20
|
+
export async function createYjsDocument(collection: string, kv: KeyValueStore): Promise<Y.Doc> {
|
|
21
|
+
const clientIdKey = `yjsClientId:${collection}`;
|
|
22
|
+
let clientId = await kv.get<number>(clientIdKey);
|
|
23
|
+
|
|
24
|
+
if (!clientId) {
|
|
25
|
+
clientId = Math.floor(Math.random() * 2147483647);
|
|
26
|
+
await kv.set(clientIdKey, clientId);
|
|
27
|
+
logger.info('Generated new Yjs clientID', { collection, clientId });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const ydoc = new Y.Doc({
|
|
31
|
+
guid: collection,
|
|
32
|
+
clientID: clientId,
|
|
33
|
+
} as any);
|
|
34
|
+
|
|
35
|
+
logger.info('Created Yjs document', { collection, clientId });
|
|
36
|
+
return ydoc;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Apply a binary update to a Yjs document.
|
|
41
|
+
* Y.applyUpdateV2 is already atomic, no need for transaction wrapper.
|
|
42
|
+
*/
|
|
43
|
+
export function applyUpdate(doc: Y.Doc, update: Uint8Array, origin?: string): void {
|
|
44
|
+
Y.applyUpdateV2(doc, update, origin);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get a Y.Map from a Yjs document by name.
|
|
49
|
+
*/
|
|
50
|
+
export function getYMap<T = unknown>(doc: Y.Doc, name: string): Y.Map<T> {
|
|
51
|
+
return doc.getMap(name);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Execute a function within a Yjs transaction.
|
|
56
|
+
*/
|
|
57
|
+
export function yjsTransact<A>(doc: Y.Doc, fn: () => A, origin?: string): A {
|
|
58
|
+
return doc.transact(fn, origin);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Execute a function within a Yjs transaction and capture the delta.
|
|
63
|
+
* Returns both the function result and a delta containing only the changes made.
|
|
64
|
+
*/
|
|
65
|
+
export function transactWithDelta<A>(
|
|
66
|
+
doc: Y.Doc,
|
|
67
|
+
fn: () => A,
|
|
68
|
+
origin?: string
|
|
69
|
+
): { result: A; delta: Uint8Array } {
|
|
70
|
+
const beforeVector = Y.encodeStateVector(doc);
|
|
71
|
+
const result = doc.transact(fn, origin);
|
|
72
|
+
const delta = Y.encodeStateAsUpdateV2(doc, beforeVector);
|
|
73
|
+
return { result, delta };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ============================================================================
|
|
77
|
+
// Yjs Serialization System
|
|
78
|
+
// ============================================================================
|
|
79
|
+
// Yjs uses `instanceof AbstractType` internally in toJSON() which breaks when
|
|
80
|
+
// multiple Yjs module instances exist (common with bundlers). We detect Yjs
|
|
81
|
+
// types by their internal structure (`doc`, `_map`, `_start` properties) which
|
|
82
|
+
// is stable across instances, then manually iterate using forEach/toArray.
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if a value is a Yjs AbstractType by checking internal properties.
|
|
87
|
+
* All Yjs types (Y.Map, Y.Array, Y.Text, Y.XmlFragment, etc.) extend AbstractType
|
|
88
|
+
* and have these properties regardless of which module instance created them.
|
|
89
|
+
*/
|
|
90
|
+
function isYjsAbstractType(value: unknown): boolean {
|
|
91
|
+
if (value === null || typeof value !== 'object') return false;
|
|
92
|
+
const v = value as Record<string, unknown>;
|
|
93
|
+
// AbstractType has: doc (Doc|null), _map (Map), _eH (event handler)
|
|
94
|
+
return '_map' in v && '_eH' in v && 'doc' in v;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if a value is a Y.Map.
|
|
99
|
+
* Y.Map has keys() method which Y.XmlFragment does not.
|
|
100
|
+
*/
|
|
101
|
+
function isYMap(value: unknown): boolean {
|
|
102
|
+
if (!isYjsAbstractType(value)) return false;
|
|
103
|
+
const v = value as Record<string, unknown>;
|
|
104
|
+
return typeof v.keys === 'function' && typeof v.get === 'function';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Check if a value is a Y.Array (has toArray but not get - distinguishes from Y.Map).
|
|
109
|
+
*/
|
|
110
|
+
function isYArray(value: unknown): boolean {
|
|
111
|
+
if (!isYjsAbstractType(value)) return false;
|
|
112
|
+
const v = value as Record<string, unknown>;
|
|
113
|
+
return typeof v.toArray === 'function' && typeof v.get !== 'function';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if a value is a Y.XmlFragment or Y.XmlElement.
|
|
118
|
+
* XmlFragment has toArray() and get(index), but NOT keys() like Y.Map.
|
|
119
|
+
*/
|
|
120
|
+
function isYXmlFragment(value: unknown): value is Y.XmlFragment {
|
|
121
|
+
if (!isYjsAbstractType(value)) return false;
|
|
122
|
+
const v = value as Record<string, unknown>;
|
|
123
|
+
// XmlFragment has toArray() but NOT keys() - keys() is unique to Y.Map
|
|
124
|
+
return typeof v.toArray === 'function' && typeof v.keys !== 'function';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Recursively serialize a Yjs value to plain JavaScript.
|
|
129
|
+
* Handles Y.Map, Y.Array, Y.XmlFragment without using instanceof.
|
|
130
|
+
*/
|
|
131
|
+
function serialize(value: unknown): unknown {
|
|
132
|
+
// Primitives pass through
|
|
133
|
+
if (value === null || value === undefined) return value;
|
|
134
|
+
if (typeof value !== 'object') return value;
|
|
135
|
+
|
|
136
|
+
// Check for XmlFragment first (converts to ProseMirror JSON)
|
|
137
|
+
if (isYXmlFragment(value)) {
|
|
138
|
+
return fragmentToJSON(value);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Y.Map - iterate with forEach and recursively serialize values
|
|
142
|
+
if (isYMap(value)) {
|
|
143
|
+
const result: Record<string, unknown> = {};
|
|
144
|
+
const ymap = value as Y.Map<unknown>;
|
|
145
|
+
ymap.forEach((v, k) => {
|
|
146
|
+
result[k] = serialize(v);
|
|
147
|
+
});
|
|
148
|
+
return result;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Y.Array - convert to array and recursively serialize elements
|
|
152
|
+
if (isYArray(value)) {
|
|
153
|
+
return (value as Y.Array<unknown>).toArray().map(serialize);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Regular object/array (not a Yjs type) - return as-is
|
|
157
|
+
return value;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Serialize a Y.Map to a plain object.
|
|
162
|
+
*/
|
|
163
|
+
export function serializeYMap(ymap: Y.Map<unknown>): Record<string, unknown> {
|
|
164
|
+
return serialize(ymap) as Record<string, unknown>;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Extract all items from a Y.Map as plain objects.
|
|
169
|
+
*/
|
|
170
|
+
export function extractItems<T>(ymap: Y.Map<unknown>): T[] {
|
|
171
|
+
const items: T[] = [];
|
|
172
|
+
ymap.forEach((value) => {
|
|
173
|
+
if (isYMap(value)) {
|
|
174
|
+
items.push(serialize(value) as T);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
return items;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Extract a single item from a Y.Map by key.
|
|
182
|
+
*/
|
|
183
|
+
export function extractItem<T>(ymap: Y.Map<unknown>, key: string): T | null {
|
|
184
|
+
const value = ymap.get(key);
|
|
185
|
+
if (isYMap(value)) {
|
|
186
|
+
return serialize(value) as T;
|
|
187
|
+
}
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
import type { XmlFragmentJSON, XmlNodeJSON } from '$/shared/types.js';
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Check if a value looks like ProseMirror/BlockNote JSON document.
|
|
195
|
+
* Used internally to auto-detect prose fields during insert/update.
|
|
196
|
+
*/
|
|
197
|
+
export function isDoc(value: unknown): value is XmlFragmentJSON {
|
|
198
|
+
return (
|
|
199
|
+
typeof value === 'object' &&
|
|
200
|
+
value !== null &&
|
|
201
|
+
'type' in value &&
|
|
202
|
+
(value as { type: unknown }).type === 'doc'
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Convert a Y.XmlFragment to ProseMirror-compatible JSON.
|
|
208
|
+
*/
|
|
209
|
+
export function fragmentToJSON(fragment: Y.XmlFragment): XmlFragmentJSON {
|
|
210
|
+
const content: XmlNodeJSON[] = [];
|
|
211
|
+
|
|
212
|
+
for (const child of fragment.toArray()) {
|
|
213
|
+
if (child instanceof Y.XmlElement) {
|
|
214
|
+
content.push(xmlElementToJSON(child));
|
|
215
|
+
} else if (child instanceof Y.XmlText) {
|
|
216
|
+
const textContent = xmlTextToJSON(child);
|
|
217
|
+
if (textContent.length > 0) {
|
|
218
|
+
content.push({
|
|
219
|
+
type: 'paragraph',
|
|
220
|
+
content: textContent,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
type: 'doc',
|
|
228
|
+
content: content.length > 0 ? content : [{ type: 'paragraph' }],
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function xmlElementToJSON(element: Y.XmlElement): XmlNodeJSON {
|
|
233
|
+
const result: XmlNodeJSON = {
|
|
234
|
+
type: element.nodeName,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
const attrs = element.getAttributes();
|
|
238
|
+
if (Object.keys(attrs).length > 0) {
|
|
239
|
+
result.attrs = attrs;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const content: XmlNodeJSON[] = [];
|
|
243
|
+
for (const child of element.toArray()) {
|
|
244
|
+
if (child instanceof Y.XmlElement) {
|
|
245
|
+
content.push(xmlElementToJSON(child));
|
|
246
|
+
} else if (child instanceof Y.XmlText) {
|
|
247
|
+
content.push(...xmlTextToJSON(child));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (content.length > 0) {
|
|
252
|
+
result.content = content;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function xmlTextToJSON(text: Y.XmlText): XmlNodeJSON[] {
|
|
259
|
+
const result: XmlNodeJSON[] = [];
|
|
260
|
+
const delta = text.toDelta();
|
|
261
|
+
|
|
262
|
+
for (const op of delta) {
|
|
263
|
+
if (typeof op.insert === 'string') {
|
|
264
|
+
const node: XmlNodeJSON = {
|
|
265
|
+
type: 'text',
|
|
266
|
+
text: op.insert,
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
if (op.attributes && Object.keys(op.attributes).length > 0) {
|
|
270
|
+
node.marks = Object.entries(op.attributes).map(([type, attrs]) => ({
|
|
271
|
+
type,
|
|
272
|
+
attrs: typeof attrs === 'object' ? (attrs as Record<string, unknown>) : undefined,
|
|
273
|
+
}));
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
result.push(node);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return result;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Initialize a Y.XmlFragment from ProseMirror-compatible JSON.
|
|
285
|
+
*/
|
|
286
|
+
export function fragmentFromJSON(fragment: Y.XmlFragment, json: XmlFragmentJSON): void {
|
|
287
|
+
if (!json.content) return;
|
|
288
|
+
|
|
289
|
+
for (const node of json.content) {
|
|
290
|
+
appendNodeToFragment(fragment, node);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Extract plain text from ProseMirror/BlockNote JSON content.
|
|
296
|
+
* Handles various content structures defensively for search and display.
|
|
297
|
+
*/
|
|
298
|
+
export function extract(content: unknown): string {
|
|
299
|
+
if (!content || typeof content !== 'object') return '';
|
|
300
|
+
|
|
301
|
+
const doc = content as { content?: unknown; type?: string };
|
|
302
|
+
|
|
303
|
+
// Handle XmlFragmentJSON format - content must be an array
|
|
304
|
+
if (!doc.content || !Array.isArray(doc.content)) return '';
|
|
305
|
+
|
|
306
|
+
return doc.content
|
|
307
|
+
.map((block: { content?: unknown }) => {
|
|
308
|
+
if (!block.content || !Array.isArray(block.content)) return '';
|
|
309
|
+
return block.content.map((node: { text?: string }) => node.text || '').join('');
|
|
310
|
+
})
|
|
311
|
+
.join(' ');
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function appendNodeToFragment(parent: Y.XmlFragment | Y.XmlElement, node: XmlNodeJSON): void {
|
|
315
|
+
if (node.type === 'text') {
|
|
316
|
+
const text = new Y.XmlText();
|
|
317
|
+
if (node.text) {
|
|
318
|
+
const attrs: Record<string, unknown> = {};
|
|
319
|
+
if (node.marks) {
|
|
320
|
+
for (const mark of node.marks) {
|
|
321
|
+
attrs[mark.type] = mark.attrs ?? true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
text.insert(0, node.text, Object.keys(attrs).length > 0 ? attrs : undefined);
|
|
325
|
+
}
|
|
326
|
+
parent.insert(parent.length, [text]);
|
|
327
|
+
} else {
|
|
328
|
+
const element = new Y.XmlElement(node.type);
|
|
329
|
+
|
|
330
|
+
if (node.attrs) {
|
|
331
|
+
for (const [key, value] of Object.entries(node.attrs)) {
|
|
332
|
+
element.setAttribute(key, value as string);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
if (node.content) {
|
|
337
|
+
for (const child of node.content) {
|
|
338
|
+
appendNodeToFragment(element, child);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
parent.insert(parent.length, [element]);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Serialize any value, handling Yjs types specially.
|
|
348
|
+
* Uses our custom serialization system that works across module instances.
|
|
349
|
+
*/
|
|
350
|
+
export function serializeYMapValue(value: unknown): unknown {
|
|
351
|
+
return serialize(value);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Get a Y.XmlFragment from a document's field.
|
|
356
|
+
* Returns null if the document or field doesn't exist, or if the field is not an XmlFragment.
|
|
357
|
+
*/
|
|
358
|
+
export function getFragmentFromYMap(
|
|
359
|
+
ymap: Y.Map<unknown>,
|
|
360
|
+
documentId: string,
|
|
361
|
+
field: string
|
|
362
|
+
): Y.XmlFragment | null {
|
|
363
|
+
const doc = ymap.get(documentId);
|
|
364
|
+
if (!isYMap(doc)) {
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const fieldValue = (doc as Y.Map<unknown>).get(field);
|
|
369
|
+
if (isYXmlFragment(fieldValue)) {
|
|
370
|
+
return fieldValue;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return null;
|
|
374
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQLite adapter wrappers for different platforms.
|
|
3
|
+
*
|
|
4
|
+
* These are wrapper classes - the consuming app imports and initializes
|
|
5
|
+
* the actual database packages, then passes them to these wrappers.
|
|
6
|
+
*/
|
|
7
|
+
export { SqlJsAdapter, type SqlJsDatabase, type SqlJsAdapterOptions } from './sqljs.js';
|
|
8
|
+
export { OPSqliteAdapter, type OPSQLiteDatabase } from './opsqlite.js';
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* op-sqlite adapter wrapper for React Native SQLite.
|
|
3
|
+
*
|
|
4
|
+
* The consuming app imports @op-engineering/op-sqlite and opens the database,
|
|
5
|
+
* then passes it to this wrapper.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* import { open } from '@op-engineering/op-sqlite';
|
|
10
|
+
* import { OPSqliteAdapter } from '@trestleinc/replicate/client';
|
|
11
|
+
*
|
|
12
|
+
* const db = open({ name: 'myapp.db' });
|
|
13
|
+
* const adapter = new OPSqliteAdapter(db);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import type { SqliteAdapter } from '../sqlite-level.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Interface for op-sqlite Database.
|
|
20
|
+
* Consumer must install @op-engineering/op-sqlite and pass a Database instance.
|
|
21
|
+
*/
|
|
22
|
+
export interface OPSQLiteDatabase {
|
|
23
|
+
execute(sql: string, params?: unknown[]): Promise<{ rows: Record<string, unknown>[] }>;
|
|
24
|
+
close(): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Wraps an op-sqlite Database as a SqliteAdapter.
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```typescript
|
|
32
|
+
* import { open } from '@op-engineering/op-sqlite';
|
|
33
|
+
* import { OPSqliteAdapter } from '@trestleinc/replicate/client';
|
|
34
|
+
*
|
|
35
|
+
* const db = open({ name: 'myapp.db' });
|
|
36
|
+
* const adapter = new OPSqliteAdapter(db);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export class OPSqliteAdapter implements SqliteAdapter {
|
|
40
|
+
private db: OPSQLiteDatabase;
|
|
41
|
+
|
|
42
|
+
constructor(db: OPSQLiteDatabase) {
|
|
43
|
+
this.db = db;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async execute(sql: string, params?: unknown[]): Promise<{ rows: Record<string, unknown>[] }> {
|
|
47
|
+
const result = await this.db.execute(sql, params);
|
|
48
|
+
return { rows: result.rows || [] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
close(): void {
|
|
52
|
+
this.db.close();
|
|
53
|
+
}
|
|
54
|
+
}
|