@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
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import Reflib from '@iebh/reflib';
|
|
3
|
+
import {v4 as uuid4} from 'uuid';
|
|
4
|
+
import {nanoid} from 'nanoid';
|
|
5
|
+
import { BoundSupabaseyFunction } from '@iebh/supabasey';
|
|
6
|
+
|
|
7
|
+
// DATABASE TABLE TYPE DEFINITIONS {{{
|
|
8
|
+
interface ProjectRow {
|
|
9
|
+
data: {
|
|
10
|
+
temp?: { [key: string]: any };
|
|
11
|
+
// TODO: add other properties in project data
|
|
12
|
+
};
|
|
13
|
+
// TODO: Define other columns in project table
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface UserRow {
|
|
17
|
+
id: string;
|
|
18
|
+
data: {
|
|
19
|
+
id: string;
|
|
20
|
+
credits: number;
|
|
21
|
+
[key: string]: any;
|
|
22
|
+
// TODO: add other properties in user data
|
|
23
|
+
};
|
|
24
|
+
// TODO: Define other columns in user table
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface NamespaceRow {
|
|
28
|
+
project: string;
|
|
29
|
+
name: string;
|
|
30
|
+
data: {
|
|
31
|
+
[key: string]: any;
|
|
32
|
+
// TODO: add other properties in namespace data
|
|
33
|
+
};
|
|
34
|
+
// TODO: Define other namespace in user table
|
|
35
|
+
}
|
|
36
|
+
// }}}
|
|
37
|
+
|
|
38
|
+
// Interface for each syncroconfic
|
|
39
|
+
interface SyncroEntityConfig {
|
|
40
|
+
singular: string;
|
|
41
|
+
initState: (args: {
|
|
42
|
+
supabasey: BoundSupabaseyFunction;
|
|
43
|
+
id: string; // Primary ID for the entity
|
|
44
|
+
relation?: string; // Optional relation identifier (for namespaces, libraries)
|
|
45
|
+
}) => Promise<any>;
|
|
46
|
+
|
|
47
|
+
flushState: (args: {
|
|
48
|
+
supabasey: BoundSupabaseyFunction;
|
|
49
|
+
state: any; // The state object to flush
|
|
50
|
+
id?: string; // Primary ID (used in some lookups like namespaces)
|
|
51
|
+
fsId?: string; // ID often passed to Supabase RPCs (might be same as 'id')
|
|
52
|
+
relation?: string; // Optional relation identifier
|
|
53
|
+
}) => Promise<any>; // Return type signifies completion/result of flush
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
type SyncroConfig = Record<string, SyncroEntityConfig>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Entities we support Syncro paths for, each should correspond directly with a Firebase/Firestore collection name
|
|
60
|
+
*
|
|
61
|
+
* @type {Object} An object lookup of entities
|
|
62
|
+
*
|
|
63
|
+
* @property {String} singular The singular noun for the item
|
|
64
|
+
* @property {Function} initState Function called to initialize state when Firestore has no existing document. Called as `({supabase:BoundSupabaseyFunction, entity:String, id:String, relation?:string})` and expected to return the initial data object state
|
|
65
|
+
* @property {Function} flushState Function called to flush state from Firebase to Supabase. Called the same as `initState` + `{state:Object}`
|
|
66
|
+
*/
|
|
67
|
+
const syncroConfig: SyncroConfig = {
|
|
68
|
+
projects: { // {{{
|
|
69
|
+
singular: 'project',
|
|
70
|
+
async initState({supabasey, id}) {
|
|
71
|
+
let projectData = await supabasey((supabase) => supabase
|
|
72
|
+
.from('projects')
|
|
73
|
+
.select('data')
|
|
74
|
+
.eq('id', id)
|
|
75
|
+
.maybeSingle<ProjectRow>()
|
|
76
|
+
);
|
|
77
|
+
if (!projectData) throw new Error(`Syncro project "${id}" not found`);
|
|
78
|
+
let data = projectData.data;
|
|
79
|
+
|
|
80
|
+
// MIGRATION - Move data.temp{} into Supabase files + add pointer to filename {{{
|
|
81
|
+
if (
|
|
82
|
+
data.temp // Project contains temp subkey
|
|
83
|
+
&& Object.values(data.temp).some((t: any) => typeof t == 'object') // Some of the temp keys are objects
|
|
84
|
+
) {
|
|
85
|
+
console.log('[MIGRATION] tera-fy project v1 -> v2', data.temp);
|
|
86
|
+
const tempObject = data.temp;
|
|
87
|
+
|
|
88
|
+
await Promise.all(
|
|
89
|
+
Object.entries(data.temp)
|
|
90
|
+
.filter(([, branch]) => typeof branch == 'object')
|
|
91
|
+
.map(([toolKey, ]) => {
|
|
92
|
+
console.log(`[MIGRATION] Converting data.temp[${toolKey}]...`);
|
|
93
|
+
|
|
94
|
+
const toolName = toolKey.split('-')[0];
|
|
95
|
+
const fileName = `data-${toolName}-${nanoid()}.json`;
|
|
96
|
+
console.log('[MIGRATION] Creating filename:', fileName);
|
|
97
|
+
|
|
98
|
+
return Promise.resolve()
|
|
99
|
+
.then(()=> supabasey((supabase) => supabase // Split data.temp[toolKey] -> file {{{
|
|
100
|
+
.storage
|
|
101
|
+
.from('projects')
|
|
102
|
+
.upload(
|
|
103
|
+
`${id}/${fileName}`,
|
|
104
|
+
new File(
|
|
105
|
+
[
|
|
106
|
+
new Blob(
|
|
107
|
+
[
|
|
108
|
+
JSON.stringify(tempObject[toolKey], null, '\t'),
|
|
109
|
+
],
|
|
110
|
+
{
|
|
111
|
+
type: 'application/json',
|
|
112
|
+
},
|
|
113
|
+
),
|
|
114
|
+
],
|
|
115
|
+
fileName,
|
|
116
|
+
{
|
|
117
|
+
type: 'application/json',
|
|
118
|
+
},
|
|
119
|
+
),
|
|
120
|
+
{
|
|
121
|
+
cacheControl: '3600',
|
|
122
|
+
upsert: true,
|
|
123
|
+
},
|
|
124
|
+
)
|
|
125
|
+
)) // }}}
|
|
126
|
+
.then(()=> tempObject[toolKey] = fileName) // Replace data.temp[toolKey] with new filename
|
|
127
|
+
.catch(e => {
|
|
128
|
+
console.warn('[MIGRATION] Failed to create file', fileName, '-', e);
|
|
129
|
+
throw e;
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
} // }}}
|
|
135
|
+
|
|
136
|
+
return data;
|
|
137
|
+
},
|
|
138
|
+
flushState({supabasey, state, fsId}) {
|
|
139
|
+
// Import Supabasey because 'supabasey' lowercase is just a function
|
|
140
|
+
return supabasey(supabase => supabase.rpc('syncro_merge_data', {
|
|
141
|
+
table_name: 'projects',
|
|
142
|
+
entity_id: fsId,
|
|
143
|
+
new_data: state,
|
|
144
|
+
}));
|
|
145
|
+
},
|
|
146
|
+
}, // }}}
|
|
147
|
+
project_libraries: { // {{{
|
|
148
|
+
singular: 'project library',
|
|
149
|
+
async initState({supabasey, id, relation}) {
|
|
150
|
+
if (!relation || !/_\*$/.test(relation)) throw new Error('Project library relation missing, path should resemble "project_library::${PROJECT}::${LIBRARY_FILE_ID}_*"');
|
|
151
|
+
|
|
152
|
+
let fileId = relation.replace(/_\*$/, '');
|
|
153
|
+
|
|
154
|
+
const files = await supabasey((supabase) => supabase
|
|
155
|
+
.storage
|
|
156
|
+
.from('projects')
|
|
157
|
+
.list(id))
|
|
158
|
+
|
|
159
|
+
const file = files?.find((f: any) => f.id == fileId);
|
|
160
|
+
if (!file) return Promise.reject(`Invalid file ID "${fileId}"`);
|
|
161
|
+
|
|
162
|
+
const blob = await supabasey((supabase) => supabase
|
|
163
|
+
.storage
|
|
164
|
+
.from('projects')
|
|
165
|
+
.download(`${id}/${file.name}`))
|
|
166
|
+
|
|
167
|
+
if (!blob) throw new Error('Failed to download file blob');
|
|
168
|
+
|
|
169
|
+
const refs = await Reflib.uploadFile({
|
|
170
|
+
file: new File(
|
|
171
|
+
[blob],
|
|
172
|
+
file.name.replace(/^.*[/\\]/, ''), // Extract basename from original file name
|
|
173
|
+
),
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
return Object.fromEntries(refs // Transform Reflib Ref array into a keyed UUID object
|
|
177
|
+
.map((ref: any) => [ // Construct Object.fromEntries() compatible object [key, val] tuple
|
|
178
|
+
uuid4(), // TODO: This should really be using V5 with some-kind of namespacing but I can't get my head around the documentation - MC 2025-02-21
|
|
179
|
+
ref, // The actual ref payload
|
|
180
|
+
])
|
|
181
|
+
)
|
|
182
|
+
},
|
|
183
|
+
flushState() {
|
|
184
|
+
throw new Error('Flushing project_libraries::* namespace is not yet supported');
|
|
185
|
+
},
|
|
186
|
+
}, // }}}
|
|
187
|
+
project_namespaces: { // {{{
|
|
188
|
+
singular: 'project namespace',
|
|
189
|
+
async initState({supabasey, id, relation}) {
|
|
190
|
+
if (!relation) throw new Error('Project namespace relation missing, path should resemble "project_namespaces::${PROJECT}::${RELATION}"');
|
|
191
|
+
let rows = await supabasey((supabase) => supabase
|
|
192
|
+
.from('project_namespaces')
|
|
193
|
+
.select('data')
|
|
194
|
+
.eq('project', id)
|
|
195
|
+
.eq('name', relation)
|
|
196
|
+
.limit(1)
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
if (rows && rows.length == 1) {
|
|
200
|
+
return rows[0].data;
|
|
201
|
+
} else {
|
|
202
|
+
const newItem = await supabasey((supabase) => supabase
|
|
203
|
+
.from('project_namespaces') // Doesn't exist - create it
|
|
204
|
+
.insert<NamespaceRow>({
|
|
205
|
+
project: id,
|
|
206
|
+
name: relation,
|
|
207
|
+
data: {},
|
|
208
|
+
})
|
|
209
|
+
.select('data')
|
|
210
|
+
.single<NamespaceRow>() // Assuming insert returns the single inserted row
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
if (!newItem) throw new Error('Failed to create project namespace');
|
|
214
|
+
return newItem.data;
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
flushState({supabasey, state, id, relation}) {
|
|
218
|
+
return supabasey((supabase) => supabase
|
|
219
|
+
.from('project_namespaces')
|
|
220
|
+
.update({
|
|
221
|
+
edited_at: new Date().toISOString(),
|
|
222
|
+
data: state,
|
|
223
|
+
})
|
|
224
|
+
.eq('project', id)
|
|
225
|
+
.eq('name', relation)
|
|
226
|
+
);
|
|
227
|
+
},
|
|
228
|
+
}, // }}}
|
|
229
|
+
test: { // {{{
|
|
230
|
+
singular: 'test',
|
|
231
|
+
async initState({supabasey, id}: {supabasey: BoundSupabaseyFunction, id: string}) {
|
|
232
|
+
let rows = await supabasey((supabase) => supabase
|
|
233
|
+
.from('test')
|
|
234
|
+
.select('data')
|
|
235
|
+
.eq('id', id)
|
|
236
|
+
.limit(1)
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
if (!rows || rows.length !== 1) return Promise.reject(`Syncro test item "${id}" not found`);
|
|
240
|
+
return rows[0].data;
|
|
241
|
+
},
|
|
242
|
+
flushState({supabasey, state, fsId}) {
|
|
243
|
+
return supabasey(supabase => supabase.rpc('syncro_merge_data', {
|
|
244
|
+
table_name: 'test',
|
|
245
|
+
entity_id: fsId,
|
|
246
|
+
new_data: state,
|
|
247
|
+
}));
|
|
248
|
+
},
|
|
249
|
+
}, // }}}
|
|
250
|
+
users: { // {{{
|
|
251
|
+
singular: 'user',
|
|
252
|
+
async initState({supabasey, id}: {supabasey: BoundSupabaseyFunction, id: string}) {
|
|
253
|
+
let user = await supabasey((supabase) => supabase
|
|
254
|
+
.from('users')
|
|
255
|
+
.select('data')
|
|
256
|
+
.eq('id', id)
|
|
257
|
+
.maybeSingle<UserRow>()
|
|
258
|
+
);
|
|
259
|
+
if (user) return user.data; // User is valid and already exists
|
|
260
|
+
|
|
261
|
+
// User row doesn't already exist - need to create stub
|
|
262
|
+
let newUser = await supabasey((supabase) => supabase
|
|
263
|
+
.from('users')
|
|
264
|
+
.insert<UserRow>({
|
|
265
|
+
id,
|
|
266
|
+
data: {
|
|
267
|
+
id,
|
|
268
|
+
credits: 1000,
|
|
269
|
+
},
|
|
270
|
+
})
|
|
271
|
+
.select('data')
|
|
272
|
+
.single<UserRow>() // Assuming insert returns the single inserted row
|
|
273
|
+
);
|
|
274
|
+
if (!newUser) throw new Error('Failed to create user');
|
|
275
|
+
return newUser.data; // Return back the data that eventually got created - allowing for database triggers, default field values etc.
|
|
276
|
+
|
|
277
|
+
},
|
|
278
|
+
flushState({supabasey, state, fsId}) {
|
|
279
|
+
return supabasey(supabase => supabase.rpc('syncro_merge_data', {
|
|
280
|
+
table_name: 'users',
|
|
281
|
+
entity_id: fsId,
|
|
282
|
+
new_data: state,
|
|
283
|
+
}));
|
|
284
|
+
},
|
|
285
|
+
}, // }}}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
export default syncroConfig;
|
|
@@ -3,6 +3,20 @@ import {doc as FirestoreDocRef, getDoc as FirestoreGetDoc} from 'firebase/firest
|
|
|
3
3
|
import Syncro from './syncro.js';
|
|
4
4
|
import SyncroEntities from './entities.js';
|
|
5
5
|
|
|
6
|
+
// Define a basic structure for what pathSplit should return
|
|
7
|
+
interface PathSplitResult {
|
|
8
|
+
entity: string;
|
|
9
|
+
id: string;
|
|
10
|
+
relation?: string; // Making relation optional
|
|
11
|
+
fsCollection: string;
|
|
12
|
+
fsId: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Define a structure for options passed to flush
|
|
16
|
+
interface FlushOptions {
|
|
17
|
+
destroy?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
/**
|
|
7
21
|
* @class SyncroKeyed
|
|
8
22
|
* TERA Isomorphic SyncroKeyed class
|
|
@@ -27,7 +41,7 @@ export default class SyncroKeyed extends Syncro {
|
|
|
27
41
|
* Various storage about keyed path
|
|
28
42
|
*/
|
|
29
43
|
keyedPath = {
|
|
30
|
-
getKey(path, index) {
|
|
44
|
+
getKey(path: string, index: number): string {
|
|
31
45
|
return path + '_' + index;
|
|
32
46
|
},
|
|
33
47
|
};
|
|
@@ -38,7 +52,7 @@ export default class SyncroKeyed extends Syncro {
|
|
|
38
52
|
*
|
|
39
53
|
* @type {Array<Syncro>}
|
|
40
54
|
*/
|
|
41
|
-
members = [];
|
|
55
|
+
members: Syncro[] = [];
|
|
42
56
|
|
|
43
57
|
|
|
44
58
|
/**
|
|
@@ -47,13 +61,13 @@ export default class SyncroKeyed extends Syncro {
|
|
|
47
61
|
* @param {String} path Mount path for the Syncro. Should be in the form `${ENTITY}::${ID}(::${RELATION})?_*` (must contain a '*' operator)
|
|
48
62
|
* @param {Object} [options] Additional instance setters (mutates instance directly)
|
|
49
63
|
*/
|
|
50
|
-
constructor(path, options) {
|
|
64
|
+
constructor(path: string, options?: Record<string, any>) {
|
|
51
65
|
super(path, options);
|
|
52
66
|
|
|
53
67
|
if (!/\*/.test(path)) throw new Error('SyncroKeyed paths must contain at least one asterisk as an object pagination indicator');
|
|
54
68
|
|
|
55
|
-
let {prefix, suffix} = /^(?<prefix>.+)\*(?<suffix>.*)$/.exec(path)
|
|
56
|
-
this.keyedPath.getKey = (path, index) => `${prefix}${index}${suffix}`;
|
|
69
|
+
let {prefix, suffix} = /^(?<prefix>.+)\*(?<suffix>.*)$/.exec(path)!.groups!;
|
|
70
|
+
this.keyedPath.getKey = (path: string, index: number): string => `${prefix}${index}${suffix}`;
|
|
57
71
|
}
|
|
58
72
|
|
|
59
73
|
|
|
@@ -62,7 +76,7 @@ export default class SyncroKeyed extends Syncro {
|
|
|
62
76
|
*
|
|
63
77
|
* @returns {Promise} A promise which resolves when the operation has completed
|
|
64
78
|
*/
|
|
65
|
-
async destroy() {
|
|
79
|
+
async destroy(): Promise<never[]> { // Match base class signature
|
|
66
80
|
this.debug('Destroy!');
|
|
67
81
|
await Promise.all(
|
|
68
82
|
this.members.map(member =>
|
|
@@ -71,6 +85,7 @@ export default class SyncroKeyed extends Syncro {
|
|
|
71
85
|
);
|
|
72
86
|
|
|
73
87
|
this.members = [];
|
|
88
|
+
return [] as never[]; // Return empty array cast to never[] to satisfy signature
|
|
74
89
|
}
|
|
75
90
|
|
|
76
91
|
|
|
@@ -80,14 +95,15 @@ export default class SyncroKeyed extends Syncro {
|
|
|
80
95
|
*
|
|
81
96
|
* @returns {Promise<Syncro>} A promise which resolves as this SyncroKeyed instance when completed
|
|
82
97
|
*/
|
|
83
|
-
mount() {
|
|
84
|
-
|
|
98
|
+
mount(): Promise<Syncro> {
|
|
99
|
+
// Cast the result to the expected interface
|
|
100
|
+
let {entity, id, relation, fsCollection, fsId} = Syncro.pathSplit(this.path, {allowAsterisk: true}) as PathSplitResult;
|
|
85
101
|
|
|
86
102
|
return Promise.resolve()
|
|
87
|
-
.then(()=> new Promise(resolve => { // Mount all members by looking for similar keys
|
|
103
|
+
.then(()=> new Promise<void>(resolve => { // Mount all members by looking for similar keys
|
|
88
104
|
this.members = []; // Reset member list
|
|
89
105
|
|
|
90
|
-
let seekMember = async (index) => {
|
|
106
|
+
let seekMember = async (index: number) => {
|
|
91
107
|
let memberId = fsId.replace('*', ''+index);
|
|
92
108
|
this.debug('Seek keyedMember', fsCollection, '#', memberId);
|
|
93
109
|
|
|
@@ -96,7 +112,7 @@ export default class SyncroKeyed extends Syncro {
|
|
|
96
112
|
let doc = await FirestoreGetDoc(docRef);
|
|
97
113
|
if (doc.exists()) { // Found a matching entry
|
|
98
114
|
// Expand member lookup with the new member by its numeric index
|
|
99
|
-
this.keyedMembersExpand(index);
|
|
115
|
+
await this.keyedMembersExpand(index);
|
|
100
116
|
|
|
101
117
|
// Queue up next member fetcher
|
|
102
118
|
setTimeout(()=> seekMember(index+1));
|
|
@@ -116,19 +132,21 @@ export default class SyncroKeyed extends Syncro {
|
|
|
116
132
|
this.debug('Populate initial SyncroKeyed state');
|
|
117
133
|
|
|
118
134
|
// Extract base data + add document and return new hook
|
|
119
|
-
|
|
135
|
+
const entityKey = entity as keyof typeof SyncroEntities;
|
|
136
|
+
if (!SyncroEntities[entityKey]) throw new Error(`Unknown Sync entity "${entity}"`);
|
|
120
137
|
|
|
121
138
|
// Go fetch the initial state object
|
|
122
|
-
let state = await SyncroEntities[
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
entity, id, relation,
|
|
139
|
+
let state = await SyncroEntities[entityKey].initState({
|
|
140
|
+
supabasey: Syncro.supabasey,
|
|
141
|
+
id, relation,
|
|
126
142
|
});
|
|
127
143
|
|
|
128
144
|
await this.keyedAssign(state);
|
|
129
145
|
})
|
|
130
146
|
.then(()=> { // Create the reactive
|
|
131
147
|
let reactive = this.getReactive(this.proxy());
|
|
148
|
+
// Assuming this.value should hold the reactive proxy
|
|
149
|
+
// If this.value is inherited and has a specific type, this might need adjustment
|
|
132
150
|
this.value = reactive.doc;
|
|
133
151
|
})
|
|
134
152
|
.then(()=> this)
|
|
@@ -140,38 +158,46 @@ export default class SyncroKeyed extends Syncro {
|
|
|
140
158
|
*
|
|
141
159
|
* @returns {Proxy} A proxy of all combined members
|
|
142
160
|
*/
|
|
143
|
-
proxy() {
|
|
161
|
+
proxy(): Record<string | symbol, any> { // Return type can be more specific if needed
|
|
144
162
|
return new Proxy(this, {
|
|
145
163
|
// Return the full list of keys
|
|
146
|
-
ownKeys(target) {
|
|
147
|
-
return target.members.flatMap(m => Object.keys(m.value));
|
|
164
|
+
ownKeys(target: SyncroKeyed): ArrayLike<string | symbol> {
|
|
165
|
+
return target.members.flatMap(m => Object.keys(m.value || {}));
|
|
148
166
|
},
|
|
149
167
|
|
|
150
168
|
// Return if we have a lookup key
|
|
151
|
-
has(target, prop) {
|
|
152
|
-
|
|
169
|
+
has(target: SyncroKeyed, prop: string | symbol): boolean {
|
|
170
|
+
// Ensure m.value exists before checking property
|
|
171
|
+
return target.members.some(m => m.value && prop in m.value);
|
|
153
172
|
},
|
|
154
173
|
|
|
155
174
|
// Scope through members until we get a hit on the key
|
|
156
|
-
get(target, prop) {
|
|
157
|
-
let targetMember = target.members.find(m => prop in m.value);
|
|
158
|
-
|
|
175
|
+
get(target: SyncroKeyed, prop: string | symbol): any {
|
|
176
|
+
let targetMember = target.members.find(m => m.value && prop in m.value);
|
|
177
|
+
// Access value via targetMember.value if found
|
|
178
|
+
return targetMember ? targetMember.value[prop] : undefined;
|
|
159
179
|
},
|
|
160
180
|
|
|
161
181
|
// Set the member key if one already exists, otherwise overflow onto the next member
|
|
162
|
-
set(target, prop, value) {
|
|
163
|
-
let targetMember = target.members.find(m => prop in m.value);
|
|
164
|
-
if (targetMember) {
|
|
165
|
-
targetMember[prop] = value;
|
|
182
|
+
set(target: SyncroKeyed, prop: string | symbol, value: any): boolean {
|
|
183
|
+
let targetMember = target.members.find(m => m.value && prop in m.value);
|
|
184
|
+
if (targetMember && targetMember.value) {
|
|
185
|
+
targetMember.value[prop] = value;
|
|
166
186
|
} else {
|
|
167
|
-
|
|
187
|
+
// Assuming keyedSet handles adding the value appropriately
|
|
188
|
+
target.keyedSet(prop as string, value); // Cast prop to string if keyedSet expects string
|
|
168
189
|
}
|
|
190
|
+
return true; // Proxy set must return boolean
|
|
169
191
|
},
|
|
170
192
|
|
|
171
193
|
// Remove a key
|
|
172
|
-
deleteProperty(target, prop) {
|
|
173
|
-
let targetMember = target.members.find(m => prop in m);
|
|
174
|
-
if (targetMember
|
|
194
|
+
deleteProperty(target: SyncroKeyed, prop: string | symbol): boolean {
|
|
195
|
+
let targetMember = target.members.find(m => m.value && prop in m.value);
|
|
196
|
+
if (targetMember && targetMember.value) {
|
|
197
|
+
delete targetMember.value[prop];
|
|
198
|
+
return true; // Indicate success
|
|
199
|
+
}
|
|
200
|
+
return false; // Indicate key not found or failure
|
|
175
201
|
},
|
|
176
202
|
});
|
|
177
203
|
}
|
|
@@ -185,21 +211,22 @@ export default class SyncroKeyed extends Syncro {
|
|
|
185
211
|
*
|
|
186
212
|
* @returns {Promise} A promise which resolves when the operation has completed
|
|
187
213
|
*/
|
|
188
|
-
async flush(options) {
|
|
189
|
-
let settings = {
|
|
214
|
+
async flush(options?: FlushOptions): Promise<void> { // Match base class signature
|
|
215
|
+
let settings: FlushOptions = {
|
|
190
216
|
destroy: false,
|
|
191
217
|
...options,
|
|
192
218
|
};
|
|
193
219
|
|
|
194
220
|
await Promise.all(
|
|
195
221
|
this.members.map(member =>
|
|
196
|
-
member.flush({
|
|
222
|
+
member.flush({ // Assuming member.flush returns Promise<void> now
|
|
197
223
|
destroy: settings.destroy,
|
|
198
224
|
})
|
|
199
225
|
)
|
|
200
226
|
);
|
|
201
227
|
|
|
202
228
|
if (settings.destroy) await this.destroy();
|
|
229
|
+
return; // Return void to match signature
|
|
203
230
|
}
|
|
204
231
|
|
|
205
232
|
|
|
@@ -212,19 +239,28 @@ export default class SyncroKeyed extends Syncro {
|
|
|
212
239
|
*
|
|
213
240
|
* @returns {Promise<*>} A promise which resolves when the operation has completed with the set value
|
|
214
241
|
*/
|
|
215
|
-
async keyedSet(key, value) {
|
|
216
|
-
let candidateMember = this.members.find(m => Object.keys(m.value).length < this.
|
|
217
|
-
if (candidateMember) {
|
|
218
|
-
|
|
242
|
+
async keyedSet(key: string, value: any): Promise<any> {
|
|
243
|
+
let candidateMember = this.members.find(m => m.value && Object.keys(m.value).length < this.keyedConfig.maxKeys);
|
|
244
|
+
if (candidateMember?.value) {
|
|
245
|
+
candidateMember.value[key] = value;
|
|
246
|
+
return value;
|
|
219
247
|
} else { // No candidate - need to expand then set
|
|
220
248
|
// Extend members
|
|
221
|
-
await this.keyedMembersExpand();
|
|
249
|
+
await this.keyedMembersExpand(); // Call without index to append
|
|
250
|
+
|
|
251
|
+
// Get the newly added member
|
|
252
|
+
let newMember = this.members.at(-1);
|
|
253
|
+
if (!newMember || !newMember.value) {
|
|
254
|
+
throw new Error('Failed to expand members or new member has no value object');
|
|
255
|
+
}
|
|
222
256
|
|
|
223
|
-
// Sanity check
|
|
224
|
-
|
|
225
|
-
|
|
257
|
+
// Sanity check (check the new member's value)
|
|
258
|
+
if (Object.keys(newMember.value).length >= this.keyedConfig.maxKeys) {
|
|
259
|
+
throw new Error(`Need to append key "${key}" but newly added member (offset #${this.members.length-1}) has size of ${Object.keys(newMember.value).length}, exceeding maxKeys ${this.keyedConfig.maxKeys}`);
|
|
260
|
+
}
|
|
226
261
|
|
|
227
|
-
|
|
262
|
+
newMember.value[key] = value;
|
|
263
|
+
return value;
|
|
228
264
|
}
|
|
229
265
|
}
|
|
230
266
|
|
|
@@ -236,9 +272,10 @@ export default class SyncroKeyed extends Syncro {
|
|
|
236
272
|
*
|
|
237
273
|
* @param {Object} state The value to merge
|
|
238
274
|
*/
|
|
239
|
-
async keyedAssign(state) {
|
|
275
|
+
async keyedAssign(state: Record<string, any>): Promise<void> {
|
|
240
276
|
// Can we assume we have a blank state - this speeds up existing key checks significantly
|
|
241
|
-
|
|
277
|
+
// Ensure members[0] and its value exist
|
|
278
|
+
let isBlank = this.members.length === 1 && this.members[0]?.value && Object.keys(this.members[0].value).length === 0;
|
|
242
279
|
|
|
243
280
|
if (isBlank) {
|
|
244
281
|
let chunks = chunk(Object.entries(state), this.keyedConfig.maxKeys)
|
|
@@ -251,16 +288,22 @@ export default class SyncroKeyed extends Syncro {
|
|
|
251
288
|
// Create chunk document if its missing
|
|
252
289
|
if (!this.members[chunkIndex]) await this.keyedMembersExpand(chunkIndex);
|
|
253
290
|
|
|
254
|
-
//
|
|
255
|
-
|
|
291
|
+
// Ensure the member exists and has setFirestoreState method
|
|
292
|
+
const member = this.members[chunkIndex];
|
|
293
|
+
if (member && typeof (member as any).setFirestoreState === 'function') {
|
|
294
|
+
// Populate its state - Cast to any temporarily if setFirestoreState is not public/recognized
|
|
295
|
+
await (member as any).setFirestoreState(chunk, {method: 'set'});
|
|
296
|
+
} else {
|
|
297
|
+
console.warn(`Member at index ${chunkIndex} is missing or does not have setFirestoreState`);
|
|
298
|
+
}
|
|
256
299
|
})
|
|
257
300
|
);
|
|
258
301
|
|
|
259
302
|
} else { // Non-blank - call keyedSet() on each key of state, merging slowly
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
303
|
+
// Replace Array.fromAsync with a standard loop
|
|
304
|
+
for (const [key, val] of Object.entries(state)) {
|
|
305
|
+
await this.keyedSet(key, val);
|
|
306
|
+
}
|
|
264
307
|
}
|
|
265
308
|
}
|
|
266
309
|
|
|
@@ -271,12 +314,21 @@ export default class SyncroKeyed extends Syncro {
|
|
|
271
314
|
* @param {Number} [index] The index to use when expanding, if omitted the next index offset is used
|
|
272
315
|
* @returns {Promise} A promise which resolves when the operation has completed
|
|
273
316
|
*/
|
|
274
|
-
async keyedMembersExpand(index) {
|
|
275
|
-
index
|
|
276
|
-
if (this.members[index])
|
|
317
|
+
async keyedMembersExpand(index?: number): Promise<void> {
|
|
318
|
+
index = index ?? this.members.length; // Use provided index or next available slot
|
|
319
|
+
if (this.members[index]) {
|
|
320
|
+
// If member already exists (e.g., during initial seek), just ensure it's mounted.
|
|
321
|
+
// If it was called intentionally with an existing index, maybe log a warning or skip.
|
|
322
|
+
this.debug(`keyedMembersExpand called for existing index ${index}, ensuring mounted.`);
|
|
323
|
+
if (!this.members[index].value) { // If it exists but isn't mounted somehow
|
|
324
|
+
await this.members[index].mount({ initialState: {} });
|
|
325
|
+
}
|
|
326
|
+
return; // Exit if member already exists
|
|
327
|
+
}
|
|
277
328
|
|
|
278
329
|
let syncroPath = this.keyedPath.getKey(this.path, index);
|
|
279
|
-
|
|
330
|
+
// Pass empty options object {} or specify allowAsterisk: false if needed
|
|
331
|
+
let {fsCollection, fsId} = Syncro.pathSplit(syncroPath, {}) as PathSplitResult;
|
|
280
332
|
this.debug('Expand SyncroKeyed size to index=', index);
|
|
281
333
|
|
|
282
334
|
// Create a new Syncro member, inheriteing some details from this parent item
|
|
@@ -290,7 +342,12 @@ export default class SyncroKeyed extends Syncro {
|
|
|
290
342
|
initialState: {}, // Force intital state to empty object so we don't get stuck in a loop
|
|
291
343
|
});
|
|
292
344
|
|
|
293
|
-
|
|
345
|
+
// Insert at the correct index if specified, otherwise push
|
|
346
|
+
if (index < this.members.length) {
|
|
347
|
+
this.members.splice(index, 0, syncro);
|
|
348
|
+
} else {
|
|
349
|
+
this.members.push(syncro);
|
|
350
|
+
}
|
|
294
351
|
}
|
|
295
352
|
|
|
296
353
|
}
|