@instantdb/core 0.22.56 → 0.22.57
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/dist/commonjs/Reactor.d.ts +5 -0
- package/dist/commonjs/Reactor.d.ts.map +1 -1
- package/dist/commonjs/Reactor.js +43 -5
- package/dist/commonjs/Reactor.js.map +1 -1
- package/dist/commonjs/SyncTable.d.ts +140 -0
- package/dist/commonjs/SyncTable.d.ts.map +1 -0
- package/dist/commonjs/SyncTable.js +538 -0
- package/dist/commonjs/SyncTable.js.map +1 -0
- package/dist/commonjs/index.d.ts +21 -1
- package/dist/commonjs/index.d.ts.map +1 -1
- package/dist/commonjs/index.js +19 -1
- package/dist/commonjs/index.js.map +1 -1
- package/dist/commonjs/instaql.d.ts +1 -0
- package/dist/commonjs/instaql.d.ts.map +1 -1
- package/dist/commonjs/instaql.js +9 -5
- package/dist/commonjs/instaql.js.map +1 -1
- package/dist/commonjs/store.d.ts +5 -0
- package/dist/commonjs/store.d.ts.map +1 -1
- package/dist/commonjs/store.js +11 -0
- package/dist/commonjs/store.js.map +1 -1
- package/dist/commonjs/utils/PersistedObject.d.ts +11 -5
- package/dist/commonjs/utils/PersistedObject.d.ts.map +1 -1
- package/dist/commonjs/utils/PersistedObject.js +12 -2
- package/dist/commonjs/utils/PersistedObject.js.map +1 -1
- package/dist/esm/Reactor.d.ts +5 -0
- package/dist/esm/Reactor.d.ts.map +1 -1
- package/dist/esm/Reactor.js +43 -5
- package/dist/esm/Reactor.js.map +1 -1
- package/dist/esm/SyncTable.d.ts +140 -0
- package/dist/esm/SyncTable.d.ts.map +1 -0
- package/dist/esm/SyncTable.js +498 -0
- package/dist/esm/SyncTable.js.map +1 -0
- package/dist/esm/index.d.ts +21 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +19 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/instaql.d.ts +1 -0
- package/dist/esm/instaql.d.ts.map +1 -1
- package/dist/esm/instaql.js +8 -5
- package/dist/esm/instaql.js.map +1 -1
- package/dist/esm/store.d.ts +5 -0
- package/dist/esm/store.d.ts.map +1 -1
- package/dist/esm/store.js +9 -3
- package/dist/esm/store.js.map +1 -1
- package/dist/esm/utils/PersistedObject.d.ts +11 -5
- package/dist/esm/utils/PersistedObject.d.ts.map +1 -1
- package/dist/esm/utils/PersistedObject.js +12 -2
- package/dist/esm/utils/PersistedObject.js.map +1 -1
- package/dist/standalone/index.js +1579 -1135
- package/dist/standalone/index.umd.cjs +3 -3
- package/package.json +2 -2
- package/src/Reactor.js +60 -4
- package/src/SyncTable.ts +821 -0
- package/src/index.ts +48 -0
- package/src/instaql.js +11 -5
- package/src/store.js +11 -3
- package/src/utils/PersistedObject.ts +15 -6
package/src/SyncTable.ts
ADDED
|
@@ -0,0 +1,821 @@
|
|
|
1
|
+
import { PersistedObject } from './utils/PersistedObject.ts';
|
|
2
|
+
import * as s from './store.js';
|
|
3
|
+
import weakHash from './utils/weakHash.ts';
|
|
4
|
+
import uuid from './utils/uuid.ts';
|
|
5
|
+
import { Logger } from './Reactor.js';
|
|
6
|
+
import instaql, { compareOrder } from './instaql.js';
|
|
7
|
+
import { InstaQLResponse, ValidQuery } from './queryTypes.ts';
|
|
8
|
+
import { EntitiesDef, IContainEntitiesAndLinks } from './schemaTypes.ts';
|
|
9
|
+
|
|
10
|
+
type SubState = {
|
|
11
|
+
txId?: number;
|
|
12
|
+
subscriptionId: string;
|
|
13
|
+
token: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type Sub = {
|
|
17
|
+
query: any;
|
|
18
|
+
hash: string;
|
|
19
|
+
table: string;
|
|
20
|
+
orderField: string;
|
|
21
|
+
orderDirection: 'asc' | 'desc';
|
|
22
|
+
orderFieldType: 'string' | 'number' | 'date' | 'boolean';
|
|
23
|
+
state?: SubState;
|
|
24
|
+
entities?: Array<{ entity: any; store: any; serverCreatedAt: number }>;
|
|
25
|
+
createdAt: number;
|
|
26
|
+
updatedAt: number;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// We could make a better type for this if we had a return type for s.toJSON
|
|
30
|
+
type SubsInStorage = Subs;
|
|
31
|
+
|
|
32
|
+
type Subs = { [hash: string]: Sub };
|
|
33
|
+
|
|
34
|
+
type StartMsg = {
|
|
35
|
+
op: 'start-sync';
|
|
36
|
+
q: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
type EndMsg = {
|
|
40
|
+
op: 'remove-sync';
|
|
41
|
+
'subscription-id': string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type ResyncMsg = {
|
|
45
|
+
op: 'resync-table';
|
|
46
|
+
'subscription-id': string;
|
|
47
|
+
'tx-id': number;
|
|
48
|
+
token: string;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type SendMsg = StartMsg | EndMsg | ResyncMsg;
|
|
52
|
+
|
|
53
|
+
type StartSyncOkMsg = {
|
|
54
|
+
'subscription-id': string;
|
|
55
|
+
'client-event-id': string;
|
|
56
|
+
q: string;
|
|
57
|
+
token: string;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
type Triple = [string, string, any, number];
|
|
61
|
+
|
|
62
|
+
type SyncLoadBatchMsg = {
|
|
63
|
+
'subscription-id': string;
|
|
64
|
+
'join-rows': Array<Triple[]>;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
type SyncInitFinishMsg = {
|
|
68
|
+
'subscription-id': string;
|
|
69
|
+
'tx-id': number;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
type SyncUpdateTriplesMsg = {
|
|
73
|
+
'subscription-id': string;
|
|
74
|
+
txes: {
|
|
75
|
+
'tx-id': number;
|
|
76
|
+
changes: { action: 'added' | 'removed'; triple: Triple }[];
|
|
77
|
+
}[];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
type TrySend = (eventId: string, msg: SendMsg) => void;
|
|
81
|
+
|
|
82
|
+
type Config = { useDateObjects: boolean };
|
|
83
|
+
|
|
84
|
+
// Modifies the data in place because it comes directly from storage
|
|
85
|
+
function syncSubsFromStorage(
|
|
86
|
+
parsed: SubsInStorage,
|
|
87
|
+
useDateObjects: boolean,
|
|
88
|
+
): Subs {
|
|
89
|
+
for (const key in parsed) {
|
|
90
|
+
const sub = parsed[key];
|
|
91
|
+
for (const e of sub.entities || []) {
|
|
92
|
+
e.store.useDateObjects = useDateObjects;
|
|
93
|
+
e.store = s.fromJSON(e.store);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function syncSubsToStorage(subs: Subs): SubsInStorage {
|
|
100
|
+
const jsonSubs = {};
|
|
101
|
+
for (const key in subs) {
|
|
102
|
+
const sub = subs[key];
|
|
103
|
+
if (sub.entities) {
|
|
104
|
+
const entities = [];
|
|
105
|
+
for (const e of sub.entities) {
|
|
106
|
+
entities.push({ ...e, store: s.toJSON(e.store) });
|
|
107
|
+
}
|
|
108
|
+
jsonSubs[key] = { ...sub, entities };
|
|
109
|
+
} else {
|
|
110
|
+
jsonSubs[key] = sub;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return jsonSubs;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function onMergeSubs(storageSubs: Subs | null, inMemorySubs: Subs): Subs {
|
|
117
|
+
const subs = {};
|
|
118
|
+
for (const [k, v] of Object.entries(inMemorySubs)) {
|
|
119
|
+
subs[k] = v;
|
|
120
|
+
}
|
|
121
|
+
if (storageSubs) {
|
|
122
|
+
for (const [k, v] of Object.entries(storageSubs)) {
|
|
123
|
+
subs[k] = v;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return subs;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function queryEntity(sub: Sub, store: any) {
|
|
130
|
+
const res = instaql({ store, pageInfo: null, aggregate: null }, sub.query);
|
|
131
|
+
return res.data[sub.table][0];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function getServerCreatedAt(sub: Sub, store: any, entityId: string): number {
|
|
135
|
+
const aid = s.getAttrByFwdIdentName(store, sub.table, 'id')?.id;
|
|
136
|
+
if (!aid) {
|
|
137
|
+
return -1;
|
|
138
|
+
}
|
|
139
|
+
const t = s.getInMap(store.eav, [entityId, aid, entityId]);
|
|
140
|
+
if (!t) {
|
|
141
|
+
return -1;
|
|
142
|
+
}
|
|
143
|
+
return t[3];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function applyChangesToStore(
|
|
147
|
+
store: any,
|
|
148
|
+
changes: SyncUpdateTriplesMsg['txes'][number]['changes'],
|
|
149
|
+
): void {
|
|
150
|
+
for (const { action, triple } of changes) {
|
|
151
|
+
switch (action) {
|
|
152
|
+
case 'added':
|
|
153
|
+
s.addTriple(store, triple);
|
|
154
|
+
break;
|
|
155
|
+
case 'removed':
|
|
156
|
+
s.retractTriple(store, triple);
|
|
157
|
+
break;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function changedFieldsOfChanges(
|
|
163
|
+
store: any,
|
|
164
|
+
changes: SyncUpdateTriplesMsg['txes'][number]['changes'],
|
|
165
|
+
): {
|
|
166
|
+
[eid: string]: SyncTransaction<
|
|
167
|
+
any,
|
|
168
|
+
any,
|
|
169
|
+
any
|
|
170
|
+
>['updated'][number]['changedFields'];
|
|
171
|
+
} {
|
|
172
|
+
// This will be more complicated when we include links, we can either add a
|
|
173
|
+
// changedLinks field or we can have something like 'bookshelves.title`
|
|
174
|
+
const changedFields = {};
|
|
175
|
+
for (const { action, triple } of changes) {
|
|
176
|
+
const [e, a, v] = triple;
|
|
177
|
+
const field = store.attrs[a]?.['forward-identity']?.[2];
|
|
178
|
+
if (!field) continue;
|
|
179
|
+
|
|
180
|
+
const fields = changedFields[e] ?? {};
|
|
181
|
+
changedFields[e] = fields;
|
|
182
|
+
|
|
183
|
+
const oldNew = fields[field] ?? {};
|
|
184
|
+
fields[field] = oldNew;
|
|
185
|
+
|
|
186
|
+
switch (action) {
|
|
187
|
+
case 'added':
|
|
188
|
+
oldNew.newValue = v;
|
|
189
|
+
break;
|
|
190
|
+
case 'removed':
|
|
191
|
+
// Only take the first thing that was removed, in case we modified things in the middle
|
|
192
|
+
if (oldNew.oldValue === undefined) {
|
|
193
|
+
oldNew.oldValue = v;
|
|
194
|
+
}
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return changedFields;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function subData(sub: Sub, entities: NonNullable<Sub['entities']>) {
|
|
202
|
+
return { [sub.table]: entities.map((e) => e.entity) };
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function sortEntitiesInPlace(sub: Sub, entities: NonNullable<Sub['entities']>) {
|
|
206
|
+
const dataType = sub.orderFieldType;
|
|
207
|
+
if (sub.orderField === 'serverCreatedAt') {
|
|
208
|
+
entities.sort(
|
|
209
|
+
sub.orderDirection === 'asc'
|
|
210
|
+
? function compareEntities(a, b) {
|
|
211
|
+
return compareOrder(
|
|
212
|
+
a.entity.id,
|
|
213
|
+
a.serverCreatedAt,
|
|
214
|
+
b.entity.id,
|
|
215
|
+
b.serverCreatedAt,
|
|
216
|
+
dataType,
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
: function compareEntities(b, a) {
|
|
220
|
+
return compareOrder(
|
|
221
|
+
a.entity.id,
|
|
222
|
+
a.serverCreatedAt,
|
|
223
|
+
b.entity.id,
|
|
224
|
+
b.serverCreatedAt,
|
|
225
|
+
dataType,
|
|
226
|
+
);
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const field = sub.orderField;
|
|
233
|
+
|
|
234
|
+
entities.sort(
|
|
235
|
+
sub.orderDirection === 'asc'
|
|
236
|
+
? function compareEntities(a, b) {
|
|
237
|
+
return compareOrder(
|
|
238
|
+
a.entity.id,
|
|
239
|
+
a.entity[field],
|
|
240
|
+
b.entity.id,
|
|
241
|
+
b.entity[field],
|
|
242
|
+
dataType,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
: function compareEntities(b, a) {
|
|
246
|
+
return compareOrder(
|
|
247
|
+
a.entity.id,
|
|
248
|
+
a.entity[field],
|
|
249
|
+
b.entity.id,
|
|
250
|
+
b.entity[field],
|
|
251
|
+
dataType,
|
|
252
|
+
);
|
|
253
|
+
},
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export enum CallbackEventType {
|
|
258
|
+
InitialSyncBatch = 'InitialSyncBatch',
|
|
259
|
+
InitialSyncComplete = 'InitialSyncComplete',
|
|
260
|
+
LoadFromStorage = 'LoadFromStorage',
|
|
261
|
+
SyncTransaction = 'SyncTransaction',
|
|
262
|
+
Error = 'Error',
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
type QueryEntities<
|
|
266
|
+
Schema extends IContainEntitiesAndLinks<EntitiesDef, any>,
|
|
267
|
+
Q extends ValidQuery<Q, Schema>,
|
|
268
|
+
UseDates extends boolean,
|
|
269
|
+
> = InstaQLResponse<Schema, Q, UseDates>[keyof InstaQLResponse<
|
|
270
|
+
Schema,
|
|
271
|
+
Q,
|
|
272
|
+
UseDates
|
|
273
|
+
>];
|
|
274
|
+
|
|
275
|
+
type QueryEntity<
|
|
276
|
+
Schema extends IContainEntitiesAndLinks<EntitiesDef, any>,
|
|
277
|
+
Q extends ValidQuery<Q, Schema>,
|
|
278
|
+
UseDates extends boolean,
|
|
279
|
+
> = QueryEntities<Schema, Q, UseDates> extends (infer E)[] ? E : never;
|
|
280
|
+
|
|
281
|
+
type ChangedFields<Entity> = {
|
|
282
|
+
[K in keyof Entity]?: {
|
|
283
|
+
oldValue: Entity[K];
|
|
284
|
+
newValue: Entity[K];
|
|
285
|
+
};
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export interface BaseCallbackEvent<
|
|
289
|
+
Schema extends IContainEntitiesAndLinks<EntitiesDef, any>,
|
|
290
|
+
Q extends ValidQuery<Q, Schema>,
|
|
291
|
+
UseDates extends boolean,
|
|
292
|
+
> {
|
|
293
|
+
type: CallbackEventType;
|
|
294
|
+
data: InstaQLResponse<Schema, Q, UseDates>;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export interface InitialSyncBatch<
|
|
298
|
+
Schema extends IContainEntitiesAndLinks<EntitiesDef, any>,
|
|
299
|
+
Q extends ValidQuery<Q, Schema>,
|
|
300
|
+
UseDates extends boolean,
|
|
301
|
+
> extends BaseCallbackEvent<Schema, Q, UseDates> {
|
|
302
|
+
type: CallbackEventType.InitialSyncBatch;
|
|
303
|
+
batch: QueryEntities<Schema, Q, UseDates>;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export interface InitialSyncComplete<
|
|
307
|
+
Schema extends IContainEntitiesAndLinks<EntitiesDef, any>,
|
|
308
|
+
Q extends ValidQuery<Q, Schema>,
|
|
309
|
+
UseDates extends boolean,
|
|
310
|
+
> extends BaseCallbackEvent<Schema, Q, UseDates> {
|
|
311
|
+
type: CallbackEventType.InitialSyncComplete;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export interface SyncTransaction<
|
|
315
|
+
Schema extends IContainEntitiesAndLinks<EntitiesDef, any>,
|
|
316
|
+
Q extends ValidQuery<Q, Schema>,
|
|
317
|
+
UseDates extends boolean,
|
|
318
|
+
> extends BaseCallbackEvent<Schema, Q, UseDates> {
|
|
319
|
+
type: CallbackEventType.SyncTransaction;
|
|
320
|
+
added: QueryEntities<Schema, Q, UseDates>;
|
|
321
|
+
removed: QueryEntities<Schema, Q, UseDates>;
|
|
322
|
+
updated: {
|
|
323
|
+
oldEntity: QueryEntity<Schema, Q, UseDates>;
|
|
324
|
+
newEntity: QueryEntity<Schema, Q, UseDates>;
|
|
325
|
+
changedFields: ChangedFields<QueryEntity<Schema, Q, UseDates>>;
|
|
326
|
+
}[];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
export interface LoadFromStorage<
|
|
330
|
+
Schema extends IContainEntitiesAndLinks<EntitiesDef, any>,
|
|
331
|
+
Q extends ValidQuery<Q, Schema>,
|
|
332
|
+
UseDates extends boolean,
|
|
333
|
+
> extends BaseCallbackEvent<Schema, Q, UseDates> {
|
|
334
|
+
type: CallbackEventType.LoadFromStorage;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
export interface SetupError<
|
|
338
|
+
Schema extends IContainEntitiesAndLinks<EntitiesDef, any>,
|
|
339
|
+
Q extends ValidQuery<Q, Schema>,
|
|
340
|
+
UseDates extends boolean,
|
|
341
|
+
> extends BaseCallbackEvent<Schema, Q, UseDates> {
|
|
342
|
+
type: CallbackEventType.Error;
|
|
343
|
+
error: { message: string; hint?: any; type: string; status: number };
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export type CallbackEvent<
|
|
347
|
+
Schema extends IContainEntitiesAndLinks<EntitiesDef, any>,
|
|
348
|
+
Q extends ValidQuery<Q, Schema>,
|
|
349
|
+
UseDates extends boolean,
|
|
350
|
+
> =
|
|
351
|
+
| InitialSyncBatch<Schema, Q, UseDates>
|
|
352
|
+
| InitialSyncComplete<Schema, Q, UseDates>
|
|
353
|
+
| SyncTransaction<Schema, Q, UseDates>
|
|
354
|
+
| LoadFromStorage<Schema, Q, UseDates>
|
|
355
|
+
| SetupError<Schema, Q, UseDates>;
|
|
356
|
+
|
|
357
|
+
export type SyncTableCallback<
|
|
358
|
+
Schema extends IContainEntitiesAndLinks<EntitiesDef, any>,
|
|
359
|
+
Q extends ValidQuery<Q, Schema>,
|
|
360
|
+
UseDates extends boolean,
|
|
361
|
+
> = (event: CallbackEvent<Schema, Q, UseDates>) => void;
|
|
362
|
+
|
|
363
|
+
export class SyncTable {
|
|
364
|
+
private trySend: TrySend;
|
|
365
|
+
private subs: PersistedObject<Subs>;
|
|
366
|
+
// Using any for the SyncCallback because we'd need Reactor to be typed
|
|
367
|
+
private callbacks: { [hash: string]: SyncTableCallback<any, any, any>[] } =
|
|
368
|
+
{};
|
|
369
|
+
private config: Config;
|
|
370
|
+
private idToHash: { [subscriptionId: string]: string } = {};
|
|
371
|
+
private log: Logger;
|
|
372
|
+
private createStore: (triples: Triple[]) => any;
|
|
373
|
+
|
|
374
|
+
constructor(
|
|
375
|
+
trySend: TrySend,
|
|
376
|
+
persister: Storage,
|
|
377
|
+
config: Config,
|
|
378
|
+
log: Logger,
|
|
379
|
+
createStore: (triples: Triple[]) => any,
|
|
380
|
+
) {
|
|
381
|
+
this.trySend = trySend;
|
|
382
|
+
this.config = config;
|
|
383
|
+
this.log = log;
|
|
384
|
+
this.createStore = createStore;
|
|
385
|
+
|
|
386
|
+
this.subs = new PersistedObject<Subs>(
|
|
387
|
+
persister,
|
|
388
|
+
'subs',
|
|
389
|
+
{},
|
|
390
|
+
onMergeSubs,
|
|
391
|
+
syncSubsToStorage,
|
|
392
|
+
(x) => syncSubsFromStorage(x, this.config.useDateObjects),
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
public beforeUnload() {
|
|
397
|
+
this.subs.flush();
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
public subscribe(
|
|
401
|
+
q: any,
|
|
402
|
+
cb: SyncTableCallback<any, any, any>,
|
|
403
|
+
): (
|
|
404
|
+
opts?: { keepSubscription?: boolean | null | undefined } | null | undefined,
|
|
405
|
+
) => void {
|
|
406
|
+
const hash = weakHash(q);
|
|
407
|
+
this.callbacks[hash] = this.callbacks[hash] || [];
|
|
408
|
+
this.callbacks[hash].push(cb);
|
|
409
|
+
|
|
410
|
+
this.initSubscription(q, hash, cb);
|
|
411
|
+
|
|
412
|
+
return (opts?: { keepSubscription?: boolean | null | undefined }) => {
|
|
413
|
+
this.unsubscribe(hash, cb, opts?.keepSubscription);
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
private unsubscribe(
|
|
418
|
+
hash: string,
|
|
419
|
+
cb: SyncTableCallback<any, any, any>,
|
|
420
|
+
keepSubscription: boolean | null | undefined,
|
|
421
|
+
) {
|
|
422
|
+
const cbs = (this.callbacks[hash] || []).filter((x) => x !== cb);
|
|
423
|
+
this.callbacks[hash] = cbs;
|
|
424
|
+
|
|
425
|
+
if (!cbs.length) {
|
|
426
|
+
delete this.callbacks[hash];
|
|
427
|
+
if (keepSubscription) {
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const sub = this.subs.currentValue[hash];
|
|
431
|
+
if (sub?.state) {
|
|
432
|
+
this.clearSubscriptionData(sub.state.subscriptionId);
|
|
433
|
+
}
|
|
434
|
+
this.subs.set((prev) => {
|
|
435
|
+
delete prev[hash];
|
|
436
|
+
return prev;
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
private sendStart(query: string) {
|
|
442
|
+
this.trySend(uuid(), {
|
|
443
|
+
op: 'start-sync',
|
|
444
|
+
q: query,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
private sendResync(sub: Sub, state: SubState) {
|
|
449
|
+
// Make sure we can find the hash from the subscriptionId
|
|
450
|
+
this.idToHash[state.subscriptionId] = sub.hash;
|
|
451
|
+
this.trySend(state.subscriptionId, {
|
|
452
|
+
op: 'resync-table',
|
|
453
|
+
'subscription-id': state.subscriptionId,
|
|
454
|
+
'tx-id': state.txId,
|
|
455
|
+
token: state.token,
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
private sendRemove(state: SubState) {
|
|
460
|
+
this.trySend(uuid(), {
|
|
461
|
+
op: 'remove-sync',
|
|
462
|
+
'subscription-id': state.subscriptionId,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
private async initSubscription(
|
|
467
|
+
query: any,
|
|
468
|
+
hash: string,
|
|
469
|
+
cb?: SyncTableCallback<any, any, any>,
|
|
470
|
+
) {
|
|
471
|
+
// Wait for storage to load so that we know if we already have an existing subscription
|
|
472
|
+
await this.subs.waitForLoaded();
|
|
473
|
+
const existingSub = this.subs.currentValue[hash];
|
|
474
|
+
|
|
475
|
+
if (existingSub && existingSub.state && existingSub.state.txId) {
|
|
476
|
+
this.sendResync(existingSub, existingSub.state);
|
|
477
|
+
|
|
478
|
+
if (existingSub.entities && cb) {
|
|
479
|
+
const k = Object.keys(query)[0];
|
|
480
|
+
cb({
|
|
481
|
+
type: CallbackEventType.LoadFromStorage,
|
|
482
|
+
data: subData(existingSub, existingSub.entities),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
const table = Object.keys(query)[0];
|
|
490
|
+
const orderBy = query[table]?.$?.order || { serverCreatedAt: 'asc' };
|
|
491
|
+
const [orderField, orderDirection] = Object.entries(orderBy)[0] as [
|
|
492
|
+
string,
|
|
493
|
+
'asc' | 'desc',
|
|
494
|
+
];
|
|
495
|
+
|
|
496
|
+
const orderFieldType =
|
|
497
|
+
orderField === 'serverCreatedAt'
|
|
498
|
+
? 'number'
|
|
499
|
+
: s.getAttrByFwdIdentName(this.createStore([]), table, orderField)?.[
|
|
500
|
+
'checked-data-type'
|
|
501
|
+
];
|
|
502
|
+
|
|
503
|
+
this.subs.set((prev) => {
|
|
504
|
+
prev[hash] = {
|
|
505
|
+
query,
|
|
506
|
+
hash: hash,
|
|
507
|
+
table,
|
|
508
|
+
orderDirection,
|
|
509
|
+
orderField,
|
|
510
|
+
orderFieldType,
|
|
511
|
+
createdAt: Date.now(),
|
|
512
|
+
updatedAt: Date.now(),
|
|
513
|
+
};
|
|
514
|
+
return prev;
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
this.sendStart(query);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
public async flushPending() {
|
|
521
|
+
await this.subs.waitForLoaded();
|
|
522
|
+
for (const hash of Object.keys(this.callbacks)) {
|
|
523
|
+
const sub = this.subs.currentValue[hash];
|
|
524
|
+
if (sub) {
|
|
525
|
+
await this.initSubscription(sub.query, sub.hash);
|
|
526
|
+
} else {
|
|
527
|
+
this.log.error('Missing sub for hash in flushPending', hash);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
public onStartSyncOk(msg: StartSyncOkMsg) {
|
|
533
|
+
const subscriptionId = msg['subscription-id'];
|
|
534
|
+
const q = msg.q;
|
|
535
|
+
const hash = weakHash(q);
|
|
536
|
+
|
|
537
|
+
this.idToHash[subscriptionId] = hash;
|
|
538
|
+
|
|
539
|
+
this.subs.set((prev) => {
|
|
540
|
+
const sub = prev[hash];
|
|
541
|
+
if (!sub) {
|
|
542
|
+
this.log.error(
|
|
543
|
+
'Missing sub for hash',
|
|
544
|
+
hash,
|
|
545
|
+
'subscription-id',
|
|
546
|
+
subscriptionId,
|
|
547
|
+
'query',
|
|
548
|
+
q,
|
|
549
|
+
);
|
|
550
|
+
return prev;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
sub.state = {
|
|
554
|
+
subscriptionId: subscriptionId,
|
|
555
|
+
token: msg.token,
|
|
556
|
+
};
|
|
557
|
+
return prev;
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
private notifyCbs(hash: string, event: CallbackEvent<any, any, any>) {
|
|
562
|
+
for (const cb of this.callbacks[hash] || []) {
|
|
563
|
+
cb(event);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
public onSyncLoadBatch(msg: SyncLoadBatchMsg) {
|
|
568
|
+
const subscriptionId = msg['subscription-id'];
|
|
569
|
+
const joinRows = msg['join-rows'];
|
|
570
|
+
const hash = this.idToHash[subscriptionId];
|
|
571
|
+
if (!hash) {
|
|
572
|
+
this.log.error('Missing hash for subscription', msg);
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const batch = [];
|
|
577
|
+
const currentValue = this.subs.set((prev) => {
|
|
578
|
+
const sub = prev[hash];
|
|
579
|
+
if (!sub) {
|
|
580
|
+
this.log.error('Missing sub for hash', hash, msg);
|
|
581
|
+
return prev;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
const entities = sub.entities ?? [];
|
|
585
|
+
sub.entities = entities;
|
|
586
|
+
|
|
587
|
+
for (const entRows of joinRows) {
|
|
588
|
+
const store = this.createStore(entRows);
|
|
589
|
+
const entity = queryEntity(sub, store);
|
|
590
|
+
entities.push({
|
|
591
|
+
store,
|
|
592
|
+
entity,
|
|
593
|
+
serverCreatedAt: getServerCreatedAt(sub, store, entity.id),
|
|
594
|
+
});
|
|
595
|
+
batch.push(entity);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
sub.updatedAt = Date.now();
|
|
599
|
+
|
|
600
|
+
return prev;
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
const sub = currentValue[hash];
|
|
604
|
+
if (sub.entities) {
|
|
605
|
+
this.notifyCbs(hash, {
|
|
606
|
+
type: CallbackEventType.InitialSyncBatch,
|
|
607
|
+
data: subData(sub, sub.entities),
|
|
608
|
+
batch,
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
public onSyncInitFinish(msg: SyncInitFinishMsg) {
|
|
614
|
+
const subscriptionId = msg['subscription-id'];
|
|
615
|
+
const hash = this.idToHash[subscriptionId];
|
|
616
|
+
if (!hash) {
|
|
617
|
+
this.log.error('Missing hash for subscription', msg);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
this.subs.set((prev) => {
|
|
621
|
+
const sub = prev[hash];
|
|
622
|
+
if (!sub) {
|
|
623
|
+
this.log.error('Missing sub for hash', hash, msg);
|
|
624
|
+
return prev;
|
|
625
|
+
}
|
|
626
|
+
const state = sub.state;
|
|
627
|
+
if (!state) {
|
|
628
|
+
this.log.error('Sub never set init, missing result', sub, msg);
|
|
629
|
+
return prev;
|
|
630
|
+
}
|
|
631
|
+
state.txId = msg['tx-id'];
|
|
632
|
+
sub.updatedAt = Date.now();
|
|
633
|
+
return prev;
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
const sub = this.subs.currentValue[hash];
|
|
637
|
+
|
|
638
|
+
if (sub) {
|
|
639
|
+
this.notifyCbs(hash, {
|
|
640
|
+
type: CallbackEventType.InitialSyncComplete,
|
|
641
|
+
data: subData(sub, sub.entities || []),
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
public onSyncUpdateTriples(msg: SyncUpdateTriplesMsg) {
|
|
647
|
+
const subscriptionId = msg['subscription-id'];
|
|
648
|
+
const hash = this.idToHash[subscriptionId];
|
|
649
|
+
if (!hash) {
|
|
650
|
+
this.log.error('Missing hash for subscription', msg);
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
this.subs.set((prev) => {
|
|
654
|
+
const sub = prev[hash];
|
|
655
|
+
if (!sub) {
|
|
656
|
+
this.log.error('Missing sub for hash', hash, msg);
|
|
657
|
+
return prev;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
const state = sub.state;
|
|
661
|
+
if (!state) {
|
|
662
|
+
this.log.error('Missing state for sub', sub, msg);
|
|
663
|
+
return prev;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
for (const tx of msg.txes) {
|
|
667
|
+
if (state.txId >= tx['tx-id']) {
|
|
668
|
+
continue;
|
|
669
|
+
}
|
|
670
|
+
state.txId = tx['tx-id'];
|
|
671
|
+
const idxesToDelete = [];
|
|
672
|
+
// Note: this won't work as well when links are involved
|
|
673
|
+
const byEid: {
|
|
674
|
+
[eid: string]: SyncUpdateTriplesMsg['txes'][number]['changes'];
|
|
675
|
+
} = {};
|
|
676
|
+
for (const change of tx.changes) {
|
|
677
|
+
const eidChanges = byEid[change.triple[0]] ?? [];
|
|
678
|
+
byEid[change.triple[0]] = eidChanges;
|
|
679
|
+
eidChanges.push(change);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
const entities = sub.entities || [];
|
|
683
|
+
sub.entities = entities;
|
|
684
|
+
|
|
685
|
+
const updated: SyncTransaction<any, any, any>['updated'] = [];
|
|
686
|
+
// Update the existing stores, if we already know about this entity
|
|
687
|
+
eidLoop: for (const [eid, changes] of Object.entries(byEid)) {
|
|
688
|
+
for (const [entIdx, ent] of Object.entries(entities)) {
|
|
689
|
+
if (s.hasEntity(ent.store, eid)) {
|
|
690
|
+
applyChangesToStore(ent.store, changes);
|
|
691
|
+
const entity = queryEntity(sub, ent.store);
|
|
692
|
+
const changedFields = changedFieldsOfChanges(ent.store, changes)[
|
|
693
|
+
eid
|
|
694
|
+
];
|
|
695
|
+
for (const [k, { oldValue, newValue }] of Object.entries(
|
|
696
|
+
changedFields || {},
|
|
697
|
+
)) {
|
|
698
|
+
if (oldValue === newValue) {
|
|
699
|
+
delete changedFields[k];
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (entity) {
|
|
703
|
+
updated.push({
|
|
704
|
+
oldEntity: ent.entity,
|
|
705
|
+
newEntity: entity,
|
|
706
|
+
changedFields: changedFields || {},
|
|
707
|
+
});
|
|
708
|
+
ent.entity = entity;
|
|
709
|
+
} else {
|
|
710
|
+
idxesToDelete.push(entIdx);
|
|
711
|
+
}
|
|
712
|
+
delete byEid[eid];
|
|
713
|
+
continue eidLoop;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
const added = [];
|
|
719
|
+
// If we have anything left in byEid, then this must be a new entity we don't know about
|
|
720
|
+
for (const [_eid, changes] of Object.entries(byEid)) {
|
|
721
|
+
const store = this.createStore([]);
|
|
722
|
+
applyChangesToStore(store, changes);
|
|
723
|
+
const entity = queryEntity(sub, store);
|
|
724
|
+
if (!entity) {
|
|
725
|
+
this.log.error('No entity found after applying change', {
|
|
726
|
+
sub,
|
|
727
|
+
changes,
|
|
728
|
+
store,
|
|
729
|
+
});
|
|
730
|
+
1;
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
entities.push({
|
|
734
|
+
store,
|
|
735
|
+
entity,
|
|
736
|
+
serverCreatedAt: getServerCreatedAt(sub, store, entity.id),
|
|
737
|
+
});
|
|
738
|
+
added.push(entity);
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const removed = [];
|
|
742
|
+
|
|
743
|
+
for (const idx of idxesToDelete.sort().reverse()) {
|
|
744
|
+
removed.push(entities[idx].entity);
|
|
745
|
+
entities.splice(idx, 1);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
sortEntitiesInPlace(sub, entities);
|
|
749
|
+
this.notifyCbs(hash, {
|
|
750
|
+
type: CallbackEventType.SyncTransaction,
|
|
751
|
+
data: subData(sub, sub.entities),
|
|
752
|
+
added,
|
|
753
|
+
removed,
|
|
754
|
+
updated,
|
|
755
|
+
});
|
|
756
|
+
sub.updatedAt = Date.now();
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
return prev;
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
private clearSubscriptionData(subscriptionId: string) {
|
|
764
|
+
const hash = this.idToHash[subscriptionId];
|
|
765
|
+
if (hash) {
|
|
766
|
+
delete this.idToHash[subscriptionId];
|
|
767
|
+
const sub = this.subs.currentValue[hash];
|
|
768
|
+
if (sub.state) {
|
|
769
|
+
this.sendRemove(sub.state);
|
|
770
|
+
}
|
|
771
|
+
this.subs.set((prev) => {
|
|
772
|
+
delete prev[hash];
|
|
773
|
+
return prev;
|
|
774
|
+
});
|
|
775
|
+
if (sub) {
|
|
776
|
+
return sub;
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
public onStartSyncError(msg: {
|
|
782
|
+
op: 'error';
|
|
783
|
+
'original-event': StartMsg;
|
|
784
|
+
'client-event-id': string;
|
|
785
|
+
status: number;
|
|
786
|
+
type: string;
|
|
787
|
+
message?: string;
|
|
788
|
+
hint?: any;
|
|
789
|
+
}) {
|
|
790
|
+
const hash = weakHash(msg['original-event']['q']);
|
|
791
|
+
const error = {
|
|
792
|
+
message: msg.message || 'Uh-oh, something went wrong. Ping Joe & Stopa.',
|
|
793
|
+
status: msg.status,
|
|
794
|
+
type: msg.type,
|
|
795
|
+
hint: msg.hint,
|
|
796
|
+
};
|
|
797
|
+
|
|
798
|
+
const k = Object.keys(msg['original-event']['q'])[0];
|
|
799
|
+
this.notifyCbs(hash, {
|
|
800
|
+
type: CallbackEventType.Error,
|
|
801
|
+
data: { [k]: [] },
|
|
802
|
+
error,
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
public onResyncError(msg: {
|
|
807
|
+
op: 'error';
|
|
808
|
+
'original-event': ResyncMsg;
|
|
809
|
+
status: number;
|
|
810
|
+
type: string;
|
|
811
|
+
}) {
|
|
812
|
+
// Clear the subscription and start from scrath on any resync error
|
|
813
|
+
// This can happen if the auth changed and we need to refetch with the
|
|
814
|
+
// new auth or if the subscription is too far behind.
|
|
815
|
+
const subscriptionId = msg['original-event']['subscription-id'];
|
|
816
|
+
const removedSub = this.clearSubscriptionData(subscriptionId);
|
|
817
|
+
if (removedSub) {
|
|
818
|
+
this.initSubscription(removedSub.query, removedSub.hash);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|