velocious 1.0.473 → 1.0.475

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 (96) hide show
  1. package/README.md +7 -2
  2. package/build/authorization/base-resource.js +99 -0
  3. package/build/configuration-types.js +41 -3
  4. package/build/configuration.js +41 -1
  5. package/build/database/drivers/base.js +51 -3
  6. package/build/database/drivers/mysql/index.js +74 -7
  7. package/build/database/drivers/sqlite/connection-sql-js.js +16 -5
  8. package/build/database/drivers/sqlite/index.web.js +29 -15
  9. package/build/database/drivers/sqlite/web-persistence.js +521 -0
  10. package/build/environment-handlers/node/cli/commands/generate/frontend-models.js +64 -1
  11. package/build/frontend-model-controller.js +139 -16
  12. package/build/frontend-model-resource/base-resource.js +258 -46
  13. package/build/frontend-model-resource/velocious-attachment-resource.js +0 -3
  14. package/build/frontend-models/base.js +146 -2
  15. package/build/frontend-models/query.js +3 -3
  16. package/build/frontend-models/resource-definition.js +400 -23
  17. package/build/routes/hooks/frontend-model-command-route-hook.js +8 -0
  18. package/build/src/authorization/base-resource.d.ts +41 -0
  19. package/build/src/authorization/base-resource.d.ts.map +1 -1
  20. package/build/src/authorization/base-resource.js +88 -1
  21. package/build/src/configuration-types.d.ts +128 -5
  22. package/build/src/configuration-types.d.ts.map +1 -1
  23. package/build/src/configuration-types.js +38 -4
  24. package/build/src/configuration.d.ts +18 -1
  25. package/build/src/configuration.d.ts.map +1 -1
  26. package/build/src/configuration.js +37 -2
  27. package/build/src/database/drivers/base.d.ts +30 -0
  28. package/build/src/database/drivers/base.d.ts.map +1 -1
  29. package/build/src/database/drivers/base.js +47 -4
  30. package/build/src/database/drivers/mysql/index.d.ts +32 -3
  31. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  32. package/build/src/database/drivers/mysql/index.js +65 -7
  33. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +8 -1
  34. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
  35. package/build/src/database/drivers/sqlite/connection-sql-js.js +15 -6
  36. package/build/src/database/drivers/sqlite/index.web.d.ts +5 -5
  37. package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
  38. package/build/src/database/drivers/sqlite/index.web.js +27 -13
  39. package/build/src/database/drivers/sqlite/web-persistence.d.ts +77 -0
  40. package/build/src/database/drivers/sqlite/web-persistence.d.ts.map +1 -0
  41. package/build/src/database/drivers/sqlite/web-persistence.js +464 -0
  42. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts +30 -0
  43. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.d.ts.map +1 -1
  44. package/build/src/environment-handlers/node/cli/commands/generate/frontend-models.js +54 -2
  45. package/build/src/frontend-model-controller.d.ts +34 -0
  46. package/build/src/frontend-model-controller.d.ts.map +1 -1
  47. package/build/src/frontend-model-controller.js +125 -17
  48. package/build/src/frontend-model-resource/base-resource.d.ts +69 -0
  49. package/build/src/frontend-model-resource/base-resource.d.ts.map +1 -1
  50. package/build/src/frontend-model-resource/base-resource.js +247 -54
  51. package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts +0 -2
  52. package/build/src/frontend-model-resource/velocious-attachment-resource.d.ts.map +1 -1
  53. package/build/src/frontend-model-resource/velocious-attachment-resource.js +1 -3
  54. package/build/src/frontend-models/base.d.ts +29 -0
  55. package/build/src/frontend-models/base.d.ts.map +1 -1
  56. package/build/src/frontend-models/base.js +131 -3
  57. package/build/src/frontend-models/query.js +4 -4
  58. package/build/src/frontend-models/resource-definition.d.ts +6 -0
  59. package/build/src/frontend-models/resource-definition.d.ts.map +1 -1
  60. package/build/src/frontend-models/resource-definition.js +355 -28
  61. package/build/src/routes/hooks/frontend-model-command-route-hook.d.ts.map +1 -1
  62. package/build/src/routes/hooks/frontend-model-command-route-hook.js +8 -1
  63. package/build/src/sync/device-identity.d.ts +240 -0
  64. package/build/src/sync/device-identity.d.ts.map +1 -0
  65. package/build/src/sync/device-identity.js +454 -0
  66. package/build/src/sync/local-mutation-log.d.ts +212 -0
  67. package/build/src/sync/local-mutation-log.d.ts.map +1 -0
  68. package/build/src/sync/local-mutation-log.js +404 -0
  69. package/build/src/sync/offline-grant.d.ts +157 -0
  70. package/build/src/sync/offline-grant.d.ts.map +1 -0
  71. package/build/src/sync/offline-grant.js +292 -0
  72. package/build/sync/device-identity.js +503 -0
  73. package/build/sync/local-mutation-log.js +448 -0
  74. package/build/sync/offline-grant.js +327 -0
  75. package/build/tsconfig.tsbuildinfo +1 -1
  76. package/package.json +3 -3
  77. package/scripts/tensorbuzz-retry +21 -0
  78. package/src/authorization/base-resource.js +99 -0
  79. package/src/configuration-types.js +41 -3
  80. package/src/configuration.js +41 -1
  81. package/src/database/drivers/base.js +51 -3
  82. package/src/database/drivers/mysql/index.js +74 -7
  83. package/src/database/drivers/sqlite/connection-sql-js.js +16 -5
  84. package/src/database/drivers/sqlite/index.web.js +29 -15
  85. package/src/database/drivers/sqlite/web-persistence.js +521 -0
  86. package/src/environment-handlers/node/cli/commands/generate/frontend-models.js +64 -1
  87. package/src/frontend-model-controller.js +139 -16
  88. package/src/frontend-model-resource/base-resource.js +258 -46
  89. package/src/frontend-model-resource/velocious-attachment-resource.js +0 -3
  90. package/src/frontend-models/base.js +146 -2
  91. package/src/frontend-models/query.js +3 -3
  92. package/src/frontend-models/resource-definition.js +400 -23
  93. package/src/routes/hooks/frontend-model-command-route-hook.js +8 -0
  94. package/src/sync/device-identity.js +503 -0
  95. package/src/sync/local-mutation-log.js +448 -0
  96. package/src/sync/offline-grant.js +327 -0
@@ -0,0 +1,521 @@
1
+ // @ts-check
2
+
3
+ import BetterLocalStorage from "better-localstorage"
4
+
5
+ const SUPPORT_CHECK_FILE = ".velocious-opfs-support-check"
6
+ const SUPPORT_CHECK_BYTES = new Uint8Array([118, 101, 108, 111, 99, 105, 111, 117, 115])
7
+
8
+ /**
9
+ * SQLite web persistence adapter.
10
+ * @typedef {object} SqliteWebPersistence
11
+ * @property {"indexeddb" | "localstorage" | "opfs"} name - Persistence backend name.
12
+ * @property {() => Promise<void>} delete - Deletes the persisted database.
13
+ * @property {() => Promise<Uint8Array | undefined>} load - Loads persisted database bytes.
14
+ * @property {(content: Uint8Array) => Promise<void>} save - Saves persisted database bytes.
15
+ */
16
+
17
+ /**
18
+ * Browser-like environment used for web persistence detection.
19
+ * @typedef {object} SqliteWebPersistenceEnvironment
20
+ * @property {unknown} [indexedDB] - IndexedDB global.
21
+ * @property {unknown} [navigator] - Navigator global.
22
+ */
23
+
24
+ /**
25
+ * Creates the best SQLite web persistence adapter supported by the current browser.
26
+ * @param {object} args - Arguments.
27
+ * @param {string} args.databaseName - Database name.
28
+ * @param {SqliteWebPersistenceEnvironment} [args.environment] - Browser-like environment.
29
+ * @returns {Promise<SqliteWebPersistence>} - Selected persistence adapter.
30
+ */
31
+ export async function createSqliteWebPersistence({databaseName, environment = globalThis}) {
32
+ const localStoragePersistence = new LocalStoragePersistence({databaseName})
33
+ const opfsPersistence = new OpfsPersistence({databaseName, environment})
34
+ const indexedDbPersistence = new IndexedDbPersistence({databaseName, environment})
35
+
36
+ const selectedPersistence = await selectSupportedPersistence({environment, indexedDbPersistence, localStoragePersistence, opfsPersistence})
37
+
38
+ await migratePersistedDatabase({
39
+ databaseName,
40
+ destinationPersistence: selectedPersistence,
41
+ environment,
42
+ sourcePersistences: [localStoragePersistence, indexedDbPersistence, opfsPersistence]
43
+ })
44
+
45
+ return selectedPersistence
46
+ }
47
+
48
+ /**
49
+ * Deletes SQLite web database bytes from every available persistence backend.
50
+ * @param {object} args - Arguments.
51
+ * @param {string} args.databaseName - Database name.
52
+ * @param {SqliteWebPersistenceEnvironment} [args.environment] - Browser-like environment.
53
+ * @returns {Promise<void>} - Resolves when all available backends were cleared.
54
+ */
55
+ export async function deleteSqliteWebPersistences({databaseName, environment = globalThis}) {
56
+ const persistences = [
57
+ new LocalStoragePersistence({databaseName}),
58
+ new OpfsPersistence({databaseName, environment}),
59
+ new IndexedDbPersistence({databaseName, environment})
60
+ ]
61
+
62
+ for (const persistence of persistences) await deletePersistenceIfAvailable(persistence)
63
+ }
64
+
65
+ /**
66
+ * Returns the legacy SQLite web storage key for a database name.
67
+ * @param {string} databaseName - Database name.
68
+ * @returns {string} - Persistence key.
69
+ */
70
+ export function sqliteWebPersistenceKey(databaseName) {
71
+ if (!databaseName) throw new Error("No name given in arguments for SQLite Web database")
72
+
73
+ return `VelociousDatabaseDriversSqliteWeb---${databaseName}`
74
+ }
75
+
76
+ /** OPFS-backed SQL.js database file persistence. */
77
+ class OpfsPersistence {
78
+ /** @type {"opfs"} */
79
+ name = "opfs"
80
+
81
+ /**
82
+ * Creates OPFS persistence.
83
+ * @param {object} args - Arguments.
84
+ * @param {string} args.databaseName - Database name.
85
+ * @param {SqliteWebPersistenceEnvironment} args.environment - Browser-like environment.
86
+ */
87
+ constructor({databaseName, environment}) {
88
+ this.databaseName = databaseName
89
+ this.environment = environment
90
+ }
91
+
92
+ /**
93
+ * Deletes the OPFS database file.
94
+ * @returns {Promise<void>} - Resolves when deleted.
95
+ */
96
+ async delete() {
97
+ const directory = await opfsDirectory(this.environment)
98
+
99
+ try {
100
+ await directory.removeEntry(sqliteWebPersistenceKey(this.databaseName))
101
+ } catch (error) {
102
+ if (!isNotFoundError(error)) throw error
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Loads the OPFS database file.
108
+ * @returns {Promise<Uint8Array | undefined>} - Persisted bytes.
109
+ */
110
+ async load() {
111
+ const directory = await opfsDirectory(this.environment)
112
+
113
+ try {
114
+ const fileHandle = await directory.getFileHandle(sqliteWebPersistenceKey(this.databaseName))
115
+ const file = await fileHandle.getFile()
116
+ const arrayBuffer = await file.arrayBuffer()
117
+
118
+ return new Uint8Array(arrayBuffer)
119
+ } catch (error) {
120
+ if (isNotFoundError(error)) return undefined
121
+
122
+ throw error
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Checks whether the OPFS database file exists.
128
+ * @returns {Promise<boolean>} - Whether content exists.
129
+ */
130
+ async exists() {
131
+ try {
132
+ return (await this.load()) !== undefined
133
+ } catch {
134
+ return false
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Saves database bytes.
140
+ * @param {Uint8Array} content - Database bytes.
141
+ * @returns {Promise<void>} - Resolves when saved.
142
+ */
143
+ async save(content) {
144
+ const directory = await opfsDirectory(this.environment)
145
+ const fileHandle = await directory.getFileHandle(sqliteWebPersistenceKey(this.databaseName), {create: true})
146
+ const writable = await fileHandle.createWritable()
147
+
148
+ await writable.write(arrayBufferFromBytes(content))
149
+ await writable.close()
150
+ }
151
+ }
152
+
153
+ /** IndexedDB-backed SQL.js database blob persistence. */
154
+ class IndexedDbPersistence {
155
+ /** @type {"indexeddb"} */
156
+ name = "indexeddb"
157
+
158
+ /**
159
+ * Creates IndexedDB persistence.
160
+ * @param {object} args - Arguments.
161
+ * @param {string} args.databaseName - Database name.
162
+ * @param {SqliteWebPersistenceEnvironment} args.environment - Browser-like environment.
163
+ */
164
+ constructor({databaseName, environment}) {
165
+ this.databaseName = databaseName
166
+ this.environment = environment
167
+ }
168
+
169
+ /**
170
+ * Deletes the IndexedDB database entry.
171
+ * @returns {Promise<void>} - Resolves when deleted.
172
+ */
173
+ async delete() {
174
+ const database = await openIndexedDb(this.environment)
175
+
176
+ await indexedDbRequest(database.transaction("databases", "readwrite").objectStore("databases").delete(sqliteWebPersistenceKey(this.databaseName)))
177
+ database.close()
178
+ }
179
+
180
+ /**
181
+ * Loads the IndexedDB database entry.
182
+ * @returns {Promise<Uint8Array | undefined>} - Persisted bytes.
183
+ */
184
+ async load() {
185
+ const database = await openIndexedDb(this.environment)
186
+ const result = await indexedDbRequest(database.transaction("databases", "readonly").objectStore("databases").get(sqliteWebPersistenceKey(this.databaseName)))
187
+
188
+ database.close()
189
+
190
+ if (result === undefined) return undefined
191
+ if (result instanceof Uint8Array) return result
192
+ if (result instanceof ArrayBuffer) return new Uint8Array(result)
193
+
194
+ throw new Error("SQLite web IndexedDB persistence returned unsupported content")
195
+ }
196
+
197
+ /**
198
+ * Checks whether the IndexedDB database entry exists.
199
+ * @returns {Promise<boolean>} - Whether content exists.
200
+ */
201
+ async exists() {
202
+ try {
203
+ const database = await openIndexedDb(this.environment)
204
+ const result = await indexedDbRequest(database.transaction("databases", "readonly").objectStore("databases").get(sqliteWebPersistenceKey(this.databaseName)))
205
+
206
+ database.close()
207
+
208
+ return result !== undefined && result !== null
209
+ } catch {
210
+ return false
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Saves database bytes.
216
+ * @param {Uint8Array} content - Database bytes.
217
+ * @returns {Promise<void>} - Resolves when saved.
218
+ */
219
+ async save(content) {
220
+ const database = await openIndexedDb(this.environment)
221
+
222
+ await indexedDbRequest(database.transaction("databases", "readwrite").objectStore("databases").put(content, sqliteWebPersistenceKey(this.databaseName)))
223
+ database.close()
224
+ }
225
+ }
226
+
227
+ /** LocalStorage-backed SQL.js database blob persistence for legacy migrations. */
228
+ class LocalStoragePersistence {
229
+
230
+ /** @type {"localstorage"} */
231
+ name = "localstorage"
232
+
233
+ /**
234
+ * Creates localStorage persistence.
235
+ * @param {object} args - Arguments.
236
+ * @param {string} args.databaseName - Database name.
237
+ */
238
+ constructor({databaseName}) {
239
+ this.databaseName = databaseName
240
+ /** @type {BetterLocalStorage | undefined} */
241
+ this.storage = undefined
242
+ }
243
+
244
+ /**
245
+ * Deletes the localStorage database entry.
246
+ * @returns {Promise<void>} - Resolves when deleted.
247
+ */
248
+ async delete() {
249
+ await this.localStorage().delete(sqliteWebPersistenceKey(this.databaseName))
250
+ }
251
+
252
+ /**
253
+ * Loads the localStorage database entry.
254
+ * @returns {Promise<Uint8Array | undefined>} - Persisted bytes.
255
+ */
256
+ async load() {
257
+ const content = await this.localStorage().get(sqliteWebPersistenceKey(this.databaseName))
258
+
259
+ if (content === null || content === undefined) return undefined
260
+ if (content instanceof Uint8Array) return content
261
+ if (content instanceof ArrayBuffer) return new Uint8Array(content)
262
+
263
+ return /** @type {Uint8Array} */ (content)
264
+ }
265
+
266
+ /**
267
+ * Saves database bytes.
268
+ * @param {Uint8Array} content - Database bytes.
269
+ * @returns {Promise<void>} - Resolves when saved.
270
+ */
271
+ async save(content) {
272
+ await this.localStorage().set(sqliteWebPersistenceKey(this.databaseName), content)
273
+ }
274
+
275
+ /**
276
+ * Checks whether the legacy localStorage database exists.
277
+ * @returns {Promise<boolean>} - Whether content exists.
278
+ */
279
+ async exists() {
280
+ try {
281
+ const content = await this.localStorage().get(sqliteWebPersistenceKey(this.databaseName))
282
+
283
+ return content !== undefined && content !== null
284
+ } catch {
285
+ return false
286
+ }
287
+ }
288
+
289
+ /**
290
+ * Returns the localStorage wrapper.
291
+ * @returns {BetterLocalStorage} - Storage wrapper.
292
+ */
293
+ localStorage() {
294
+ this.storage ||= new BetterLocalStorage()
295
+
296
+ return this.storage
297
+ }
298
+ }
299
+
300
+
301
+ /**
302
+ * Selects the preferred available SQLite web persistence backend.
303
+ * @param {object} args - Arguments.
304
+ * @param {SqliteWebPersistenceEnvironment} args.environment - Browser-like environment.
305
+ * @param {IndexedDbPersistence} args.indexedDbPersistence - IndexedDB persistence adapter.
306
+ * @param {LocalStoragePersistence} args.localStoragePersistence - Legacy localStorage persistence adapter.
307
+ * @param {OpfsPersistence} args.opfsPersistence - OPFS persistence adapter.
308
+ * @returns {Promise<SqliteWebPersistence>} - Selected persistence adapter.
309
+ */
310
+ async function selectSupportedPersistence({environment, indexedDbPersistence, localStoragePersistence, opfsPersistence}) {
311
+ if (await supportsOpfsPersistence(environment)) return opfsPersistence
312
+ if (await supportsIndexedDbPersistence(environment)) return indexedDbPersistence
313
+
314
+ return localStoragePersistence
315
+ }
316
+
317
+ /**
318
+ * Migrates any existing database bytes into the selected persistence backend.
319
+ * @param {object} args - Arguments.
320
+ * @param {string} args.databaseName - Database name.
321
+ * @param {SqliteWebPersistence} args.destinationPersistence - Selected persistence adapter.
322
+ * @param {SqliteWebPersistenceEnvironment} args.environment - Browser-like environment.
323
+ * @param {{delete: () => Promise<void>, load: () => Promise<Uint8Array | undefined>}[]} args.sourcePersistences - Persistence adapters to scan for existing bytes.
324
+ * @returns {Promise<void>} - Resolves when migration is complete.
325
+ */
326
+ async function migratePersistedDatabase({databaseName, destinationPersistence, environment, sourcePersistences}) {
327
+ if (await destinationPersistence.load() !== undefined) return
328
+
329
+ for (const sourcePersistence of sourcePersistences) {
330
+ if (sourcePersistence === destinationPersistence) continue
331
+
332
+ const databaseBytes = await loadPersistenceIfAvailable(sourcePersistence)
333
+ if (databaseBytes === undefined) continue
334
+
335
+ await destinationPersistence.save(databaseBytes)
336
+ await deleteSqliteWebPersistences({databaseName, environment})
337
+ await destinationPersistence.save(databaseBytes)
338
+ return
339
+ }
340
+ }
341
+
342
+ /**
343
+ * Loads a persistence backend, ignoring unavailable backend errors.
344
+ * @param {{load: () => Promise<Uint8Array | undefined>}} persistence - Persistence adapter.
345
+ * @returns {Promise<Uint8Array | undefined>} - Persisted bytes, if available.
346
+ */
347
+ async function loadPersistenceIfAvailable(persistence) {
348
+ try {
349
+ return await persistence.load()
350
+ } catch {
351
+ return undefined
352
+ }
353
+ }
354
+
355
+ /**
356
+ * Tests whether OPFS persistence is usable.
357
+ * @param {SqliteWebPersistenceEnvironment} environment - Browser-like environment.
358
+ * @returns {Promise<boolean>} - Whether OPFS can be used.
359
+ */
360
+ async function supportsOpfsPersistence(environment) {
361
+ try {
362
+ const directory = await opfsDirectory(environment)
363
+ const fileHandle = await directory.getFileHandle(SUPPORT_CHECK_FILE, {create: true})
364
+ const writable = await fileHandle.createWritable()
365
+
366
+ await writable.write(arrayBufferFromBytes(SUPPORT_CHECK_BYTES))
367
+ await writable.close()
368
+
369
+ const file = await fileHandle.getFile()
370
+ const readBack = new Uint8Array(await file.arrayBuffer())
371
+
372
+ await directory.removeEntry(SUPPORT_CHECK_FILE)
373
+
374
+ return sameBytes(readBack, SUPPORT_CHECK_BYTES)
375
+ } catch {
376
+ return false
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Tests whether IndexedDB persistence is usable.
382
+ * @param {SqliteWebPersistenceEnvironment} environment - Browser-like environment.
383
+ * @returns {Promise<boolean>} - Whether IndexedDB can be used.
384
+ */
385
+ async function supportsIndexedDbPersistence(environment) {
386
+ try {
387
+ const database = await openIndexedDb(environment)
388
+ const store = database.transaction("databases", "readwrite").objectStore("databases")
389
+
390
+ await indexedDbRequest(store.put(SUPPORT_CHECK_BYTES, SUPPORT_CHECK_FILE))
391
+
392
+ const readBack = await indexedDbRequest(store.get(SUPPORT_CHECK_FILE))
393
+
394
+ await indexedDbRequest(store.delete(SUPPORT_CHECK_FILE))
395
+ database.close()
396
+
397
+ return readBack instanceof Uint8Array && sameBytes(readBack, SUPPORT_CHECK_BYTES)
398
+ } catch {
399
+ return false
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Opens the SQLite web IndexedDB database.
405
+ * @param {SqliteWebPersistenceEnvironment} environment - Browser-like environment.
406
+ * @returns {Promise<IDBDatabase>} - Open database.
407
+ */
408
+ async function openIndexedDb(environment) {
409
+ const indexedDb = indexedDbFromEnvironment(environment)
410
+
411
+ if (!indexedDb || typeof indexedDb.open !== "function") throw new Error("IndexedDB is not available")
412
+
413
+ const request = indexedDb.open("VelociousDatabaseDriversSqliteWeb", 1)
414
+ request.onupgradeneeded = () => {
415
+ const database = request.result
416
+
417
+ if (!database.objectStoreNames.contains("databases")) database.createObjectStore("databases")
418
+ }
419
+
420
+ return await indexedDbRequest(request)
421
+ }
422
+
423
+ /**
424
+ * Resolves an IndexedDB request.
425
+ * @template T
426
+ * @param {IDBRequest<T>} request - IndexedDB request.
427
+ * @returns {Promise<T>} - Request result.
428
+ */
429
+ function indexedDbRequest(request) {
430
+ return new Promise((resolve, reject) => {
431
+ request.onsuccess = () => resolve(request.result)
432
+ request.onerror = () => reject(request.error || new Error("IndexedDB request failed"))
433
+ })
434
+ }
435
+
436
+ /**
437
+ * Deletes a persistence backend, ignoring unavailable backend errors.
438
+ * @param {{delete: () => Promise<void>}} persistence - Persistence adapter.
439
+ * @returns {Promise<void>} - Resolves when deletion was attempted.
440
+ */
441
+ async function deletePersistenceIfAvailable(persistence) {
442
+ try {
443
+ await persistence.delete()
444
+ } catch {
445
+ // Ignore unavailable backends so reset clears every backend the browser can access.
446
+ }
447
+ }
448
+
449
+ /**
450
+ * Gets OPFS root directory.
451
+ * @param {SqliteWebPersistenceEnvironment} environment - Browser-like environment.
452
+ * @returns {Promise<FileSystemDirectoryHandle>} - OPFS root directory.
453
+ */
454
+ async function opfsDirectory(environment) {
455
+ const navigatorObject = navigatorFromEnvironment(environment)
456
+ const storage = navigatorObject.storage
457
+
458
+ if (!storage || typeof storage.getDirectory !== "function") throw new Error("OPFS is not available")
459
+
460
+ return await storage.getDirectory()
461
+ }
462
+
463
+ /**
464
+ * Gets navigator from environment.
465
+ * @param {SqliteWebPersistenceEnvironment} environment - Browser-like environment.
466
+ * @returns {{storage?: {getDirectory?: () => Promise<FileSystemDirectoryHandle>}}} - Navigator-like object.
467
+ */
468
+ function navigatorFromEnvironment(environment) {
469
+ const candidate = environment.navigator
470
+
471
+ if (!candidate || typeof candidate !== "object") return {}
472
+
473
+ return /** @type {{storage?: {getDirectory?: () => Promise<FileSystemDirectoryHandle>}}} */ (candidate)
474
+ }
475
+
476
+ /**
477
+ * Gets IndexedDB from environment.
478
+ * @param {SqliteWebPersistenceEnvironment} environment - Browser-like environment.
479
+ * @returns {{open?: (name: string, version?: number) => IDBOpenDBRequest} | undefined} - IndexedDB-like object.
480
+ */
481
+ function indexedDbFromEnvironment(environment) {
482
+ const candidate = environment.indexedDB
483
+
484
+ if (!candidate || typeof candidate !== "object") return undefined
485
+
486
+ return /** @type {{open?: (name: string, version?: number) => IDBOpenDBRequest}} */ (candidate)
487
+ }
488
+
489
+ /**
490
+ * Converts bytes to a standalone ArrayBuffer for browser file writes.
491
+ * @param {Uint8Array} bytes - Bytes to convert.
492
+ * @returns {ArrayBuffer} - Standalone ArrayBuffer.
493
+ */
494
+ function arrayBufferFromBytes(bytes) {
495
+ const copy = new Uint8Array(bytes.byteLength)
496
+
497
+ copy.set(bytes)
498
+
499
+ return copy.buffer
500
+ }
501
+
502
+ /**
503
+ * Checks whether an error is a file-not-found error.
504
+ * @param {unknown} error - Error candidate.
505
+ * @returns {boolean} - Whether the error is not found.
506
+ */
507
+ function isNotFoundError(error) {
508
+ return error instanceof Error && error.name === "NotFoundError"
509
+ }
510
+
511
+ /**
512
+ * Compares two byte arrays.
513
+ * @param {Uint8Array} left - Left bytes.
514
+ * @param {Uint8Array} right - Right bytes.
515
+ * @returns {boolean} - Whether bytes match.
516
+ */
517
+ function sameBytes(left, right) {
518
+ if (left.length !== right.length) return false
519
+
520
+ return left.every((value, index) => value === right[index])
521
+ }
@@ -442,6 +442,13 @@ export default class DbGenerateFrontendModels extends BaseCommand {
442
442
  }
443
443
  fileContent += " },\n"
444
444
  }
445
+ if (modelConfig.sync?.enabled) {
446
+ fileContent += this.formattedJsonProperty({
447
+ indent: " ",
448
+ propertyName: "sync",
449
+ value: modelConfig.sync
450
+ })
451
+ }
445
452
  fileContent += " }\n"
446
453
  fileContent += " }\n"
447
454
 
@@ -999,6 +1006,62 @@ export default class DbGenerateFrontendModels extends BaseCommand {
999
1006
  return output
1000
1007
  }
1001
1008
 
1009
+ /**
1010
+ * Runs formatted JSON property.
1011
+ * @param {object} args - Formatting args.
1012
+ * @param {string} args.indent - Base indentation.
1013
+ * @param {string} args.propertyName - Object property name.
1014
+ * @param {unknown} args.value - JSON-compatible value.
1015
+ * @returns {string} - Formatted property.
1016
+ */
1017
+ formattedJsonProperty({indent, propertyName, value}) {
1018
+ return `${indent}${propertyName}: ${this.formattedJsonValue({indent, value})},\n`
1019
+ }
1020
+
1021
+ /**
1022
+ * Runs formatted JSON value.
1023
+ * @param {object} args - Formatting args.
1024
+ * @param {string} args.indent - Indentation before this value.
1025
+ * @param {unknown} args.value - JSON-compatible value.
1026
+ * @returns {string} - Formatted value.
1027
+ */
1028
+ formattedJsonValue({indent, value}) {
1029
+ if (Array.isArray(value)) {
1030
+ let output = "[\n"
1031
+
1032
+ for (const entry of value) {
1033
+ output += `${indent} ${this.formattedJsonValue({indent: `${indent} `, value: entry})},\n`
1034
+ }
1035
+
1036
+ output += `${indent}]`
1037
+
1038
+ return output
1039
+ }
1040
+
1041
+ if (value && typeof value === "object") {
1042
+ let output = "{\n"
1043
+
1044
+ for (const key of Object.keys(value)) {
1045
+ output += `${indent} ${this.formattedObjectKey(key)}: ${this.formattedJsonValue({indent: `${indent} `, value: /** @type {Record<string, unknown>} */ (value)[key]})},\n`
1046
+ }
1047
+
1048
+ output += `${indent}}`
1049
+
1050
+ return output
1051
+ }
1052
+
1053
+ return JSON.stringify(value)
1054
+ }
1055
+
1056
+ /**
1057
+ * Runs formatted object key.
1058
+ * @param {string} key - Object key.
1059
+ * @returns {string} - JavaScript object key.
1060
+ */
1061
+ formattedObjectKey(key) {
1062
+ return /^[A-Za-z_$][\w$]*$/.test(key) ? key : JSON.stringify(key)
1063
+ }
1064
+
1002
1065
  /**
1003
1066
  * Runs attribute definitions for model.
1004
1067
  * @param {object} args - Arguments.
@@ -1439,7 +1502,7 @@ export default class DbGenerateFrontendModels extends BaseCommand {
1439
1502
  */
1440
1503
  frontendAttributeIsTranslated({attributeName, modelClass, resourceClass}) {
1441
1504
  if (resourceClass) {
1442
- const translatedAttributes = resourceClass.translatedAttributes
1505
+ const translatedAttributes = resourceClass.translatedAttributesConfig()
1443
1506
 
1444
1507
  if (Array.isArray(translatedAttributes) && translatedAttributes.includes(attributeName)) return true
1445
1508
  }