@iebh/tera-fy 2.0.21 → 2.2.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/CHANGELOG.md +38 -0
- package/api.md +68 -66
- package/dist/lib/projectFile.d.ts +182 -0
- package/dist/lib/projectFile.js +157 -0
- package/dist/lib/projectFile.js.map +1 -0
- package/dist/lib/syncro/entities.d.ts +28 -0
- package/dist/lib/syncro/entities.js +203 -0
- package/dist/lib/syncro/entities.js.map +1 -0
- package/dist/lib/syncro/keyed.d.ts +95 -0
- package/dist/lib/syncro/keyed.js +286 -0
- package/dist/lib/syncro/keyed.js.map +1 -0
- package/dist/lib/syncro/syncro.d.ts +328 -0
- package/dist/lib/syncro/syncro.js +633 -0
- package/dist/lib/syncro/syncro.js.map +1 -0
- package/dist/lib/terafy.bootstrapper.d.ts +42 -0
- package/dist/lib/terafy.bootstrapper.js +130 -0
- package/dist/lib/terafy.bootstrapper.js.map +1 -0
- package/dist/lib/terafy.client.d.ts +532 -0
- package/dist/lib/terafy.client.js +1110 -0
- package/dist/lib/terafy.client.js.map +1 -0
- package/dist/lib/terafy.proxy.d.ts +66 -0
- package/dist/lib/terafy.proxy.js +123 -0
- package/dist/lib/terafy.proxy.js.map +1 -0
- package/dist/lib/terafy.server.d.ts +607 -0
- package/dist/lib/terafy.server.js +1774 -0
- package/dist/lib/terafy.server.js.map +1 -0
- package/dist/plugin.vue2.es2019.js +30 -13
- package/dist/plugins/base.d.ts +20 -0
- package/dist/plugins/base.js +21 -0
- package/dist/plugins/base.js.map +1 -0
- package/dist/plugins/firebase.d.ts +62 -0
- package/dist/plugins/firebase.js +111 -0
- package/dist/plugins/firebase.js.map +1 -0
- package/dist/plugins/vite.d.ts +12 -0
- package/dist/plugins/vite.js +22 -0
- package/dist/plugins/vite.js.map +1 -0
- package/dist/plugins/vue2.d.ts +68 -0
- package/dist/plugins/vue2.js +96 -0
- package/dist/plugins/vue2.js.map +1 -0
- package/dist/plugins/vue3.d.ts +64 -0
- package/dist/plugins/vue3.js +96 -0
- package/dist/plugins/vue3.js.map +1 -0
- package/dist/terafy.bootstrapper.es2019.js +2 -2
- package/dist/terafy.bootstrapper.js +2 -2
- package/dist/terafy.es2019.js +2 -2
- package/dist/terafy.js +1 -1
- package/dist/utils/mixin.d.ts +11 -0
- package/dist/utils/mixin.js +15 -0
- package/dist/utils/mixin.js.map +1 -0
- package/dist/utils/pDefer.d.ts +12 -0
- package/dist/utils/pDefer.js +14 -0
- package/dist/utils/pDefer.js.map +1 -0
- package/dist/utils/pathTools.d.ts +70 -0
- package/dist/utils/pathTools.js +120 -0
- package/dist/utils/pathTools.js.map +1 -0
- package/eslint.config.js +44 -8
- package/lib/{projectFile.js → projectFile.ts} +83 -40
- package/lib/syncro/entities.ts +288 -0
- package/lib/syncro/{keyed.js → keyed.ts} +114 -57
- package/lib/syncro/{syncro.js → syncro.ts} +204 -169
- package/lib/{terafy.bootstrapper.js → terafy.bootstrapper.ts} +49 -31
- package/lib/{terafy.client.js → terafy.client.ts} +94 -86
- package/lib/{terafy.proxy.js → terafy.proxy.ts} +43 -16
- package/lib/{terafy.server.js → terafy.server.ts} +364 -223
- package/package.json +65 -26
- package/plugins/{base.js → base.ts} +3 -1
- package/plugins/{firebase.js → firebase.ts} +34 -16
- package/plugins/{vite.js → vite.ts} +3 -3
- package/plugins/{vue2.js → vue2.ts} +17 -10
- package/plugins/{vue3.js → vue3.ts} +11 -9
- package/tsconfig.json +30 -0
- package/utils/{mixin.js → mixin.ts} +1 -1
- package/utils/{pDefer.js → pDefer.ts} +10 -3
- package/utils/{pathTools.js → pathTools.ts} +11 -9
- package/lib/syncro/entities.js +0 -232
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
random,
|
|
5
5
|
sample,
|
|
6
6
|
throttle,
|
|
7
|
+
isEqual
|
|
7
8
|
} from 'lodash-es';
|
|
8
9
|
import {
|
|
9
10
|
doc as FirestoreDocRef,
|
|
@@ -11,10 +12,32 @@ import {
|
|
|
11
12
|
onSnapshot as FirestoreOnSnapshot,
|
|
12
13
|
setDoc as FirestoreSetDoc,
|
|
13
14
|
updateDoc as FirestoreUpdateDoc,
|
|
15
|
+
DocumentReference,
|
|
16
|
+
Firestore,
|
|
17
|
+
Unsubscribe,
|
|
14
18
|
} from 'firebase/firestore';
|
|
19
|
+
// @ts-ignore
|
|
15
20
|
import marshal from '@momsfriendlydevco/marshal';
|
|
16
21
|
import {nanoid} from 'nanoid';
|
|
17
22
|
import PromiseRetry from 'p-retry';
|
|
23
|
+
import {FirebaseApp} from 'firebase/app';
|
|
24
|
+
import { BoundSupabaseyFunction } from '@iebh/supabasey';
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
interface ReactiveWrapper<T = any> {
|
|
28
|
+
doc: T;
|
|
29
|
+
setState: (newState: T) => void;
|
|
30
|
+
getState: () => T;
|
|
31
|
+
watch: (cb: (newState: T) => void) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface PathSplitResult {
|
|
35
|
+
fsCollection: string;
|
|
36
|
+
fsId: string;
|
|
37
|
+
entity: string;
|
|
38
|
+
id: string;
|
|
39
|
+
relation?: string;
|
|
40
|
+
}
|
|
18
41
|
|
|
19
42
|
|
|
20
43
|
/**
|
|
@@ -28,9 +51,9 @@ export default class Syncro {
|
|
|
28
51
|
/**
|
|
29
52
|
* Firebase instance in use
|
|
30
53
|
*
|
|
31
|
-
* @type {
|
|
54
|
+
* @type {FirebaseApp}
|
|
32
55
|
*/
|
|
33
|
-
static firebase;
|
|
56
|
+
static firebase: FirebaseApp;
|
|
34
57
|
|
|
35
58
|
|
|
36
59
|
/**
|
|
@@ -38,15 +61,15 @@ export default class Syncro {
|
|
|
38
61
|
*
|
|
39
62
|
* @type {Firestore}
|
|
40
63
|
*/
|
|
41
|
-
static firestore;
|
|
64
|
+
static firestore: Firestore;
|
|
42
65
|
|
|
43
66
|
|
|
44
67
|
/**
|
|
45
|
-
*
|
|
68
|
+
* Supabasey instance in use
|
|
46
69
|
*
|
|
47
|
-
* @type {
|
|
70
|
+
* @type {Supabasey}
|
|
48
71
|
*/
|
|
49
|
-
static
|
|
72
|
+
static supabasey: BoundSupabaseyFunction;
|
|
50
73
|
|
|
51
74
|
|
|
52
75
|
/**
|
|
@@ -55,16 +78,16 @@ export default class Syncro {
|
|
|
55
78
|
*
|
|
56
79
|
* @type {String}
|
|
57
80
|
*/
|
|
58
|
-
static session;
|
|
81
|
+
static session: string | undefined;
|
|
59
82
|
|
|
60
83
|
|
|
61
84
|
/**
|
|
62
85
|
* OPTIONAL SyncroEntiries from './entiries.js' if its required
|
|
63
86
|
* This only gets populated if `config.forceLocalInit` is truthy and we've mounted at least one Syncro
|
|
64
87
|
*
|
|
65
|
-
* @type {
|
|
88
|
+
* @type {Record<string, any>}
|
|
66
89
|
*/
|
|
67
|
-
static SyncroEntities
|
|
90
|
+
static SyncroEntities: Record<string, any>;
|
|
68
91
|
|
|
69
92
|
|
|
70
93
|
/**
|
|
@@ -72,24 +95,24 @@ export default class Syncro {
|
|
|
72
95
|
*
|
|
73
96
|
* @type {String}
|
|
74
97
|
*/
|
|
75
|
-
path;
|
|
98
|
+
path: string;
|
|
76
99
|
|
|
77
100
|
|
|
78
101
|
/**
|
|
79
102
|
* The Firestore docHandle when calling various Firestore functions
|
|
80
103
|
*
|
|
81
|
-
* @type {
|
|
104
|
+
* @type {DocumentReference | undefined}
|
|
82
105
|
*/
|
|
83
|
-
docRef;
|
|
106
|
+
docRef: DocumentReference | undefined;
|
|
84
107
|
|
|
85
108
|
|
|
86
109
|
/**
|
|
87
110
|
* The reactive object managed by this Syncro instance
|
|
88
111
|
* The nature of this varies by framework and what `getReactive()` provides
|
|
89
112
|
*
|
|
90
|
-
* @type {
|
|
113
|
+
* @type {any}
|
|
91
114
|
*/
|
|
92
|
-
value;
|
|
115
|
+
value: any;
|
|
93
116
|
|
|
94
117
|
|
|
95
118
|
/**
|
|
@@ -109,9 +132,9 @@ export default class Syncro {
|
|
|
109
132
|
* @property {Object} context Additional named parameters to pass to callbacks like initState
|
|
110
133
|
*/
|
|
111
134
|
config = {
|
|
112
|
-
heartbeatInterval: 50_000, //~= 50s
|
|
113
|
-
syncroRegistryUrl: 'https://tera-tools.com/api/sync',
|
|
114
|
-
context: {},
|
|
135
|
+
heartbeatInterval: 50_000 as number, //~= 50s
|
|
136
|
+
syncroRegistryUrl: 'https://tera-tools.com/api/sync' as string,
|
|
137
|
+
context: {} as Record<string, any>,
|
|
115
138
|
};
|
|
116
139
|
|
|
117
140
|
|
|
@@ -133,7 +156,7 @@ export default class Syncro {
|
|
|
133
156
|
*
|
|
134
157
|
* @param {*...} [msg] The message to output
|
|
135
158
|
*/
|
|
136
|
-
debug(...msg) {} // eslint-disable-line no-unused-vars
|
|
159
|
+
debug(...msg: any[]) {} // eslint-disable-line no-unused-vars
|
|
137
160
|
|
|
138
161
|
|
|
139
162
|
/**
|
|
@@ -143,7 +166,7 @@ export default class Syncro {
|
|
|
143
166
|
*
|
|
144
167
|
* @param {*...} [msg] The message to output
|
|
145
168
|
*/
|
|
146
|
-
debugError(...msg) {
|
|
169
|
+
debugError(...msg: any[]) {
|
|
147
170
|
console.log(`[Syncro ${this.path}]`, ...msg);
|
|
148
171
|
}
|
|
149
172
|
|
|
@@ -154,7 +177,7 @@ export default class Syncro {
|
|
|
154
177
|
* @param {String} path Mount path for the Syncro. Should be in the form `${ENTITY}::${ID}(::${RELATION})?`
|
|
155
178
|
* @param {Object} [options] Additional instance setters (mutates instance directly), note that the `config` subkey is merged with the existing config rather than assigned
|
|
156
179
|
*/
|
|
157
|
-
constructor(path, options) {
|
|
180
|
+
constructor(path: string, options?: any) {
|
|
158
181
|
this.path = path;
|
|
159
182
|
Object.assign(this, {
|
|
160
183
|
...options,
|
|
@@ -175,7 +198,7 @@ export default class Syncro {
|
|
|
175
198
|
*
|
|
176
199
|
* @returns {Promise} A promise which resolves when the operation has completed
|
|
177
200
|
*/
|
|
178
|
-
destroy() {
|
|
201
|
+
destroy(): Promise<any[]> {
|
|
179
202
|
this.debug('Destroy!');
|
|
180
203
|
return Promise.all(this._destroyActions
|
|
181
204
|
.map(fn => fn())
|
|
@@ -188,9 +211,9 @@ export default class Syncro {
|
|
|
188
211
|
* Actions to preform when we are destroying this instance
|
|
189
212
|
* This is an array of function callbacks to execute in parallel when `destroy()` is called
|
|
190
213
|
*
|
|
191
|
-
* @type {Array<
|
|
214
|
+
* @type {Array<() => void>}
|
|
192
215
|
*/
|
|
193
|
-
_destroyActions = [];
|
|
216
|
+
_destroyActions: Array<() => void> = [];
|
|
194
217
|
|
|
195
218
|
|
|
196
219
|
/**
|
|
@@ -199,18 +222,18 @@ export default class Syncro {
|
|
|
199
222
|
*
|
|
200
223
|
* @param {Object} value Initial value of the reactive
|
|
201
224
|
*
|
|
202
|
-
* @returns {
|
|
225
|
+
* @returns {ReactiveWrapper} A reactive object prototype
|
|
203
226
|
* @property {Object} doc The reactive object
|
|
204
227
|
* @property {Function} setState Function used to overwrite the default state, called as `(newState:Object)`
|
|
205
228
|
* @property {Function} getState Function used to fetch the current snapshot state, called as `()`
|
|
206
229
|
* @property {Function} watch Function used to set up state watchers, should call its callback when a change is detected, called as `(cb:Function)`
|
|
207
230
|
*/
|
|
208
|
-
getReactive(value) {
|
|
231
|
+
getReactive(value: any): ReactiveWrapper {
|
|
209
232
|
console.warn('Syncro.getReactive has not been subclassed, assuming a POJO response');
|
|
210
|
-
let doc = {...value};
|
|
233
|
+
let doc: Record<string, any> = {...value};
|
|
211
234
|
return {
|
|
212
235
|
doc,
|
|
213
|
-
setState(state) {
|
|
236
|
+
setState(state: any) {
|
|
214
237
|
// Shallow copy all sub keys into existing object (keeping the object pointer)
|
|
215
238
|
Object.entries(state || {})
|
|
216
239
|
.forEach(([k, v]) => doc[k] = v)
|
|
@@ -218,7 +241,7 @@ export default class Syncro {
|
|
|
218
241
|
getState() {
|
|
219
242
|
return cloneDeep(doc);
|
|
220
243
|
},
|
|
221
|
-
watch(cb) { // eslint-disable-line no-unused-vars
|
|
244
|
+
watch(cb: (newState: any) => void) { // eslint-disable-line no-unused-vars
|
|
222
245
|
// Stub
|
|
223
246
|
},
|
|
224
247
|
};
|
|
@@ -237,14 +260,9 @@ export default class Syncro {
|
|
|
237
260
|
* @param {Object} [options] Additional options to mutate behaviour
|
|
238
261
|
* @param {Boolean} [options.allowAsterisk=false] Whether to allow the meta asterisk character when recognising paths, this is used by the SyncroKeyed class
|
|
239
262
|
*
|
|
240
|
-
* @returns {
|
|
241
|
-
* @property {String} fbEntity The top level Firebase collection to store within
|
|
242
|
-
* @property {String} fsId The top level Firebase ID of the collection to store as, this is either just a copy of the ID or a combination of id + relation
|
|
243
|
-
* @property {String} entity A valid entity name (in plural form e.g. 'projects')
|
|
244
|
-
* @property {String} id A valid UUID ID
|
|
245
|
-
* @property {String} [relation] A string representing a sub-relationship. Usually a short string alias
|
|
263
|
+
* @returns {PathSplitResult} An object composed of the session path components
|
|
246
264
|
*/
|
|
247
|
-
static pathSplit(path, options) {
|
|
265
|
+
static pathSplit(path: string, options?: any): PathSplitResult {
|
|
248
266
|
let settings = {
|
|
249
267
|
allowAsterisk: false,
|
|
250
268
|
...options,
|
|
@@ -264,10 +282,10 @@ export default class Syncro {
|
|
|
264
282
|
+ '$'
|
|
265
283
|
);
|
|
266
284
|
|
|
267
|
-
let extracted = { ...pathMatcher.exec(path)?.groups };
|
|
285
|
+
let extracted = { ...pathMatcher.exec(path)?.groups } as { entity?: string, id?: string, relation?: string };
|
|
268
286
|
|
|
269
|
-
if (!extracted) throw new Error(`Invalid session path syntax "${path}"`);
|
|
270
|
-
if (Syncro.SyncroEntities && !(extracted.entity in SyncroEntities)) throw new Error(`Unsupported entity "${path}" -> Entity="${extracted.entity}"`);
|
|
287
|
+
if (!extracted || !extracted.entity || !extracted.id) throw new Error(`Invalid session path syntax "${path}"`);
|
|
288
|
+
if (Syncro.SyncroEntities && !(extracted.entity in Syncro.SyncroEntities)) throw new Error(`Unsupported entity "${path}" -> Entity="${extracted.entity}"`);
|
|
271
289
|
|
|
272
290
|
return {
|
|
273
291
|
entity: extracted.entity,
|
|
@@ -291,7 +309,7 @@ export default class Syncro {
|
|
|
291
309
|
* @param {Object} snapshot The current state to convert
|
|
292
310
|
* @returns {Object} A Firebase compatible object
|
|
293
311
|
*/
|
|
294
|
-
static toFirestore(snapshot = {}) {
|
|
312
|
+
static toFirestore(snapshot: any = {}): any {
|
|
295
313
|
return marshal.serialize(snapshot, {
|
|
296
314
|
circular: false,
|
|
297
315
|
clone: true, // Clone away from the original Vue Reactive so we dont mangle it while traversing
|
|
@@ -311,7 +329,7 @@ export default class Syncro {
|
|
|
311
329
|
* @param {Object} snapshot The raw Firebase state to convert
|
|
312
330
|
* @returns {Object} A JavaScript POJO representing the converted state
|
|
313
331
|
*/
|
|
314
|
-
static fromFirestore(snapshot = {}) {
|
|
332
|
+
static fromFirestore(snapshot: any = {}): any {
|
|
315
333
|
return marshal.deserialize(snapshot, {
|
|
316
334
|
circular: false,
|
|
317
335
|
clone: true, // Clone away from original so we don't trigger a loop within Firebase
|
|
@@ -333,8 +351,8 @@ export default class Syncro {
|
|
|
333
351
|
* @param {Object} data The raw value to convert
|
|
334
352
|
* @returns {Object} A Firestore compatible, typed data structure
|
|
335
353
|
*/
|
|
336
|
-
static toFirestoreFields(data) {
|
|
337
|
-
const result = {};
|
|
354
|
+
static toFirestoreFields(data: any): Record<string, any> {
|
|
355
|
+
const result: Record<string, any> = {};
|
|
338
356
|
|
|
339
357
|
for (const [key, value] of Object.entries(data)) {
|
|
340
358
|
const type = typeof value;
|
|
@@ -348,9 +366,11 @@ export default class Syncro {
|
|
|
348
366
|
} else if (value === null) {
|
|
349
367
|
result[key] = { nullValue: null };
|
|
350
368
|
} else if (Array.isArray(value)) {
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
369
|
+
// Need to handle the inner item structure correctly
|
|
370
|
+
result[key] = { arrayValue: { values: value.map(item => {
|
|
371
|
+
const field = Syncro.toFirestoreFields({ item });
|
|
372
|
+
return field.item; // Extract the typed value from the temporary {item: ...} structure
|
|
373
|
+
}) } };
|
|
354
374
|
} else if (type === 'object') {
|
|
355
375
|
result[key] = { mapValue: { fields: Syncro.toFirestoreFields(value) } };
|
|
356
376
|
}
|
|
@@ -369,26 +389,25 @@ export default class Syncro {
|
|
|
369
389
|
* @param {Object} fields The raw Snapshot to convert
|
|
370
390
|
* @returns {Object} A JavaScript POJO representing the converted state
|
|
371
391
|
*/
|
|
372
|
-
static fromFirestoreFields(fields = {}) {
|
|
373
|
-
let result = {};
|
|
392
|
+
static fromFirestoreFields(fields: any = {}): any {
|
|
393
|
+
let result: Record<string, any> = {};
|
|
374
394
|
for (let key in fields) {
|
|
375
395
|
let value = fields[key];
|
|
376
396
|
let isDocumentType = [
|
|
377
397
|
'stringValue', 'booleanValue', 'doubleValue',
|
|
378
|
-
'integerValue', 'timestampValue', 'mapValue', 'arrayValue', 'nullValue',
|
|
379
|
-
].find(t => t ===
|
|
398
|
+
'integerValue', 'timestampValue', 'mapValue', 'arrayValue', 'nullValue', // Added nullValue
|
|
399
|
+
].find(t => t === Object.keys(value)[0]); // Check the first key of the value object
|
|
380
400
|
|
|
381
401
|
if (isDocumentType) {
|
|
382
|
-
|
|
383
|
-
.
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
} else if ('
|
|
388
|
-
|
|
389
|
-
} else
|
|
390
|
-
|
|
391
|
-
return !!list ? list.map(l => Syncro.fromFirestoreFields(l)) : [];
|
|
402
|
+
if (isDocumentType === 'mapValue') {
|
|
403
|
+
result[key] = Syncro.fromFirestoreFields(value.mapValue.fields || {});
|
|
404
|
+
} else if (isDocumentType === 'arrayValue') {
|
|
405
|
+
let list = value.arrayValue.values;
|
|
406
|
+
result[key] = !!list ? list.map((l: any) => Syncro.fromFirestoreFields(l)) : [];
|
|
407
|
+
} else if (isDocumentType === 'nullValue') {
|
|
408
|
+
result[key] = null;
|
|
409
|
+
} else {
|
|
410
|
+
result[key] = value[isDocumentType];
|
|
392
411
|
}
|
|
393
412
|
} else {
|
|
394
413
|
// This case might not be standard Firestore field structure, but handle recursively
|
|
@@ -406,14 +425,14 @@ export default class Syncro {
|
|
|
406
425
|
*
|
|
407
426
|
* @returns {Promise<Object|Null>} An eventual snapshot of the given path, if the entity doesn't exist null is returned
|
|
408
427
|
*/
|
|
409
|
-
static getSnapshot(path) {
|
|
428
|
+
static getSnapshot(path: string): Promise<any | null> {
|
|
410
429
|
let {fsCollection, fsId} = Syncro.pathSplit(path);
|
|
411
430
|
|
|
412
431
|
return Promise.resolve()
|
|
413
432
|
.then(async ()=> FirestoreGetDoc( // Set up binding and wait for it to come ready
|
|
414
433
|
FirestoreDocRef(Syncro.firestore, fsCollection, fsId)
|
|
415
434
|
))
|
|
416
|
-
.then(doc => doc
|
|
435
|
+
.then(doc => doc.exists() // Use exists() method
|
|
417
436
|
? Syncro.fromFirestore(doc.data())
|
|
418
437
|
: null
|
|
419
438
|
)
|
|
@@ -431,20 +450,23 @@ export default class Syncro {
|
|
|
431
450
|
*
|
|
432
451
|
* @returns {Promise<*>} The state object after it has been applied
|
|
433
452
|
*/
|
|
434
|
-
static setSnapshot(path, state, options) {
|
|
453
|
+
static setSnapshot(path: string, state: any, options?: { method?: 'merge' | 'set' }): Promise<any> {
|
|
435
454
|
let settings = {
|
|
436
455
|
method: 'merge',
|
|
437
456
|
...options,
|
|
438
457
|
};
|
|
439
458
|
let {fsCollection, fsId} = Syncro.pathSplit(path);
|
|
459
|
+
const docRef = FirestoreDocRef(Syncro.firestore, fsCollection, fsId);
|
|
460
|
+
const firestoreData = Syncro.toFirestore(state);
|
|
440
461
|
|
|
441
462
|
return Promise.resolve()
|
|
442
|
-
.then(()=>
|
|
443
|
-
(settings.method
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
463
|
+
.then(()=> {
|
|
464
|
+
if (settings.method === 'merge') {
|
|
465
|
+
return FirestoreUpdateDoc(docRef, firestoreData);
|
|
466
|
+
} else { // method === 'set'
|
|
467
|
+
return FirestoreSetDoc(docRef, firestoreData); // Default set overwrites
|
|
468
|
+
}
|
|
469
|
+
})
|
|
448
470
|
.then(()=> state)
|
|
449
471
|
}
|
|
450
472
|
|
|
@@ -457,7 +479,7 @@ export default class Syncro {
|
|
|
457
479
|
* @param {Number} [options.retries=3] Number of times to retry if a mounted Syncro fails its sanity checks
|
|
458
480
|
* @returns {Promise<Syncro>} A promise which resolves as this syncro instance when completed
|
|
459
481
|
*/
|
|
460
|
-
mount(options) {
|
|
482
|
+
mount(options?: any): Promise<Syncro> {
|
|
461
483
|
let settings = {
|
|
462
484
|
initialState: null,
|
|
463
485
|
retries: 5,
|
|
@@ -466,68 +488,68 @@ export default class Syncro {
|
|
|
466
488
|
};
|
|
467
489
|
|
|
468
490
|
let {fsCollection, fsId, entity} = Syncro.pathSplit(this.path);
|
|
469
|
-
let reactive; // Eventual response from reactive() with the intitial value
|
|
470
|
-
let doc; // Eventual Firebase document
|
|
491
|
+
let reactive: ReactiveWrapper; // Eventual response from reactive() with the intitial value
|
|
492
|
+
let doc: any; // Eventual Firebase document
|
|
471
493
|
|
|
472
494
|
return PromiseRetry(
|
|
473
|
-
()=>
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
495
|
+
async (): Promise<Syncro> => { // Added async here for await
|
|
496
|
+
await this.setHeartbeat(false); // Disable any existing heartbeat - this only really applies if we're changing path for some reason
|
|
497
|
+
|
|
498
|
+
// Set up binding and wait for it to come ready
|
|
499
|
+
this.docRef = FirestoreDocRef(Syncro.firestore, fsCollection, fsId);
|
|
500
|
+
|
|
501
|
+
// Initalize state
|
|
502
|
+
let initialState = await this.getFirestoreState();
|
|
503
|
+
|
|
504
|
+
// Construct a reactive component
|
|
505
|
+
reactive = this.getReactive(initialState);
|
|
506
|
+
if (!reactive.doc || !reactive.setState || !reactive.getState || !reactive.watch) throw new Error('Syncro.getReactive() requires a returned `doc`, `setState()`, `getState()` + `watch()`');
|
|
507
|
+
this.value = doc = reactive.doc;
|
|
508
|
+
|
|
509
|
+
this.debug('Initial state', {doc});
|
|
510
|
+
|
|
511
|
+
// Subscribe to remote updates
|
|
512
|
+
const unsubscribe: Unsubscribe = FirestoreOnSnapshot(this.docRef, snapshot => {
|
|
513
|
+
let snapshotData = Syncro.fromFirestore(snapshot.data());
|
|
514
|
+
this.debug('Incoming snapshot', {snapshotData});
|
|
515
|
+
reactive.setState(snapshotData);
|
|
516
|
+
});
|
|
517
|
+
this._destroyActions.push(unsubscribe); // Add the unsubscribe handle to the list of destroyAction promises we call on `destroy()`
|
|
518
|
+
|
|
519
|
+
// Optionally create the doc if it has no content
|
|
520
|
+
if (!isEmpty(doc)) { // Doc already has content - skip
|
|
521
|
+
// Do nothing
|
|
522
|
+
} else if (settings.initialState) { // Provided an intiailState - use that instead of the entities own method
|
|
523
|
+
this.debug('Populate initial Syncro state (from provided initialState)');
|
|
524
|
+
await this.setFirestoreState(settings.initialState, {method: 'set'});
|
|
525
|
+
} else {
|
|
526
|
+
this.debug(`Populate initial Syncro state (from "${entity}" Syncro worker)`);
|
|
527
|
+
const response = await fetch(`${this.config.syncroRegistryUrl}/${this.path}`);
|
|
528
|
+
if (!response.ok) {
|
|
529
|
+
throw new Error(`Failed to check Syncro "${fsCollection}::${fsId}" status - ${response.statusText}`);
|
|
508
530
|
}
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
531
|
+
// Assuming the fetch populates the syncro state server-side, no local state set needed here
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Setup local state watcher
|
|
535
|
+
reactive.watch(throttle((newState: any) => {
|
|
536
|
+
this.debug('Local change', {newState});
|
|
537
|
+
this.markDirty();
|
|
538
|
+
this.setFirestoreState(newState, {method: 'merge'});
|
|
539
|
+
}, this.throttle));
|
|
540
|
+
|
|
541
|
+
await this.setHeartbeat(true, {
|
|
518
542
|
immediate: true,
|
|
519
|
-
})
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
throw e;
|
|
524
|
-
}),
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
return this;
|
|
546
|
+
},
|
|
525
547
|
{ // PromiseRetry / p-retry options
|
|
526
548
|
retries: settings.retries,
|
|
527
549
|
minTimeout: settings.retryMinTime,
|
|
528
550
|
randomize: true,
|
|
529
551
|
factor: 3,
|
|
530
|
-
onFailedAttempt: async (e) => {
|
|
552
|
+
onFailedAttempt: async (e: any) => {
|
|
531
553
|
this.debugError(`[Attempt ${e.attemptNumber}/${e.attemptNumber + e.retriesLeft - 1}] to mount syncro`, e);
|
|
532
554
|
await this.destroy(); // Ensure cleanup on failed attempt before retry
|
|
533
555
|
},
|
|
@@ -535,24 +557,28 @@ export default class Syncro {
|
|
|
535
557
|
);
|
|
536
558
|
}
|
|
537
559
|
|
|
538
|
-
|
|
539
560
|
/**
|
|
540
561
|
* Merge a single or multiple values into a Syncro data object
|
|
562
|
+
* NOTE: Default behaviour is to flush (if any changes apply), use direct object mutation or disable with `flush:false` to disable
|
|
541
563
|
*
|
|
542
564
|
* @param {String|Object} key Either the single named key to set OR the object to merge
|
|
543
565
|
* @param {*} [value] The value to set if `key` is a string
|
|
544
566
|
*
|
|
545
567
|
* @param {Object} [options] Additional options to mutate behaviour
|
|
546
568
|
* @param {Boolean} [options.delta=true] Only merge keys that differ, skipping flush if no changes are made
|
|
547
|
-
* @param {Boolean} [options.flush=
|
|
569
|
+
* @param {Boolean} [options.flush=true] Send a flush signal that Firebase should sync to Supabase on changes
|
|
548
570
|
* @param {Boolean} [options.forceFlush=false] Flush even if no changes were made
|
|
549
571
|
* @param {Boolean} [options.flushDestroy=false] Destroy the Syncro after flushing
|
|
550
572
|
*
|
|
551
573
|
* @returns {Promise<Syncro>} A promise which resolves with this Syncro instance on completion
|
|
552
574
|
*/
|
|
553
|
-
async set(
|
|
575
|
+
async set(
|
|
576
|
+
key: string | object,
|
|
577
|
+
value: any,
|
|
578
|
+
options: { delta?: boolean, flush?: boolean, forceFlush?: boolean, flushDestroy?: boolean }
|
|
579
|
+
) {
|
|
554
580
|
// Argument mangling - [key, value, settings] -> changes{}, settings {{{
|
|
555
|
-
let changes;
|
|
581
|
+
let changes: any;
|
|
556
582
|
if (typeof key == 'string') { // Called as (key:String, value:*, options?:Object)
|
|
557
583
|
changes[key] = value;
|
|
558
584
|
} else if (typeof key == 'object') { // Called as (changes:Object, options?:Object)
|
|
@@ -564,7 +590,7 @@ export default class Syncro {
|
|
|
564
590
|
|
|
565
591
|
let settings = {
|
|
566
592
|
delta: true,
|
|
567
|
-
flush:
|
|
593
|
+
flush: true,
|
|
568
594
|
forceFlush: false,
|
|
569
595
|
flushDestroy: false,
|
|
570
596
|
...options,
|
|
@@ -595,7 +621,6 @@ export default class Syncro {
|
|
|
595
621
|
return this;
|
|
596
622
|
}
|
|
597
623
|
|
|
598
|
-
|
|
599
624
|
/**
|
|
600
625
|
* Schedule Syncro heartbeats
|
|
601
626
|
* This populates the `sync` presence meta-information
|
|
@@ -605,7 +630,7 @@ export default class Syncro {
|
|
|
605
630
|
* @param {Object} [options] Additional options to mutate behaviour
|
|
606
631
|
* @param {Boolean} [options.immediate=false] Fire a heartbeat as soon as this function is called, this is only really useful on mount
|
|
607
632
|
*/
|
|
608
|
-
setHeartbeat(enable = true, options) {
|
|
633
|
+
setHeartbeat(enable: boolean = true, options?: any): Promise<void> | void { // Return type adjusted
|
|
609
634
|
let settings = {
|
|
610
635
|
immediate: true,
|
|
611
636
|
...options,
|
|
@@ -615,15 +640,17 @@ export default class Syncro {
|
|
|
615
640
|
clearTimeout(this._heartbeatTimer);
|
|
616
641
|
|
|
617
642
|
if (enable) {
|
|
618
|
-
|
|
643
|
+
const heartbeatAction = async () => {
|
|
619
644
|
// Perform the heartbeat
|
|
620
645
|
await this.heartbeat();
|
|
621
646
|
|
|
622
647
|
// If we're enabled - schedule the next heartbeat timer
|
|
623
|
-
if (enable) this.setHeartbeat(true);
|
|
624
|
-
}
|
|
648
|
+
if (enable) this.setHeartbeat(true); // Reschedule
|
|
649
|
+
};
|
|
625
650
|
|
|
626
|
-
|
|
651
|
+
this._heartbeatTimer = setTimeout(heartbeatAction, this.config.heartbeatInterval);
|
|
652
|
+
|
|
653
|
+
if (settings.immediate) return this.heartbeat(); // Return the promise from immediate heartbeat
|
|
627
654
|
}
|
|
628
655
|
}
|
|
629
656
|
|
|
@@ -634,24 +661,29 @@ export default class Syncro {
|
|
|
634
661
|
*
|
|
635
662
|
* @returns {Promise} A promise which resolves when the operation has completed
|
|
636
663
|
*/
|
|
637
|
-
async heartbeat() {
|
|
664
|
+
async heartbeat(): Promise<void> {
|
|
638
665
|
this.debug('heartbeat!');
|
|
639
666
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
667
|
+
try {
|
|
668
|
+
const response = await fetch(`${this.config.syncroRegistryUrl}/${this.path}/heartbeat`, {
|
|
669
|
+
method: 'post',
|
|
670
|
+
headers: {
|
|
671
|
+
'Content-Type': 'application/json'
|
|
672
|
+
},
|
|
673
|
+
body: JSON.stringify({
|
|
674
|
+
session: Syncro.session,
|
|
675
|
+
...(this.isDirty && {dirty: true}),
|
|
676
|
+
}),
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
if (!response.ok) {
|
|
680
|
+
console.warn(this.path, `Heartbeat failed - ${response.statusText}`, {response});
|
|
681
|
+
}
|
|
682
|
+
this.isDirty = false; // Reset the dirty flag if it is set
|
|
683
|
+
} catch (error) {
|
|
684
|
+
console.warn(this.path, 'Heartbeat fetch error', error);
|
|
685
|
+
// Decide if isDirty should be reset on network error
|
|
686
|
+
}
|
|
655
687
|
}
|
|
656
688
|
|
|
657
689
|
|
|
@@ -664,17 +696,20 @@ export default class Syncro {
|
|
|
664
696
|
*
|
|
665
697
|
* @returns {Promise} A promise which resolves when the operation has completed
|
|
666
698
|
*/
|
|
667
|
-
setFirestoreState(state, options) {
|
|
699
|
+
setFirestoreState(state: any, options?: { method?: 'merge' | 'set' }): Promise<void> {
|
|
668
700
|
let settings = {
|
|
669
701
|
method: 'merge',
|
|
670
702
|
...options,
|
|
671
703
|
};
|
|
672
704
|
if (!this.docRef) throw new Error('mount() must be called before setting Firestore state');
|
|
673
705
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
706
|
+
const firestoreData = Syncro.toFirestore(state);
|
|
707
|
+
|
|
708
|
+
if (settings.method === 'merge') {
|
|
709
|
+
return FirestoreUpdateDoc(this.docRef, firestoreData);
|
|
710
|
+
} else { // method === 'set'
|
|
711
|
+
return FirestoreSetDoc(this.docRef, firestoreData);
|
|
712
|
+
}
|
|
678
713
|
}
|
|
679
714
|
|
|
680
715
|
|
|
@@ -684,7 +719,7 @@ export default class Syncro {
|
|
|
684
719
|
*
|
|
685
720
|
* @returns {Promise<Object>} A promise which resolves to the Firestore state
|
|
686
721
|
*/
|
|
687
|
-
getFirestoreState() {
|
|
722
|
+
getFirestoreState(): Promise<any> {
|
|
688
723
|
if (!this.docRef) throw new Error('mount() must be called before getting Firestore state');
|
|
689
724
|
|
|
690
725
|
return FirestoreGetDoc(this.docRef)
|
|
@@ -698,7 +733,7 @@ export default class Syncro {
|
|
|
698
733
|
* @see isDirty
|
|
699
734
|
* @returns {Syncro} This chainable Syncro instance
|
|
700
735
|
*/
|
|
701
|
-
markDirty() {
|
|
736
|
+
markDirty(): this {
|
|
702
737
|
this.isDirty = true;
|
|
703
738
|
return this;
|
|
704
739
|
}
|
|
@@ -713,7 +748,7 @@ export default class Syncro {
|
|
|
713
748
|
*
|
|
714
749
|
* @returns {Promise} A promise which resolves when the operation has completed
|
|
715
750
|
*/
|
|
716
|
-
flush(options) {
|
|
751
|
+
flush(options?: any): Promise<void | null> {
|
|
717
752
|
let settings = {
|
|
718
753
|
destroy: false,
|
|
719
754
|
...options,
|
|
@@ -723,16 +758,16 @@ export default class Syncro {
|
|
|
723
758
|
.then(response => response.ok
|
|
724
759
|
? null
|
|
725
760
|
: Promise.reject(response.statusText || 'An error occured')
|
|
726
|
-
)
|
|
761
|
+
);
|
|
727
762
|
}
|
|
728
763
|
|
|
729
764
|
|
|
730
765
|
/**
|
|
731
766
|
* Timer handle for heartbeats
|
|
732
767
|
*
|
|
733
|
-
* @type {
|
|
768
|
+
* @type {any}
|
|
734
769
|
*/
|
|
735
|
-
_heartbeatTimer;
|
|
770
|
+
_heartbeatTimer: any; // Using any for simplicity with NodeJS.Timeout / number
|
|
736
771
|
}
|
|
737
772
|
|
|
738
773
|
|
|
@@ -744,7 +779,7 @@ export default class Syncro {
|
|
|
744
779
|
*
|
|
745
780
|
* @returns {*} The current branch conotents
|
|
746
781
|
*/
|
|
747
|
-
export function randomBranch(depth = 0) {
|
|
782
|
+
export function randomBranch(depth: number = 0): any {
|
|
748
783
|
let dice = // Roll a dice to pick the content
|
|
749
784
|
depth == 0 ? 10 // first roll is always '10'
|
|
750
785
|
: random(0, 11 - depth, false); // Subsequent rolls bias downwards based on depth (to avoid recursion)
|
|
@@ -754,12 +789,12 @@ export function randomBranch(depth = 0) {
|
|
|
754
789
|
: dice == 1 ? true
|
|
755
790
|
: dice == 2 ? random(1, 10_000)
|
|
756
791
|
: dice == 3 ? (new Date(random(1_000_000_000_000, 1_777_777_777_777))).toISOString()
|
|
757
|
-
: dice == 5 ? Array.from({length: random(1, 10)}, ()=> random(1, 10))
|
|
792
|
+
: dice == 5 ? Array.from({length: random(1, 10)}, (): number => random(1, 10)) // Added return type hint
|
|
758
793
|
: dice == 6 ? null
|
|
759
|
-
: dice < 8 ? Array.from({length: random(1, 10)}, ()=> randomBranch(depth+1))
|
|
794
|
+
: dice < 8 ? Array.from({length: random(1, 10)}, (): any => randomBranch(depth+1)) // Added return type hint
|
|
760
795
|
: Object.fromEntries(
|
|
761
796
|
Array.from({length: random(1, 5)})
|
|
762
|
-
.map((v, k) => [
|
|
797
|
+
.map((v, k): [string, any] => [ // Added return type hint
|
|
763
798
|
sample(['foo', 'bar', 'baz', 'quz', 'flarp', 'corge', 'grault', 'garply', 'waldo', 'fred', 'plugh', 'thud'])
|
|
764
799
|
+ `_${k}`,
|
|
765
800
|
randomBranch(depth+1),
|
|
@@ -776,14 +811,14 @@ export function randomBranch(depth = 0) {
|
|
|
776
811
|
const marshalFlattenArrays = {
|
|
777
812
|
id: `~array`,
|
|
778
813
|
recursive: true,
|
|
779
|
-
test: v => Array.isArray(v),
|
|
780
|
-
serialize: v => ({_: '~array', ...v}),
|
|
781
|
-
deserialize: v => {
|
|
782
|
-
let arr = Array.from({length: Object.keys(v).length - 1});
|
|
814
|
+
test: (v: any): boolean => Array.isArray(v),
|
|
815
|
+
serialize: (v: any): any => ({_: '~array', ...v}),
|
|
816
|
+
deserialize: (v: any): any[] => {
|
|
817
|
+
let arr: any[] = Array.from({length: Object.keys(v).length - 1});
|
|
783
818
|
|
|
784
819
|
Object.entries(v)
|
|
785
820
|
.filter(([k]) => k !== '_')
|
|
786
|
-
.forEach(([k,
|
|
821
|
+
.forEach(([k, val]) => arr[+k] = val); // Changed v to val to avoid shadowing
|
|
787
822
|
|
|
788
823
|
return arr;
|
|
789
824
|
},
|