@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.
Files changed (75) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/api.md +68 -66
  3. package/dist/lib/projectFile.d.ts +182 -0
  4. package/dist/lib/projectFile.js +157 -0
  5. package/dist/lib/projectFile.js.map +1 -0
  6. package/dist/lib/syncro/entities.d.ts +28 -0
  7. package/dist/lib/syncro/entities.js +203 -0
  8. package/dist/lib/syncro/entities.js.map +1 -0
  9. package/dist/lib/syncro/keyed.d.ts +95 -0
  10. package/dist/lib/syncro/keyed.js +286 -0
  11. package/dist/lib/syncro/keyed.js.map +1 -0
  12. package/dist/lib/syncro/syncro.d.ts +328 -0
  13. package/dist/lib/syncro/syncro.js +633 -0
  14. package/dist/lib/syncro/syncro.js.map +1 -0
  15. package/dist/lib/terafy.bootstrapper.d.ts +42 -0
  16. package/dist/lib/terafy.bootstrapper.js +130 -0
  17. package/dist/lib/terafy.bootstrapper.js.map +1 -0
  18. package/dist/lib/terafy.client.d.ts +532 -0
  19. package/dist/lib/terafy.client.js +1110 -0
  20. package/dist/lib/terafy.client.js.map +1 -0
  21. package/dist/lib/terafy.proxy.d.ts +66 -0
  22. package/dist/lib/terafy.proxy.js +123 -0
  23. package/dist/lib/terafy.proxy.js.map +1 -0
  24. package/dist/lib/terafy.server.d.ts +607 -0
  25. package/dist/lib/terafy.server.js +1774 -0
  26. package/dist/lib/terafy.server.js.map +1 -0
  27. package/dist/plugin.vue2.es2019.js +30 -13
  28. package/dist/plugins/base.d.ts +20 -0
  29. package/dist/plugins/base.js +21 -0
  30. package/dist/plugins/base.js.map +1 -0
  31. package/dist/plugins/firebase.d.ts +62 -0
  32. package/dist/plugins/firebase.js +111 -0
  33. package/dist/plugins/firebase.js.map +1 -0
  34. package/dist/plugins/vite.d.ts +12 -0
  35. package/dist/plugins/vite.js +22 -0
  36. package/dist/plugins/vite.js.map +1 -0
  37. package/dist/plugins/vue2.d.ts +68 -0
  38. package/dist/plugins/vue2.js +96 -0
  39. package/dist/plugins/vue2.js.map +1 -0
  40. package/dist/plugins/vue3.d.ts +64 -0
  41. package/dist/plugins/vue3.js +96 -0
  42. package/dist/plugins/vue3.js.map +1 -0
  43. package/dist/terafy.bootstrapper.es2019.js +2 -2
  44. package/dist/terafy.bootstrapper.js +2 -2
  45. package/dist/terafy.es2019.js +2 -2
  46. package/dist/terafy.js +1 -1
  47. package/dist/utils/mixin.d.ts +11 -0
  48. package/dist/utils/mixin.js +15 -0
  49. package/dist/utils/mixin.js.map +1 -0
  50. package/dist/utils/pDefer.d.ts +12 -0
  51. package/dist/utils/pDefer.js +14 -0
  52. package/dist/utils/pDefer.js.map +1 -0
  53. package/dist/utils/pathTools.d.ts +70 -0
  54. package/dist/utils/pathTools.js +120 -0
  55. package/dist/utils/pathTools.js.map +1 -0
  56. package/eslint.config.js +44 -8
  57. package/lib/{projectFile.js → projectFile.ts} +83 -40
  58. package/lib/syncro/entities.ts +288 -0
  59. package/lib/syncro/{keyed.js → keyed.ts} +114 -57
  60. package/lib/syncro/{syncro.js → syncro.ts} +204 -169
  61. package/lib/{terafy.bootstrapper.js → terafy.bootstrapper.ts} +49 -31
  62. package/lib/{terafy.client.js → terafy.client.ts} +94 -86
  63. package/lib/{terafy.proxy.js → terafy.proxy.ts} +43 -16
  64. package/lib/{terafy.server.js → terafy.server.ts} +364 -223
  65. package/package.json +65 -26
  66. package/plugins/{base.js → base.ts} +3 -1
  67. package/plugins/{firebase.js → firebase.ts} +34 -16
  68. package/plugins/{vite.js → vite.ts} +3 -3
  69. package/plugins/{vue2.js → vue2.ts} +17 -10
  70. package/plugins/{vue3.js → vue3.ts} +11 -9
  71. package/tsconfig.json +30 -0
  72. package/utils/{mixin.js → mixin.ts} +1 -1
  73. package/utils/{pDefer.js → pDefer.ts} +10 -3
  74. package/utils/{pathTools.js → pathTools.ts} +11 -9
  75. 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).groups;
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
- let {entity, id, relation, fsCollection, fsId} = Syncro.pathSplit(this.path, {allowAsterisk: true});
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
- if (!SyncroEntities[entity]) throw new Error(`Unknown Sync entity "${entity}"`);
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[entity].initState({
123
- supabase: Syncro.supabase,
124
- fsCollection, fsId,
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
- return target.members.some(m => !! m.value[prop]);
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
- return targetMember ? targetMember[prop] : undefined;
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
- target.keyedSet(prop, value);
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) targetMember.value[prop];
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.config.maxKeys);
217
- if (candidateMember) {
218
- return candidateMember.value[key] = value;
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
- let candidateMember = this.members.at(-1);
225
- if (Object.keys(candidateMember).length >= this.config.maxKeys) throw new Error(`Need to append key "${key}" but newly added member doesnt have enough room. Member offset #${this.members.length-1} has size of ${Object.keys(candidateMember).length}`);
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
- return candidateMember.value[key] = value;
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
- let isBlank = this.members.length == 1 && Object.keys(this.members[0].value).length == 0;
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
- // Populate its state
255
- await this.members[chunkIndex].setFirestoreState(chunk, {method: 'set'});
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
- await Array.fromAsync( // In a promise-series chain call keyedSet for each item
261
- Object.entries(state),
262
- ([key, val]) => this.keyedSet(key, val),
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 ??= this.members.length;
276
- if (this.members[index]) throw new Error(`keyedMembersExpand(${index}) but index already exists`);; // Member already exists
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
- let {fsCollection, fsId} = Syncro.pathSplit(syncroPath);
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
- this.members.push(syncro);
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
  }