@foretag/tanstack-db-surrealdb 0.2.2 → 0.3.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 +2 -8
- package/dist/index.d.mts +18 -6
- package/dist/index.d.ts +18 -6
- package/dist/index.js +303 -136
- package/dist/index.mjs +303 -136
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -17,14 +17,6 @@ npm install @foretag/tanstack-db-surrealdb
|
|
|
17
17
|
bun install @foretag/tanstack-db-surrealdb
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
### JSR
|
|
21
|
-
```sh
|
|
22
|
-
# NPM
|
|
23
|
-
npx jsr add @foretag/tanstack-db-surrealdb
|
|
24
|
-
# Bun
|
|
25
|
-
bunx jsr add @foretag/tanstack-db-surrealdb
|
|
26
|
-
```
|
|
27
|
-
|
|
28
20
|
## Usage
|
|
29
21
|
```ts
|
|
30
22
|
// db.ts
|
|
@@ -60,6 +52,8 @@ export const products = createCollection(
|
|
|
60
52
|
)
|
|
61
53
|
```
|
|
62
54
|
|
|
55
|
+
For syncModes, please see [Example](https://github.com/ForetagInc/tanstack-db-surrealdb/blob/master/examples/syncMode.ts)
|
|
56
|
+
|
|
63
57
|
## Vite / Next.JS
|
|
64
58
|
|
|
65
59
|
### Vite
|
package/dist/index.d.mts
CHANGED
|
@@ -9,23 +9,35 @@ type SyncedTable<T> = WithId<T & {
|
|
|
9
9
|
sync_deleted?: boolean;
|
|
10
10
|
updated_at?: Date;
|
|
11
11
|
}>;
|
|
12
|
-
type
|
|
13
|
-
name: string;
|
|
14
|
-
fields?: (keyof T)[];
|
|
15
|
-
where?: ExprLike;
|
|
16
|
-
};
|
|
12
|
+
type SyncMode = 'eager' | 'on-demand' | 'progressive';
|
|
17
13
|
type SurrealCollectionConfig<T extends {
|
|
18
14
|
id: string | RecordId;
|
|
19
15
|
}> = {
|
|
20
16
|
id?: string;
|
|
21
17
|
db: Surreal;
|
|
22
18
|
table: TableOptions<T>;
|
|
19
|
+
syncMode?: SyncMode;
|
|
23
20
|
useLoro?: boolean;
|
|
24
21
|
onError?: (e: unknown) => void;
|
|
25
22
|
};
|
|
23
|
+
type Field<I> = keyof I | (string & {});
|
|
24
|
+
type FieldList<I> = '*' | ReadonlyArray<Field<I>>;
|
|
25
|
+
type TableOptions<T> = {
|
|
26
|
+
name: string;
|
|
27
|
+
fields?: FieldList<T>;
|
|
28
|
+
where?: ExprLike;
|
|
29
|
+
pageSize?: number;
|
|
30
|
+
initialPageSize?: number;
|
|
31
|
+
onProgress?: (info: {
|
|
32
|
+
table: string;
|
|
33
|
+
loaded: number;
|
|
34
|
+
lastBatch: number;
|
|
35
|
+
done: boolean;
|
|
36
|
+
}) => void;
|
|
37
|
+
};
|
|
26
38
|
|
|
27
39
|
declare function surrealCollectionOptions<T extends SyncedTable<object>, S extends Record<string, Container> = {
|
|
28
40
|
[k: string]: never;
|
|
29
|
-
}>({ id, useLoro, onError, db, ...config }: SurrealCollectionConfig<T>): CollectionConfig<T, string | number, never, UtilsRecord>;
|
|
41
|
+
}>({ id, useLoro, onError, db, syncMode, ...config }: SurrealCollectionConfig<T>): CollectionConfig<T, string | number, never, UtilsRecord>;
|
|
30
42
|
|
|
31
43
|
export { surrealCollectionOptions };
|
package/dist/index.d.ts
CHANGED
|
@@ -9,23 +9,35 @@ type SyncedTable<T> = WithId<T & {
|
|
|
9
9
|
sync_deleted?: boolean;
|
|
10
10
|
updated_at?: Date;
|
|
11
11
|
}>;
|
|
12
|
-
type
|
|
13
|
-
name: string;
|
|
14
|
-
fields?: (keyof T)[];
|
|
15
|
-
where?: ExprLike;
|
|
16
|
-
};
|
|
12
|
+
type SyncMode = 'eager' | 'on-demand' | 'progressive';
|
|
17
13
|
type SurrealCollectionConfig<T extends {
|
|
18
14
|
id: string | RecordId;
|
|
19
15
|
}> = {
|
|
20
16
|
id?: string;
|
|
21
17
|
db: Surreal;
|
|
22
18
|
table: TableOptions<T>;
|
|
19
|
+
syncMode?: SyncMode;
|
|
23
20
|
useLoro?: boolean;
|
|
24
21
|
onError?: (e: unknown) => void;
|
|
25
22
|
};
|
|
23
|
+
type Field<I> = keyof I | (string & {});
|
|
24
|
+
type FieldList<I> = '*' | ReadonlyArray<Field<I>>;
|
|
25
|
+
type TableOptions<T> = {
|
|
26
|
+
name: string;
|
|
27
|
+
fields?: FieldList<T>;
|
|
28
|
+
where?: ExprLike;
|
|
29
|
+
pageSize?: number;
|
|
30
|
+
initialPageSize?: number;
|
|
31
|
+
onProgress?: (info: {
|
|
32
|
+
table: string;
|
|
33
|
+
loaded: number;
|
|
34
|
+
lastBatch: number;
|
|
35
|
+
done: boolean;
|
|
36
|
+
}) => void;
|
|
37
|
+
};
|
|
26
38
|
|
|
27
39
|
declare function surrealCollectionOptions<T extends SyncedTable<object>, S extends Record<string, Container> = {
|
|
28
40
|
[k: string]: never;
|
|
29
|
-
}>({ id, useLoro, onError, db, ...config }: SurrealCollectionConfig<T>): CollectionConfig<T, string | number, never, UtilsRecord>;
|
|
41
|
+
}>({ id, useLoro, onError, db, syncMode, ...config }: SurrealCollectionConfig<T>): CollectionConfig<T, string | number, never, UtilsRecord>;
|
|
30
42
|
|
|
31
43
|
export { surrealCollectionOptions };
|
package/dist/index.js
CHANGED
|
@@ -27,74 +27,162 @@ var import_surrealdb2 = require("surrealdb");
|
|
|
27
27
|
|
|
28
28
|
// src/table.ts
|
|
29
29
|
var import_surrealdb = require("surrealdb");
|
|
30
|
-
function manageTable(db, useLoro, { name, ...args }) {
|
|
31
|
-
const
|
|
32
|
-
const
|
|
30
|
+
function manageTable(db, useLoro, { name, ...args }, syncMode = "eager") {
|
|
31
|
+
const rawFields = args.fields ?? "*";
|
|
32
|
+
const fields = rawFields === "*" ? ["*"] : [...rawFields];
|
|
33
|
+
const cache = /* @__PURE__ */ new Map();
|
|
34
|
+
let fullyLoaded = false;
|
|
35
|
+
const pageSize = args.pageSize ?? 100;
|
|
36
|
+
const initialPageSize = args.initialPageSize ?? Math.min(50, pageSize);
|
|
37
|
+
let cursor = 0;
|
|
38
|
+
let progressiveTask = null;
|
|
39
|
+
const idKey = (id) => typeof id === "string" ? id : id.toString();
|
|
40
|
+
const upsertCache = (rows) => {
|
|
41
|
+
for (const row of rows) cache.set(idKey(row.id), row);
|
|
42
|
+
};
|
|
43
|
+
const removeFromCache = (id) => {
|
|
44
|
+
cache.delete(idKey(id));
|
|
45
|
+
};
|
|
46
|
+
const listCached = () => Array.from(cache.values());
|
|
47
|
+
const buildWhere = () => {
|
|
48
|
+
if (!useLoro) return args.where;
|
|
49
|
+
return args.where ? (0, import_surrealdb.and)(args.where, (0, import_surrealdb.eq)("sync_deleted", false)) : (0, import_surrealdb.eq)("sync_deleted", false);
|
|
50
|
+
};
|
|
51
|
+
const buildQuery = () => {
|
|
33
52
|
let q = db.select(new import_surrealdb.Table(name));
|
|
34
|
-
|
|
35
|
-
|
|
53
|
+
const cond = buildWhere();
|
|
54
|
+
if (cond) q = q.where(cond);
|
|
55
|
+
return q;
|
|
56
|
+
};
|
|
57
|
+
const applyPaging = (q, start, limit) => {
|
|
58
|
+
if (typeof start === "number" && q.start) q = q.start(start);
|
|
59
|
+
if (typeof limit === "number" && q.limit) q = q.limit(limit);
|
|
60
|
+
return q;
|
|
61
|
+
};
|
|
62
|
+
const fetchAll = async () => {
|
|
63
|
+
const rows = await buildQuery().fields(...fields);
|
|
64
|
+
upsertCache(rows);
|
|
65
|
+
fullyLoaded = true;
|
|
66
|
+
return rows;
|
|
67
|
+
};
|
|
68
|
+
const fetchPage = async (opts) => {
|
|
69
|
+
const q = applyPaging(
|
|
70
|
+
buildQuery(),
|
|
71
|
+
opts?.start ?? 0,
|
|
72
|
+
opts?.limit ?? pageSize
|
|
73
|
+
);
|
|
74
|
+
const rows = await q.fields(...fields);
|
|
75
|
+
upsertCache(rows);
|
|
76
|
+
if (rows.length < (opts?.limit ?? pageSize)) fullyLoaded = true;
|
|
77
|
+
return rows;
|
|
36
78
|
};
|
|
79
|
+
const fetchById = async (id) => {
|
|
80
|
+
const key = idKey(id);
|
|
81
|
+
const cached = cache.get(key);
|
|
82
|
+
if (cached) return cached;
|
|
83
|
+
const res = await db.select(id);
|
|
84
|
+
const row = Array.isArray(res) ? res[0] : res;
|
|
85
|
+
if (!row) return null;
|
|
86
|
+
if (useLoro && row.sync_deleted)
|
|
87
|
+
return null;
|
|
88
|
+
cache.set(key, row);
|
|
89
|
+
return row;
|
|
90
|
+
};
|
|
91
|
+
const loadMore = async (limit = pageSize) => {
|
|
92
|
+
if (fullyLoaded) return { rows: [], done: true };
|
|
93
|
+
const rows = await fetchPage({ start: cursor, limit });
|
|
94
|
+
cursor += rows.length;
|
|
95
|
+
const done = fullyLoaded || rows.length < limit;
|
|
96
|
+
args.onProgress?.({
|
|
97
|
+
table: name,
|
|
98
|
+
loaded: cache.size,
|
|
99
|
+
lastBatch: rows.length,
|
|
100
|
+
done
|
|
101
|
+
});
|
|
102
|
+
return { rows, done };
|
|
103
|
+
};
|
|
104
|
+
const startProgressive = () => {
|
|
105
|
+
if (progressiveTask || fullyLoaded) return;
|
|
106
|
+
progressiveTask = (async () => {
|
|
107
|
+
if (cache.size === 0) await loadMore(initialPageSize);
|
|
108
|
+
while (!fullyLoaded) {
|
|
109
|
+
const { done } = await loadMore(pageSize);
|
|
110
|
+
if (done) break;
|
|
111
|
+
}
|
|
112
|
+
})().finally(() => {
|
|
113
|
+
progressiveTask = null;
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
const listAll = () => fetchAll();
|
|
37
117
|
const listActive = async () => {
|
|
38
|
-
if (
|
|
39
|
-
|
|
118
|
+
if (syncMode === "eager") return fetchAll();
|
|
119
|
+
if (syncMode === "progressive") {
|
|
120
|
+
if (cache.size === 0) await loadMore(initialPageSize);
|
|
121
|
+
startProgressive();
|
|
122
|
+
return listCached();
|
|
123
|
+
}
|
|
124
|
+
return listCached();
|
|
40
125
|
};
|
|
41
126
|
const create = async (data) => {
|
|
42
127
|
await db.create(new import_surrealdb.Table(name)).content(data);
|
|
43
128
|
};
|
|
44
129
|
const update = async (id, data) => {
|
|
45
130
|
if (!useLoro) {
|
|
46
|
-
await db.update(id).merge(
|
|
47
|
-
...data
|
|
48
|
-
});
|
|
131
|
+
await db.update(id).merge(data);
|
|
49
132
|
return;
|
|
50
133
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
});
|
|
57
|
-
} catch (error) {
|
|
58
|
-
console.warn(
|
|
59
|
-
`Please ensure the table ${name} has sync_deleted and updated_at fields defined`
|
|
60
|
-
);
|
|
61
|
-
console.error(
|
|
62
|
-
"Failed to update record with Loro (CRDTs) with: ",
|
|
63
|
-
error
|
|
64
|
-
);
|
|
65
|
-
}
|
|
134
|
+
await db.update(id).merge({
|
|
135
|
+
...data,
|
|
136
|
+
sync_deleted: false,
|
|
137
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
138
|
+
});
|
|
66
139
|
};
|
|
67
140
|
const remove = async (id) => {
|
|
68
141
|
await db.delete(id);
|
|
142
|
+
removeFromCache(id);
|
|
69
143
|
};
|
|
70
144
|
const softDelete = async (id) => {
|
|
71
145
|
if (!useLoro) {
|
|
72
146
|
await db.delete(id);
|
|
147
|
+
removeFromCache(id);
|
|
73
148
|
return;
|
|
74
149
|
}
|
|
75
150
|
await db.upsert(id).merge({
|
|
76
151
|
sync_deleted: true,
|
|
77
|
-
updated_at: Date
|
|
152
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
78
153
|
});
|
|
154
|
+
removeFromCache(id);
|
|
79
155
|
};
|
|
80
156
|
const subscribe = (cb) => {
|
|
81
157
|
let killed = false;
|
|
82
158
|
let live;
|
|
83
|
-
const on = (
|
|
84
|
-
|
|
159
|
+
const on = (msg) => {
|
|
160
|
+
const { action, value } = msg;
|
|
85
161
|
if (action === "KILLED") return;
|
|
86
|
-
if (action === "CREATE")
|
|
87
|
-
|
|
162
|
+
if (action === "CREATE") {
|
|
163
|
+
upsertCache([value]);
|
|
164
|
+
cb({ type: "insert", row: value });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
if (action === "UPDATE") {
|
|
168
|
+
if (useLoro && value.sync_deleted) {
|
|
169
|
+
removeFromCache(value.id);
|
|
170
|
+
cb({ type: "delete", row: { id: value.id } });
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
upsertCache([value]);
|
|
88
174
|
cb({ type: "update", row: value });
|
|
89
|
-
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (action === "DELETE") {
|
|
178
|
+
removeFromCache(value.id);
|
|
90
179
|
cb({ type: "delete", row: { id: value.id } });
|
|
180
|
+
}
|
|
91
181
|
};
|
|
92
182
|
const start = async () => {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
live.subscribe(on);
|
|
97
|
-
}
|
|
183
|
+
if (!db.isFeatureSupported(import_surrealdb.Features.LiveQueries)) return;
|
|
184
|
+
live = await db.live(new import_surrealdb.Table(name)).where(args.where);
|
|
185
|
+
live.subscribe(on);
|
|
98
186
|
};
|
|
99
187
|
void start();
|
|
100
188
|
return () => {
|
|
@@ -106,25 +194,61 @@ function manageTable(db, useLoro, { name, ...args }) {
|
|
|
106
194
|
return {
|
|
107
195
|
listAll,
|
|
108
196
|
listActive,
|
|
197
|
+
listCached,
|
|
198
|
+
fetchPage,
|
|
199
|
+
fetchById,
|
|
200
|
+
loadMore,
|
|
109
201
|
create,
|
|
110
202
|
update,
|
|
111
203
|
remove,
|
|
112
204
|
softDelete,
|
|
113
|
-
subscribe
|
|
205
|
+
subscribe,
|
|
206
|
+
get isFullyLoaded() {
|
|
207
|
+
return fullyLoaded;
|
|
208
|
+
},
|
|
209
|
+
get cachedCount() {
|
|
210
|
+
return cache.size;
|
|
211
|
+
}
|
|
114
212
|
};
|
|
115
213
|
}
|
|
116
214
|
|
|
117
215
|
// src/index.ts
|
|
216
|
+
var DEFAULT_INITIAL_PAGE_SIZE = 50;
|
|
217
|
+
var LOCAL_ID_VERIFY_CHUNK = 500;
|
|
218
|
+
var stableStringify = (value) => {
|
|
219
|
+
const toJson = (v) => {
|
|
220
|
+
if (v === null) return null;
|
|
221
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean")
|
|
222
|
+
return v;
|
|
223
|
+
if (v instanceof Date) return v.toISOString();
|
|
224
|
+
if (Array.isArray(v)) return v.map(toJson);
|
|
225
|
+
if (typeof v === "object") {
|
|
226
|
+
const o = v;
|
|
227
|
+
const keys = Object.keys(o).sort();
|
|
228
|
+
const out = {};
|
|
229
|
+
for (const k of keys) out[k] = toJson(o[k]);
|
|
230
|
+
return out;
|
|
231
|
+
}
|
|
232
|
+
return String(v);
|
|
233
|
+
};
|
|
234
|
+
return JSON.stringify(toJson(value));
|
|
235
|
+
};
|
|
236
|
+
var chunk = (arr, size) => {
|
|
237
|
+
const out = [];
|
|
238
|
+
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
|
|
239
|
+
return out;
|
|
240
|
+
};
|
|
118
241
|
function surrealCollectionOptions({
|
|
119
242
|
id,
|
|
120
243
|
useLoro = false,
|
|
121
244
|
onError,
|
|
122
245
|
db,
|
|
246
|
+
syncMode,
|
|
123
247
|
...config
|
|
124
248
|
}) {
|
|
125
249
|
let loro;
|
|
126
250
|
if (useLoro) loro = { doc: new import_loro_crdt.LoroDoc(), key: id };
|
|
127
|
-
const keyOf = (
|
|
251
|
+
const keyOf = (rid) => typeof rid === "string" ? rid : rid.toString();
|
|
128
252
|
const getKey = (row) => keyOf(row.id);
|
|
129
253
|
const loroKey = loro?.key ?? id ?? "surreal";
|
|
130
254
|
const loroMap = useLoro ? loro?.doc?.getMap?.(loroKey) ?? null : null;
|
|
@@ -133,16 +257,17 @@ function surrealCollectionOptions({
|
|
|
133
257
|
const json = loroMap.toJSON?.() ?? {};
|
|
134
258
|
return Object.values(json);
|
|
135
259
|
};
|
|
136
|
-
const
|
|
137
|
-
if (!loroMap) return;
|
|
138
|
-
loroMap.set(getKey(row), row);
|
|
260
|
+
const loroPutMany = (rows) => {
|
|
261
|
+
if (!loroMap || rows.length === 0) return;
|
|
262
|
+
for (const row of rows) loroMap.set(getKey(row), row);
|
|
139
263
|
loro?.doc?.commit?.();
|
|
140
264
|
};
|
|
141
|
-
const
|
|
142
|
-
if (!loroMap) return;
|
|
143
|
-
loroMap.delete(id2);
|
|
265
|
+
const loroRemoveMany = (ids) => {
|
|
266
|
+
if (!loroMap || ids.length === 0) return;
|
|
267
|
+
for (const id2 of ids) loroMap.delete(id2);
|
|
144
268
|
loro?.doc?.commit?.();
|
|
145
269
|
};
|
|
270
|
+
const loroRemove = (id2) => loroRemoveMany([id2]);
|
|
146
271
|
const pushQueue = [];
|
|
147
272
|
const enqueuePush = (op) => {
|
|
148
273
|
if (!useLoro) return;
|
|
@@ -154,35 +279,73 @@ function surrealCollectionOptions({
|
|
|
154
279
|
for (const op of ops) {
|
|
155
280
|
if (op.kind === "create") {
|
|
156
281
|
await table.create(op.row);
|
|
157
|
-
}
|
|
158
|
-
if (op.kind === "update") {
|
|
282
|
+
} else if (op.kind === "update") {
|
|
159
283
|
const rid = new import_surrealdb2.RecordId(
|
|
160
284
|
config.table.name,
|
|
161
285
|
op.row.id.toString()
|
|
162
286
|
);
|
|
163
287
|
await table.update(rid, op.row);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const rid = new import_surrealdb2.RecordId(config.table.name, op.id.toString());
|
|
288
|
+
} else {
|
|
289
|
+
const rid = new import_surrealdb2.RecordId(config.table.name, op.id);
|
|
167
290
|
await table.softDelete(rid);
|
|
168
291
|
}
|
|
169
292
|
}
|
|
170
293
|
};
|
|
171
294
|
const newer = (a, b) => (a?.getTime() ?? -1) > (b?.getTime() ?? -1);
|
|
172
|
-
const
|
|
173
|
-
if (
|
|
174
|
-
|
|
175
|
-
|
|
295
|
+
const fetchServerByLocalIds = async (ids) => {
|
|
296
|
+
if (ids.length === 0) return [];
|
|
297
|
+
const tableName = config.table.name;
|
|
298
|
+
const parts = chunk(ids, LOCAL_ID_VERIFY_CHUNK);
|
|
299
|
+
const out = [];
|
|
300
|
+
for (const p of parts) {
|
|
301
|
+
const [res] = await db.query(
|
|
302
|
+
"SELECT * FROM type::table($table) WHERE id IN $ids",
|
|
303
|
+
{
|
|
304
|
+
table: tableName,
|
|
305
|
+
ids: p.map((x) => new import_surrealdb2.RecordId(tableName, x))
|
|
306
|
+
}
|
|
307
|
+
);
|
|
308
|
+
if (res) out.push(...res);
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
};
|
|
312
|
+
const dedupeById = (rows) => {
|
|
313
|
+
const m = /* @__PURE__ */ new Map();
|
|
314
|
+
for (const r of rows) m.set(getKey(r), r);
|
|
315
|
+
return Array.from(m.values());
|
|
316
|
+
};
|
|
317
|
+
let prevById = /* @__PURE__ */ new Map();
|
|
318
|
+
const buildMap = (rows) => new Map(rows.map((r) => [getKey(r), r]));
|
|
319
|
+
const same = (a, b) => {
|
|
320
|
+
if (useLoro) {
|
|
321
|
+
return (a.sync_deleted ?? false) === (b.sync_deleted ?? false) && (a.updated_at?.getTime() ?? 0) === (b.updated_at?.getTime() ?? 0);
|
|
322
|
+
}
|
|
323
|
+
return stableStringify(a) === stableStringify(b);
|
|
324
|
+
};
|
|
325
|
+
const diffAndEmit = (currentRows, write) => {
|
|
326
|
+
const currById = buildMap(currentRows);
|
|
327
|
+
for (const [id2, row] of currById) {
|
|
328
|
+
const prev = prevById.get(id2);
|
|
329
|
+
if (!prev) write({ type: "insert", value: row });
|
|
330
|
+
else if (!same(prev, row)) write({ type: "update", value: row });
|
|
176
331
|
}
|
|
332
|
+
for (const [id2, prev] of prevById) {
|
|
333
|
+
if (!currById.has(id2)) write({ type: "delete", value: prev });
|
|
334
|
+
}
|
|
335
|
+
prevById = currById;
|
|
336
|
+
};
|
|
337
|
+
const reconcileBoot = (serverRows, write) => {
|
|
177
338
|
const localRows = loroToArray();
|
|
178
339
|
const serverById = new Map(serverRows.map((r) => [getKey(r), r]));
|
|
179
340
|
const localById = new Map(localRows.map((r) => [getKey(r), r]));
|
|
180
341
|
const ids = /* @__PURE__ */ new Set([...serverById.keys(), ...localById.keys()]);
|
|
181
342
|
const current = [];
|
|
343
|
+
const toRemove = [];
|
|
344
|
+
const toPut = [];
|
|
182
345
|
const applyLocal = (row) => {
|
|
183
346
|
if (!row) return;
|
|
184
|
-
if (row.sync_deleted)
|
|
185
|
-
else
|
|
347
|
+
if (row.sync_deleted) toRemove.push(getKey(row));
|
|
348
|
+
else toPut.push(row);
|
|
186
349
|
};
|
|
187
350
|
for (const id2 of ids) {
|
|
188
351
|
const s = serverById.get(id2);
|
|
@@ -242,46 +405,11 @@ function surrealCollectionOptions({
|
|
|
242
405
|
}
|
|
243
406
|
}
|
|
244
407
|
}
|
|
408
|
+
loroRemoveMany(toRemove);
|
|
409
|
+
loroPutMany(toPut);
|
|
245
410
|
diffAndEmit(current, write);
|
|
246
411
|
};
|
|
247
|
-
|
|
248
|
-
const buildMap = (rows) => new Map(rows.map((r) => [getKey(r), r]));
|
|
249
|
-
const same = (a, b) => {
|
|
250
|
-
if (useLoro) {
|
|
251
|
-
const aUpdated = a.updated_at;
|
|
252
|
-
const bUpdated = b.updated_at;
|
|
253
|
-
const aDeleted = a.sync_deleted ?? false;
|
|
254
|
-
const bDeleted = b.sync_deleted ?? false;
|
|
255
|
-
return aDeleted === bDeleted && (aUpdated?.getTime() ?? 0) === (bUpdated?.getTime() ?? 0) && JSON.stringify({
|
|
256
|
-
...a,
|
|
257
|
-
updated_at: void 0,
|
|
258
|
-
sync_deleted: void 0
|
|
259
|
-
}) === JSON.stringify({
|
|
260
|
-
...b,
|
|
261
|
-
updated_at: void 0,
|
|
262
|
-
sync_deleted: void 0
|
|
263
|
-
});
|
|
264
|
-
}
|
|
265
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
266
|
-
};
|
|
267
|
-
const diffAndEmit = (currentRows, write) => {
|
|
268
|
-
const currById = buildMap(currentRows);
|
|
269
|
-
for (const [id2, row] of currById) {
|
|
270
|
-
const prev = prevById.get(id2);
|
|
271
|
-
if (!prev) {
|
|
272
|
-
write({ type: "insert", value: row });
|
|
273
|
-
} else if (!same(prev, row)) {
|
|
274
|
-
write({ type: "update", value: row });
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
for (const [id2, prev] of prevById) {
|
|
278
|
-
if (!currById.has(id2)) {
|
|
279
|
-
write({ type: "delete", value: prev });
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
prevById = currById;
|
|
283
|
-
};
|
|
284
|
-
const table = manageTable(db, useLoro, config.table);
|
|
412
|
+
const table = manageTable(db, useLoro, config.table, syncMode);
|
|
285
413
|
const now = () => /* @__PURE__ */ new Date();
|
|
286
414
|
const sync = ({
|
|
287
415
|
begin,
|
|
@@ -295,6 +423,11 @@ function surrealCollectionOptions({
|
|
|
295
423
|
};
|
|
296
424
|
}
|
|
297
425
|
let offLive = null;
|
|
426
|
+
let work = Promise.resolve();
|
|
427
|
+
const enqueueWork = (fn) => {
|
|
428
|
+
work = work.then(fn).catch((e) => onError?.(e));
|
|
429
|
+
return work;
|
|
430
|
+
};
|
|
298
431
|
const makeTombstone = (id2) => ({
|
|
299
432
|
id: new import_surrealdb2.RecordId(config.table.name, id2).toString(),
|
|
300
433
|
updated_at: now(),
|
|
@@ -302,43 +435,84 @@ function surrealCollectionOptions({
|
|
|
302
435
|
});
|
|
303
436
|
const start = async () => {
|
|
304
437
|
try {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
else
|
|
309
|
-
|
|
310
|
-
|
|
438
|
+
let serverRows;
|
|
439
|
+
if (syncMode === "eager") {
|
|
440
|
+
serverRows = await table.listAll();
|
|
441
|
+
} else if (syncMode === "progressive") {
|
|
442
|
+
const first = await table.loadMore(
|
|
443
|
+
config.table.initialPageSize ?? DEFAULT_INITIAL_PAGE_SIZE
|
|
444
|
+
);
|
|
445
|
+
serverRows = first.rows;
|
|
446
|
+
} else {
|
|
447
|
+
serverRows = await table.listActive();
|
|
448
|
+
}
|
|
449
|
+
await enqueueWork(async () => {
|
|
450
|
+
begin();
|
|
451
|
+
if (useLoro) {
|
|
452
|
+
const localIds = loroToArray().map(getKey);
|
|
453
|
+
const verifiedServerRows = syncMode === "eager" ? serverRows : dedupeById([
|
|
454
|
+
...serverRows,
|
|
455
|
+
...await fetchServerByLocalIds(
|
|
456
|
+
localIds
|
|
457
|
+
)
|
|
458
|
+
]);
|
|
459
|
+
reconcileBoot(verifiedServerRows, write);
|
|
460
|
+
} else {
|
|
461
|
+
diffAndEmit(serverRows, write);
|
|
462
|
+
}
|
|
463
|
+
commit();
|
|
464
|
+
markReady();
|
|
465
|
+
});
|
|
466
|
+
if (syncMode === "progressive") {
|
|
467
|
+
void (async () => {
|
|
468
|
+
while (!table.isFullyLoaded) {
|
|
469
|
+
const { rows } = await table.loadMore();
|
|
470
|
+
if (rows.length === 0) break;
|
|
471
|
+
await enqueueWork(async () => {
|
|
472
|
+
begin();
|
|
473
|
+
try {
|
|
474
|
+
if (useLoro) loroPutMany(rows);
|
|
475
|
+
diffAndEmit(rows, write);
|
|
476
|
+
} finally {
|
|
477
|
+
commit();
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
})().catch((e) => onError?.(e));
|
|
482
|
+
}
|
|
311
483
|
await flushPushQueue();
|
|
312
484
|
offLive = table.subscribe((evt) => {
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
485
|
+
void enqueueWork(async () => {
|
|
486
|
+
begin();
|
|
487
|
+
try {
|
|
488
|
+
if (evt.type === "insert" || evt.type === "update") {
|
|
489
|
+
const row = evt.row;
|
|
490
|
+
const deleted = useLoro ? row.sync_deleted ?? false : false;
|
|
491
|
+
if (deleted) {
|
|
492
|
+
if (useLoro) loroRemove(getKey(row));
|
|
493
|
+
const prev = prevById.get(getKey(row)) ?? makeTombstone(getKey(row));
|
|
494
|
+
write({ type: "delete", value: prev });
|
|
495
|
+
prevById.delete(getKey(row));
|
|
496
|
+
} else {
|
|
497
|
+
if (useLoro) loroPutMany([row]);
|
|
498
|
+
const had = prevById.has(getKey(row));
|
|
499
|
+
write({
|
|
500
|
+
type: had ? "update" : "insert",
|
|
501
|
+
value: row
|
|
502
|
+
});
|
|
503
|
+
prevById.set(getKey(row), row);
|
|
504
|
+
}
|
|
323
505
|
} else {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
});
|
|
330
|
-
prevById.set(getKey(row), row);
|
|
506
|
+
const rid = getKey(evt.row);
|
|
507
|
+
if (useLoro) loroRemove(rid);
|
|
508
|
+
const prev = prevById.get(rid) ?? makeTombstone(rid);
|
|
509
|
+
write({ type: "delete", value: prev });
|
|
510
|
+
prevById.delete(rid);
|
|
331
511
|
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
if (useLoro) loroRemove(id2);
|
|
335
|
-
const prev = prevById.get(id2) ?? makeTombstone(id2);
|
|
336
|
-
write({ type: "delete", value: prev });
|
|
337
|
-
prevById.delete(id2);
|
|
512
|
+
} finally {
|
|
513
|
+
commit();
|
|
338
514
|
}
|
|
339
|
-
}
|
|
340
|
-
commit();
|
|
341
|
-
}
|
|
515
|
+
});
|
|
342
516
|
});
|
|
343
517
|
} catch (e) {
|
|
344
518
|
onError?.(e);
|
|
@@ -355,12 +529,8 @@ function surrealCollectionOptions({
|
|
|
355
529
|
for (const m of p.transaction.mutations) {
|
|
356
530
|
if (m.type !== "insert") continue;
|
|
357
531
|
const base = { ...m.modified };
|
|
358
|
-
const row = useLoro ? {
|
|
359
|
-
|
|
360
|
-
updated_at: now(),
|
|
361
|
-
sync_deleted: false
|
|
362
|
-
} : base;
|
|
363
|
-
if (useLoro) loroPut(row);
|
|
532
|
+
const row = useLoro ? { ...base, updated_at: now(), sync_deleted: false } : base;
|
|
533
|
+
if (useLoro) loroPutMany([row]);
|
|
364
534
|
await table.create(row);
|
|
365
535
|
resultRows.push(row);
|
|
366
536
|
}
|
|
@@ -372,11 +542,8 @@ function surrealCollectionOptions({
|
|
|
372
542
|
if (m.type !== "update") continue;
|
|
373
543
|
const id2 = m.key;
|
|
374
544
|
const base = { ...m.modified, id: id2 };
|
|
375
|
-
const merged = useLoro ? {
|
|
376
|
-
|
|
377
|
-
updated_at: now()
|
|
378
|
-
} : base;
|
|
379
|
-
if (useLoro) loroPut(merged);
|
|
545
|
+
const merged = useLoro ? { ...base, updated_at: now() } : base;
|
|
546
|
+
if (useLoro) loroPutMany([merged]);
|
|
380
547
|
const rid = new import_surrealdb2.RecordId(config.table.name, keyOf(id2));
|
|
381
548
|
await table.update(rid, merged);
|
|
382
549
|
resultRows.push(merged);
|
package/dist/index.mjs
CHANGED
|
@@ -9,74 +9,162 @@ import {
|
|
|
9
9
|
Features,
|
|
10
10
|
Table
|
|
11
11
|
} from "surrealdb";
|
|
12
|
-
function manageTable(db, useLoro, { name, ...args }) {
|
|
13
|
-
const
|
|
14
|
-
const
|
|
12
|
+
function manageTable(db, useLoro, { name, ...args }, syncMode = "eager") {
|
|
13
|
+
const rawFields = args.fields ?? "*";
|
|
14
|
+
const fields = rawFields === "*" ? ["*"] : [...rawFields];
|
|
15
|
+
const cache = /* @__PURE__ */ new Map();
|
|
16
|
+
let fullyLoaded = false;
|
|
17
|
+
const pageSize = args.pageSize ?? 100;
|
|
18
|
+
const initialPageSize = args.initialPageSize ?? Math.min(50, pageSize);
|
|
19
|
+
let cursor = 0;
|
|
20
|
+
let progressiveTask = null;
|
|
21
|
+
const idKey = (id) => typeof id === "string" ? id : id.toString();
|
|
22
|
+
const upsertCache = (rows) => {
|
|
23
|
+
for (const row of rows) cache.set(idKey(row.id), row);
|
|
24
|
+
};
|
|
25
|
+
const removeFromCache = (id) => {
|
|
26
|
+
cache.delete(idKey(id));
|
|
27
|
+
};
|
|
28
|
+
const listCached = () => Array.from(cache.values());
|
|
29
|
+
const buildWhere = () => {
|
|
30
|
+
if (!useLoro) return args.where;
|
|
31
|
+
return args.where ? and(args.where, eq("sync_deleted", false)) : eq("sync_deleted", false);
|
|
32
|
+
};
|
|
33
|
+
const buildQuery = () => {
|
|
15
34
|
let q = db.select(new Table(name));
|
|
16
|
-
|
|
17
|
-
|
|
35
|
+
const cond = buildWhere();
|
|
36
|
+
if (cond) q = q.where(cond);
|
|
37
|
+
return q;
|
|
38
|
+
};
|
|
39
|
+
const applyPaging = (q, start, limit) => {
|
|
40
|
+
if (typeof start === "number" && q.start) q = q.start(start);
|
|
41
|
+
if (typeof limit === "number" && q.limit) q = q.limit(limit);
|
|
42
|
+
return q;
|
|
43
|
+
};
|
|
44
|
+
const fetchAll = async () => {
|
|
45
|
+
const rows = await buildQuery().fields(...fields);
|
|
46
|
+
upsertCache(rows);
|
|
47
|
+
fullyLoaded = true;
|
|
48
|
+
return rows;
|
|
49
|
+
};
|
|
50
|
+
const fetchPage = async (opts) => {
|
|
51
|
+
const q = applyPaging(
|
|
52
|
+
buildQuery(),
|
|
53
|
+
opts?.start ?? 0,
|
|
54
|
+
opts?.limit ?? pageSize
|
|
55
|
+
);
|
|
56
|
+
const rows = await q.fields(...fields);
|
|
57
|
+
upsertCache(rows);
|
|
58
|
+
if (rows.length < (opts?.limit ?? pageSize)) fullyLoaded = true;
|
|
59
|
+
return rows;
|
|
18
60
|
};
|
|
61
|
+
const fetchById = async (id) => {
|
|
62
|
+
const key = idKey(id);
|
|
63
|
+
const cached = cache.get(key);
|
|
64
|
+
if (cached) return cached;
|
|
65
|
+
const res = await db.select(id);
|
|
66
|
+
const row = Array.isArray(res) ? res[0] : res;
|
|
67
|
+
if (!row) return null;
|
|
68
|
+
if (useLoro && row.sync_deleted)
|
|
69
|
+
return null;
|
|
70
|
+
cache.set(key, row);
|
|
71
|
+
return row;
|
|
72
|
+
};
|
|
73
|
+
const loadMore = async (limit = pageSize) => {
|
|
74
|
+
if (fullyLoaded) return { rows: [], done: true };
|
|
75
|
+
const rows = await fetchPage({ start: cursor, limit });
|
|
76
|
+
cursor += rows.length;
|
|
77
|
+
const done = fullyLoaded || rows.length < limit;
|
|
78
|
+
args.onProgress?.({
|
|
79
|
+
table: name,
|
|
80
|
+
loaded: cache.size,
|
|
81
|
+
lastBatch: rows.length,
|
|
82
|
+
done
|
|
83
|
+
});
|
|
84
|
+
return { rows, done };
|
|
85
|
+
};
|
|
86
|
+
const startProgressive = () => {
|
|
87
|
+
if (progressiveTask || fullyLoaded) return;
|
|
88
|
+
progressiveTask = (async () => {
|
|
89
|
+
if (cache.size === 0) await loadMore(initialPageSize);
|
|
90
|
+
while (!fullyLoaded) {
|
|
91
|
+
const { done } = await loadMore(pageSize);
|
|
92
|
+
if (done) break;
|
|
93
|
+
}
|
|
94
|
+
})().finally(() => {
|
|
95
|
+
progressiveTask = null;
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
const listAll = () => fetchAll();
|
|
19
99
|
const listActive = async () => {
|
|
20
|
-
if (
|
|
21
|
-
|
|
100
|
+
if (syncMode === "eager") return fetchAll();
|
|
101
|
+
if (syncMode === "progressive") {
|
|
102
|
+
if (cache.size === 0) await loadMore(initialPageSize);
|
|
103
|
+
startProgressive();
|
|
104
|
+
return listCached();
|
|
105
|
+
}
|
|
106
|
+
return listCached();
|
|
22
107
|
};
|
|
23
108
|
const create = async (data) => {
|
|
24
109
|
await db.create(new Table(name)).content(data);
|
|
25
110
|
};
|
|
26
111
|
const update = async (id, data) => {
|
|
27
112
|
if (!useLoro) {
|
|
28
|
-
await db.update(id).merge(
|
|
29
|
-
...data
|
|
30
|
-
});
|
|
113
|
+
await db.update(id).merge(data);
|
|
31
114
|
return;
|
|
32
115
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
});
|
|
39
|
-
} catch (error) {
|
|
40
|
-
console.warn(
|
|
41
|
-
`Please ensure the table ${name} has sync_deleted and updated_at fields defined`
|
|
42
|
-
);
|
|
43
|
-
console.error(
|
|
44
|
-
"Failed to update record with Loro (CRDTs) with: ",
|
|
45
|
-
error
|
|
46
|
-
);
|
|
47
|
-
}
|
|
116
|
+
await db.update(id).merge({
|
|
117
|
+
...data,
|
|
118
|
+
sync_deleted: false,
|
|
119
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
120
|
+
});
|
|
48
121
|
};
|
|
49
122
|
const remove = async (id) => {
|
|
50
123
|
await db.delete(id);
|
|
124
|
+
removeFromCache(id);
|
|
51
125
|
};
|
|
52
126
|
const softDelete = async (id) => {
|
|
53
127
|
if (!useLoro) {
|
|
54
128
|
await db.delete(id);
|
|
129
|
+
removeFromCache(id);
|
|
55
130
|
return;
|
|
56
131
|
}
|
|
57
132
|
await db.upsert(id).merge({
|
|
58
133
|
sync_deleted: true,
|
|
59
|
-
updated_at: Date
|
|
134
|
+
updated_at: /* @__PURE__ */ new Date()
|
|
60
135
|
});
|
|
136
|
+
removeFromCache(id);
|
|
61
137
|
};
|
|
62
138
|
const subscribe = (cb) => {
|
|
63
139
|
let killed = false;
|
|
64
140
|
let live;
|
|
65
|
-
const on = (
|
|
66
|
-
|
|
141
|
+
const on = (msg) => {
|
|
142
|
+
const { action, value } = msg;
|
|
67
143
|
if (action === "KILLED") return;
|
|
68
|
-
if (action === "CREATE")
|
|
69
|
-
|
|
144
|
+
if (action === "CREATE") {
|
|
145
|
+
upsertCache([value]);
|
|
146
|
+
cb({ type: "insert", row: value });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (action === "UPDATE") {
|
|
150
|
+
if (useLoro && value.sync_deleted) {
|
|
151
|
+
removeFromCache(value.id);
|
|
152
|
+
cb({ type: "delete", row: { id: value.id } });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
upsertCache([value]);
|
|
70
156
|
cb({ type: "update", row: value });
|
|
71
|
-
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (action === "DELETE") {
|
|
160
|
+
removeFromCache(value.id);
|
|
72
161
|
cb({ type: "delete", row: { id: value.id } });
|
|
162
|
+
}
|
|
73
163
|
};
|
|
74
164
|
const start = async () => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
live.subscribe(on);
|
|
79
|
-
}
|
|
165
|
+
if (!db.isFeatureSupported(Features.LiveQueries)) return;
|
|
166
|
+
live = await db.live(new Table(name)).where(args.where);
|
|
167
|
+
live.subscribe(on);
|
|
80
168
|
};
|
|
81
169
|
void start();
|
|
82
170
|
return () => {
|
|
@@ -88,25 +176,61 @@ function manageTable(db, useLoro, { name, ...args }) {
|
|
|
88
176
|
return {
|
|
89
177
|
listAll,
|
|
90
178
|
listActive,
|
|
179
|
+
listCached,
|
|
180
|
+
fetchPage,
|
|
181
|
+
fetchById,
|
|
182
|
+
loadMore,
|
|
91
183
|
create,
|
|
92
184
|
update,
|
|
93
185
|
remove,
|
|
94
186
|
softDelete,
|
|
95
|
-
subscribe
|
|
187
|
+
subscribe,
|
|
188
|
+
get isFullyLoaded() {
|
|
189
|
+
return fullyLoaded;
|
|
190
|
+
},
|
|
191
|
+
get cachedCount() {
|
|
192
|
+
return cache.size;
|
|
193
|
+
}
|
|
96
194
|
};
|
|
97
195
|
}
|
|
98
196
|
|
|
99
197
|
// src/index.ts
|
|
198
|
+
var DEFAULT_INITIAL_PAGE_SIZE = 50;
|
|
199
|
+
var LOCAL_ID_VERIFY_CHUNK = 500;
|
|
200
|
+
var stableStringify = (value) => {
|
|
201
|
+
const toJson = (v) => {
|
|
202
|
+
if (v === null) return null;
|
|
203
|
+
if (typeof v === "string" || typeof v === "number" || typeof v === "boolean")
|
|
204
|
+
return v;
|
|
205
|
+
if (v instanceof Date) return v.toISOString();
|
|
206
|
+
if (Array.isArray(v)) return v.map(toJson);
|
|
207
|
+
if (typeof v === "object") {
|
|
208
|
+
const o = v;
|
|
209
|
+
const keys = Object.keys(o).sort();
|
|
210
|
+
const out = {};
|
|
211
|
+
for (const k of keys) out[k] = toJson(o[k]);
|
|
212
|
+
return out;
|
|
213
|
+
}
|
|
214
|
+
return String(v);
|
|
215
|
+
};
|
|
216
|
+
return JSON.stringify(toJson(value));
|
|
217
|
+
};
|
|
218
|
+
var chunk = (arr, size) => {
|
|
219
|
+
const out = [];
|
|
220
|
+
for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
|
|
221
|
+
return out;
|
|
222
|
+
};
|
|
100
223
|
function surrealCollectionOptions({
|
|
101
224
|
id,
|
|
102
225
|
useLoro = false,
|
|
103
226
|
onError,
|
|
104
227
|
db,
|
|
228
|
+
syncMode,
|
|
105
229
|
...config
|
|
106
230
|
}) {
|
|
107
231
|
let loro;
|
|
108
232
|
if (useLoro) loro = { doc: new LoroDoc(), key: id };
|
|
109
|
-
const keyOf = (
|
|
233
|
+
const keyOf = (rid) => typeof rid === "string" ? rid : rid.toString();
|
|
110
234
|
const getKey = (row) => keyOf(row.id);
|
|
111
235
|
const loroKey = loro?.key ?? id ?? "surreal";
|
|
112
236
|
const loroMap = useLoro ? loro?.doc?.getMap?.(loroKey) ?? null : null;
|
|
@@ -115,16 +239,17 @@ function surrealCollectionOptions({
|
|
|
115
239
|
const json = loroMap.toJSON?.() ?? {};
|
|
116
240
|
return Object.values(json);
|
|
117
241
|
};
|
|
118
|
-
const
|
|
119
|
-
if (!loroMap) return;
|
|
120
|
-
loroMap.set(getKey(row), row);
|
|
242
|
+
const loroPutMany = (rows) => {
|
|
243
|
+
if (!loroMap || rows.length === 0) return;
|
|
244
|
+
for (const row of rows) loroMap.set(getKey(row), row);
|
|
121
245
|
loro?.doc?.commit?.();
|
|
122
246
|
};
|
|
123
|
-
const
|
|
124
|
-
if (!loroMap) return;
|
|
125
|
-
loroMap.delete(id2);
|
|
247
|
+
const loroRemoveMany = (ids) => {
|
|
248
|
+
if (!loroMap || ids.length === 0) return;
|
|
249
|
+
for (const id2 of ids) loroMap.delete(id2);
|
|
126
250
|
loro?.doc?.commit?.();
|
|
127
251
|
};
|
|
252
|
+
const loroRemove = (id2) => loroRemoveMany([id2]);
|
|
128
253
|
const pushQueue = [];
|
|
129
254
|
const enqueuePush = (op) => {
|
|
130
255
|
if (!useLoro) return;
|
|
@@ -136,35 +261,73 @@ function surrealCollectionOptions({
|
|
|
136
261
|
for (const op of ops) {
|
|
137
262
|
if (op.kind === "create") {
|
|
138
263
|
await table.create(op.row);
|
|
139
|
-
}
|
|
140
|
-
if (op.kind === "update") {
|
|
264
|
+
} else if (op.kind === "update") {
|
|
141
265
|
const rid = new RecordId(
|
|
142
266
|
config.table.name,
|
|
143
267
|
op.row.id.toString()
|
|
144
268
|
);
|
|
145
269
|
await table.update(rid, op.row);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
const rid = new RecordId(config.table.name, op.id.toString());
|
|
270
|
+
} else {
|
|
271
|
+
const rid = new RecordId(config.table.name, op.id);
|
|
149
272
|
await table.softDelete(rid);
|
|
150
273
|
}
|
|
151
274
|
}
|
|
152
275
|
};
|
|
153
276
|
const newer = (a, b) => (a?.getTime() ?? -1) > (b?.getTime() ?? -1);
|
|
154
|
-
const
|
|
155
|
-
if (
|
|
156
|
-
|
|
157
|
-
|
|
277
|
+
const fetchServerByLocalIds = async (ids) => {
|
|
278
|
+
if (ids.length === 0) return [];
|
|
279
|
+
const tableName = config.table.name;
|
|
280
|
+
const parts = chunk(ids, LOCAL_ID_VERIFY_CHUNK);
|
|
281
|
+
const out = [];
|
|
282
|
+
for (const p of parts) {
|
|
283
|
+
const [res] = await db.query(
|
|
284
|
+
"SELECT * FROM type::table($table) WHERE id IN $ids",
|
|
285
|
+
{
|
|
286
|
+
table: tableName,
|
|
287
|
+
ids: p.map((x) => new RecordId(tableName, x))
|
|
288
|
+
}
|
|
289
|
+
);
|
|
290
|
+
if (res) out.push(...res);
|
|
291
|
+
}
|
|
292
|
+
return out;
|
|
293
|
+
};
|
|
294
|
+
const dedupeById = (rows) => {
|
|
295
|
+
const m = /* @__PURE__ */ new Map();
|
|
296
|
+
for (const r of rows) m.set(getKey(r), r);
|
|
297
|
+
return Array.from(m.values());
|
|
298
|
+
};
|
|
299
|
+
let prevById = /* @__PURE__ */ new Map();
|
|
300
|
+
const buildMap = (rows) => new Map(rows.map((r) => [getKey(r), r]));
|
|
301
|
+
const same = (a, b) => {
|
|
302
|
+
if (useLoro) {
|
|
303
|
+
return (a.sync_deleted ?? false) === (b.sync_deleted ?? false) && (a.updated_at?.getTime() ?? 0) === (b.updated_at?.getTime() ?? 0);
|
|
304
|
+
}
|
|
305
|
+
return stableStringify(a) === stableStringify(b);
|
|
306
|
+
};
|
|
307
|
+
const diffAndEmit = (currentRows, write) => {
|
|
308
|
+
const currById = buildMap(currentRows);
|
|
309
|
+
for (const [id2, row] of currById) {
|
|
310
|
+
const prev = prevById.get(id2);
|
|
311
|
+
if (!prev) write({ type: "insert", value: row });
|
|
312
|
+
else if (!same(prev, row)) write({ type: "update", value: row });
|
|
158
313
|
}
|
|
314
|
+
for (const [id2, prev] of prevById) {
|
|
315
|
+
if (!currById.has(id2)) write({ type: "delete", value: prev });
|
|
316
|
+
}
|
|
317
|
+
prevById = currById;
|
|
318
|
+
};
|
|
319
|
+
const reconcileBoot = (serverRows, write) => {
|
|
159
320
|
const localRows = loroToArray();
|
|
160
321
|
const serverById = new Map(serverRows.map((r) => [getKey(r), r]));
|
|
161
322
|
const localById = new Map(localRows.map((r) => [getKey(r), r]));
|
|
162
323
|
const ids = /* @__PURE__ */ new Set([...serverById.keys(), ...localById.keys()]);
|
|
163
324
|
const current = [];
|
|
325
|
+
const toRemove = [];
|
|
326
|
+
const toPut = [];
|
|
164
327
|
const applyLocal = (row) => {
|
|
165
328
|
if (!row) return;
|
|
166
|
-
if (row.sync_deleted)
|
|
167
|
-
else
|
|
329
|
+
if (row.sync_deleted) toRemove.push(getKey(row));
|
|
330
|
+
else toPut.push(row);
|
|
168
331
|
};
|
|
169
332
|
for (const id2 of ids) {
|
|
170
333
|
const s = serverById.get(id2);
|
|
@@ -224,46 +387,11 @@ function surrealCollectionOptions({
|
|
|
224
387
|
}
|
|
225
388
|
}
|
|
226
389
|
}
|
|
390
|
+
loroRemoveMany(toRemove);
|
|
391
|
+
loroPutMany(toPut);
|
|
227
392
|
diffAndEmit(current, write);
|
|
228
393
|
};
|
|
229
|
-
|
|
230
|
-
const buildMap = (rows) => new Map(rows.map((r) => [getKey(r), r]));
|
|
231
|
-
const same = (a, b) => {
|
|
232
|
-
if (useLoro) {
|
|
233
|
-
const aUpdated = a.updated_at;
|
|
234
|
-
const bUpdated = b.updated_at;
|
|
235
|
-
const aDeleted = a.sync_deleted ?? false;
|
|
236
|
-
const bDeleted = b.sync_deleted ?? false;
|
|
237
|
-
return aDeleted === bDeleted && (aUpdated?.getTime() ?? 0) === (bUpdated?.getTime() ?? 0) && JSON.stringify({
|
|
238
|
-
...a,
|
|
239
|
-
updated_at: void 0,
|
|
240
|
-
sync_deleted: void 0
|
|
241
|
-
}) === JSON.stringify({
|
|
242
|
-
...b,
|
|
243
|
-
updated_at: void 0,
|
|
244
|
-
sync_deleted: void 0
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
return JSON.stringify(a) === JSON.stringify(b);
|
|
248
|
-
};
|
|
249
|
-
const diffAndEmit = (currentRows, write) => {
|
|
250
|
-
const currById = buildMap(currentRows);
|
|
251
|
-
for (const [id2, row] of currById) {
|
|
252
|
-
const prev = prevById.get(id2);
|
|
253
|
-
if (!prev) {
|
|
254
|
-
write({ type: "insert", value: row });
|
|
255
|
-
} else if (!same(prev, row)) {
|
|
256
|
-
write({ type: "update", value: row });
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
for (const [id2, prev] of prevById) {
|
|
260
|
-
if (!currById.has(id2)) {
|
|
261
|
-
write({ type: "delete", value: prev });
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
prevById = currById;
|
|
265
|
-
};
|
|
266
|
-
const table = manageTable(db, useLoro, config.table);
|
|
394
|
+
const table = manageTable(db, useLoro, config.table, syncMode);
|
|
267
395
|
const now = () => /* @__PURE__ */ new Date();
|
|
268
396
|
const sync = ({
|
|
269
397
|
begin,
|
|
@@ -277,6 +405,11 @@ function surrealCollectionOptions({
|
|
|
277
405
|
};
|
|
278
406
|
}
|
|
279
407
|
let offLive = null;
|
|
408
|
+
let work = Promise.resolve();
|
|
409
|
+
const enqueueWork = (fn) => {
|
|
410
|
+
work = work.then(fn).catch((e) => onError?.(e));
|
|
411
|
+
return work;
|
|
412
|
+
};
|
|
280
413
|
const makeTombstone = (id2) => ({
|
|
281
414
|
id: new RecordId(config.table.name, id2).toString(),
|
|
282
415
|
updated_at: now(),
|
|
@@ -284,43 +417,84 @@ function surrealCollectionOptions({
|
|
|
284
417
|
});
|
|
285
418
|
const start = async () => {
|
|
286
419
|
try {
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
else
|
|
291
|
-
|
|
292
|
-
|
|
420
|
+
let serverRows;
|
|
421
|
+
if (syncMode === "eager") {
|
|
422
|
+
serverRows = await table.listAll();
|
|
423
|
+
} else if (syncMode === "progressive") {
|
|
424
|
+
const first = await table.loadMore(
|
|
425
|
+
config.table.initialPageSize ?? DEFAULT_INITIAL_PAGE_SIZE
|
|
426
|
+
);
|
|
427
|
+
serverRows = first.rows;
|
|
428
|
+
} else {
|
|
429
|
+
serverRows = await table.listActive();
|
|
430
|
+
}
|
|
431
|
+
await enqueueWork(async () => {
|
|
432
|
+
begin();
|
|
433
|
+
if (useLoro) {
|
|
434
|
+
const localIds = loroToArray().map(getKey);
|
|
435
|
+
const verifiedServerRows = syncMode === "eager" ? serverRows : dedupeById([
|
|
436
|
+
...serverRows,
|
|
437
|
+
...await fetchServerByLocalIds(
|
|
438
|
+
localIds
|
|
439
|
+
)
|
|
440
|
+
]);
|
|
441
|
+
reconcileBoot(verifiedServerRows, write);
|
|
442
|
+
} else {
|
|
443
|
+
diffAndEmit(serverRows, write);
|
|
444
|
+
}
|
|
445
|
+
commit();
|
|
446
|
+
markReady();
|
|
447
|
+
});
|
|
448
|
+
if (syncMode === "progressive") {
|
|
449
|
+
void (async () => {
|
|
450
|
+
while (!table.isFullyLoaded) {
|
|
451
|
+
const { rows } = await table.loadMore();
|
|
452
|
+
if (rows.length === 0) break;
|
|
453
|
+
await enqueueWork(async () => {
|
|
454
|
+
begin();
|
|
455
|
+
try {
|
|
456
|
+
if (useLoro) loroPutMany(rows);
|
|
457
|
+
diffAndEmit(rows, write);
|
|
458
|
+
} finally {
|
|
459
|
+
commit();
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
})().catch((e) => onError?.(e));
|
|
464
|
+
}
|
|
293
465
|
await flushPushQueue();
|
|
294
466
|
offLive = table.subscribe((evt) => {
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
if (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
467
|
+
void enqueueWork(async () => {
|
|
468
|
+
begin();
|
|
469
|
+
try {
|
|
470
|
+
if (evt.type === "insert" || evt.type === "update") {
|
|
471
|
+
const row = evt.row;
|
|
472
|
+
const deleted = useLoro ? row.sync_deleted ?? false : false;
|
|
473
|
+
if (deleted) {
|
|
474
|
+
if (useLoro) loroRemove(getKey(row));
|
|
475
|
+
const prev = prevById.get(getKey(row)) ?? makeTombstone(getKey(row));
|
|
476
|
+
write({ type: "delete", value: prev });
|
|
477
|
+
prevById.delete(getKey(row));
|
|
478
|
+
} else {
|
|
479
|
+
if (useLoro) loroPutMany([row]);
|
|
480
|
+
const had = prevById.has(getKey(row));
|
|
481
|
+
write({
|
|
482
|
+
type: had ? "update" : "insert",
|
|
483
|
+
value: row
|
|
484
|
+
});
|
|
485
|
+
prevById.set(getKey(row), row);
|
|
486
|
+
}
|
|
305
487
|
} else {
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
});
|
|
312
|
-
prevById.set(getKey(row), row);
|
|
488
|
+
const rid = getKey(evt.row);
|
|
489
|
+
if (useLoro) loroRemove(rid);
|
|
490
|
+
const prev = prevById.get(rid) ?? makeTombstone(rid);
|
|
491
|
+
write({ type: "delete", value: prev });
|
|
492
|
+
prevById.delete(rid);
|
|
313
493
|
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
if (useLoro) loroRemove(id2);
|
|
317
|
-
const prev = prevById.get(id2) ?? makeTombstone(id2);
|
|
318
|
-
write({ type: "delete", value: prev });
|
|
319
|
-
prevById.delete(id2);
|
|
494
|
+
} finally {
|
|
495
|
+
commit();
|
|
320
496
|
}
|
|
321
|
-
}
|
|
322
|
-
commit();
|
|
323
|
-
}
|
|
497
|
+
});
|
|
324
498
|
});
|
|
325
499
|
} catch (e) {
|
|
326
500
|
onError?.(e);
|
|
@@ -337,12 +511,8 @@ function surrealCollectionOptions({
|
|
|
337
511
|
for (const m of p.transaction.mutations) {
|
|
338
512
|
if (m.type !== "insert") continue;
|
|
339
513
|
const base = { ...m.modified };
|
|
340
|
-
const row = useLoro ? {
|
|
341
|
-
|
|
342
|
-
updated_at: now(),
|
|
343
|
-
sync_deleted: false
|
|
344
|
-
} : base;
|
|
345
|
-
if (useLoro) loroPut(row);
|
|
514
|
+
const row = useLoro ? { ...base, updated_at: now(), sync_deleted: false } : base;
|
|
515
|
+
if (useLoro) loroPutMany([row]);
|
|
346
516
|
await table.create(row);
|
|
347
517
|
resultRows.push(row);
|
|
348
518
|
}
|
|
@@ -354,11 +524,8 @@ function surrealCollectionOptions({
|
|
|
354
524
|
if (m.type !== "update") continue;
|
|
355
525
|
const id2 = m.key;
|
|
356
526
|
const base = { ...m.modified, id: id2 };
|
|
357
|
-
const merged = useLoro ? {
|
|
358
|
-
|
|
359
|
-
updated_at: now()
|
|
360
|
-
} : base;
|
|
361
|
-
if (useLoro) loroPut(merged);
|
|
527
|
+
const merged = useLoro ? { ...base, updated_at: now() } : base;
|
|
528
|
+
if (useLoro) loroPutMany([merged]);
|
|
362
529
|
const rid = new RecordId(config.table.name, keyOf(id2));
|
|
363
530
|
await table.update(rid, merged);
|
|
364
531
|
resultRows.push(merged);
|