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