@prabhask5/stellar-engine 1.1.6 → 1.1.8

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 (265) hide show
  1. package/README.md +68 -25
  2. package/dist/actions/remoteChange.d.ts +143 -18
  3. package/dist/actions/remoteChange.d.ts.map +1 -1
  4. package/dist/actions/remoteChange.js +182 -58
  5. package/dist/actions/remoteChange.js.map +1 -1
  6. package/dist/actions/truncateTooltip.d.ts +56 -0
  7. package/dist/actions/truncateTooltip.d.ts.map +1 -0
  8. package/dist/actions/truncateTooltip.js +312 -0
  9. package/dist/actions/truncateTooltip.js.map +1 -0
  10. package/dist/auth/admin.d.ts +40 -3
  11. package/dist/auth/admin.d.ts.map +1 -1
  12. package/dist/auth/admin.js +45 -5
  13. package/dist/auth/admin.js.map +1 -1
  14. package/dist/auth/crypto.d.ts +55 -5
  15. package/dist/auth/crypto.d.ts.map +1 -1
  16. package/dist/auth/crypto.js +58 -5
  17. package/dist/auth/crypto.js.map +1 -1
  18. package/dist/auth/deviceVerification.d.ts +236 -20
  19. package/dist/auth/deviceVerification.d.ts.map +1 -1
  20. package/dist/auth/deviceVerification.js +293 -40
  21. package/dist/auth/deviceVerification.js.map +1 -1
  22. package/dist/auth/displayUtils.d.ts +98 -0
  23. package/dist/auth/displayUtils.d.ts.map +1 -0
  24. package/dist/auth/displayUtils.js +133 -0
  25. package/dist/auth/displayUtils.js.map +1 -0
  26. package/dist/auth/loginGuard.d.ts +108 -14
  27. package/dist/auth/loginGuard.d.ts.map +1 -1
  28. package/dist/auth/loginGuard.js +153 -31
  29. package/dist/auth/loginGuard.js.map +1 -1
  30. package/dist/auth/offlineCredentials.d.ts +132 -15
  31. package/dist/auth/offlineCredentials.d.ts.map +1 -1
  32. package/dist/auth/offlineCredentials.js +167 -23
  33. package/dist/auth/offlineCredentials.js.map +1 -1
  34. package/dist/auth/offlineLogin.d.ts +96 -10
  35. package/dist/auth/offlineLogin.d.ts.map +1 -1
  36. package/dist/auth/offlineLogin.js +82 -15
  37. package/dist/auth/offlineLogin.js.map +1 -1
  38. package/dist/auth/offlineSession.d.ts +83 -9
  39. package/dist/auth/offlineSession.d.ts.map +1 -1
  40. package/dist/auth/offlineSession.js +104 -13
  41. package/dist/auth/offlineSession.js.map +1 -1
  42. package/dist/auth/resolveAuthState.d.ts +70 -8
  43. package/dist/auth/resolveAuthState.d.ts.map +1 -1
  44. package/dist/auth/resolveAuthState.js +142 -46
  45. package/dist/auth/resolveAuthState.js.map +1 -1
  46. package/dist/auth/singleUser.d.ts +390 -37
  47. package/dist/auth/singleUser.d.ts.map +1 -1
  48. package/dist/auth/singleUser.js +505 -133
  49. package/dist/auth/singleUser.js.map +1 -1
  50. package/dist/bin/install-pwa.d.ts +25 -0
  51. package/dist/bin/install-pwa.d.ts.map +1 -0
  52. package/dist/bin/install-pwa.js +2197 -0
  53. package/dist/bin/install-pwa.js.map +1 -0
  54. package/dist/config.d.ts +132 -12
  55. package/dist/config.d.ts.map +1 -1
  56. package/dist/config.js +87 -9
  57. package/dist/config.js.map +1 -1
  58. package/dist/conflicts.d.ts +246 -23
  59. package/dist/conflicts.d.ts.map +1 -1
  60. package/dist/conflicts.js +495 -46
  61. package/dist/conflicts.js.map +1 -1
  62. package/dist/data.d.ts +338 -18
  63. package/dist/data.d.ts.map +1 -1
  64. package/dist/data.js +385 -34
  65. package/dist/data.js.map +1 -1
  66. package/dist/database.d.ts +72 -14
  67. package/dist/database.d.ts.map +1 -1
  68. package/dist/database.js +120 -29
  69. package/dist/database.js.map +1 -1
  70. package/dist/debug.d.ts +77 -1
  71. package/dist/debug.d.ts.map +1 -1
  72. package/dist/debug.js +88 -1
  73. package/dist/debug.js.map +1 -1
  74. package/dist/deviceId.d.ts +38 -7
  75. package/dist/deviceId.d.ts.map +1 -1
  76. package/dist/deviceId.js +68 -10
  77. package/dist/deviceId.js.map +1 -1
  78. package/dist/engine.d.ts +175 -3
  79. package/dist/engine.d.ts.map +1 -1
  80. package/dist/engine.js +831 -110
  81. package/dist/engine.js.map +1 -1
  82. package/dist/entries/actions.d.ts +14 -0
  83. package/dist/entries/actions.d.ts.map +1 -1
  84. package/dist/entries/actions.js +27 -1
  85. package/dist/entries/actions.js.map +1 -1
  86. package/dist/entries/auth.d.ts +16 -0
  87. package/dist/entries/auth.d.ts.map +1 -1
  88. package/dist/entries/auth.js +73 -1
  89. package/dist/entries/auth.js.map +1 -1
  90. package/dist/entries/config.d.ts +12 -0
  91. package/dist/entries/config.d.ts.map +1 -1
  92. package/dist/entries/config.js +18 -1
  93. package/dist/entries/config.js.map +1 -1
  94. package/dist/entries/kit.d.ts +21 -9
  95. package/dist/entries/kit.d.ts.map +1 -1
  96. package/dist/entries/kit.js +57 -8
  97. package/dist/entries/kit.js.map +1 -1
  98. package/dist/entries/stores.d.ts +11 -0
  99. package/dist/entries/stores.d.ts.map +1 -1
  100. package/dist/entries/stores.js +43 -2
  101. package/dist/entries/stores.js.map +1 -1
  102. package/dist/entries/types.d.ts +11 -1
  103. package/dist/entries/types.d.ts.map +1 -1
  104. package/dist/entries/types.js +10 -0
  105. package/dist/entries/types.js.map +1 -1
  106. package/dist/entries/utils.d.ts +7 -1
  107. package/dist/entries/utils.d.ts.map +1 -1
  108. package/dist/entries/utils.js +23 -2
  109. package/dist/entries/utils.js.map +1 -1
  110. package/dist/entries/vite.d.ts +20 -0
  111. package/dist/entries/vite.d.ts.map +1 -0
  112. package/dist/entries/vite.js +26 -0
  113. package/dist/entries/vite.js.map +1 -0
  114. package/dist/index.d.ts +33 -2
  115. package/dist/index.d.ts.map +1 -1
  116. package/dist/index.js +176 -21
  117. package/dist/index.js.map +1 -1
  118. package/dist/kit/auth.d.ts +80 -0
  119. package/dist/kit/auth.d.ts.map +1 -0
  120. package/dist/kit/auth.js +72 -0
  121. package/dist/kit/auth.js.map +1 -0
  122. package/dist/kit/confirm.d.ts +111 -0
  123. package/dist/kit/confirm.d.ts.map +1 -0
  124. package/dist/kit/confirm.js +169 -0
  125. package/dist/kit/confirm.js.map +1 -0
  126. package/dist/kit/loads.d.ts +189 -0
  127. package/dist/kit/loads.d.ts.map +1 -0
  128. package/dist/kit/loads.js +205 -0
  129. package/dist/kit/loads.js.map +1 -0
  130. package/dist/kit/server.d.ts +175 -0
  131. package/dist/kit/server.d.ts.map +1 -0
  132. package/dist/kit/server.js +297 -0
  133. package/dist/kit/server.js.map +1 -0
  134. package/dist/kit/sw.d.ts +176 -0
  135. package/dist/kit/sw.d.ts.map +1 -0
  136. package/dist/kit/sw.js +320 -0
  137. package/dist/kit/sw.js.map +1 -0
  138. package/dist/queue.d.ts +274 -0
  139. package/dist/queue.d.ts.map +1 -1
  140. package/dist/queue.js +556 -38
  141. package/dist/queue.js.map +1 -1
  142. package/dist/realtime.d.ts +241 -27
  143. package/dist/realtime.d.ts.map +1 -1
  144. package/dist/realtime.js +633 -109
  145. package/dist/realtime.js.map +1 -1
  146. package/dist/runtime/runtimeConfig.d.ts +91 -16
  147. package/dist/runtime/runtimeConfig.d.ts.map +1 -1
  148. package/dist/runtime/runtimeConfig.js +146 -19
  149. package/dist/runtime/runtimeConfig.js.map +1 -1
  150. package/dist/stores/authState.d.ts +150 -11
  151. package/dist/stores/authState.d.ts.map +1 -1
  152. package/dist/stores/authState.js +169 -17
  153. package/dist/stores/authState.js.map +1 -1
  154. package/dist/stores/network.d.ts +39 -0
  155. package/dist/stores/network.d.ts.map +1 -1
  156. package/dist/stores/network.js +169 -16
  157. package/dist/stores/network.js.map +1 -1
  158. package/dist/stores/remoteChanges.d.ts +327 -52
  159. package/dist/stores/remoteChanges.d.ts.map +1 -1
  160. package/dist/stores/remoteChanges.js +337 -75
  161. package/dist/stores/remoteChanges.js.map +1 -1
  162. package/dist/stores/sync.d.ts +130 -0
  163. package/dist/stores/sync.d.ts.map +1 -1
  164. package/dist/stores/sync.js +167 -7
  165. package/dist/stores/sync.js.map +1 -1
  166. package/dist/supabase/auth.d.ts +326 -19
  167. package/dist/supabase/auth.d.ts.map +1 -1
  168. package/dist/supabase/auth.js +374 -26
  169. package/dist/supabase/auth.js.map +1 -1
  170. package/dist/supabase/client.d.ts +79 -6
  171. package/dist/supabase/client.d.ts.map +1 -1
  172. package/dist/supabase/client.js +158 -15
  173. package/dist/supabase/client.js.map +1 -1
  174. package/dist/supabase/validate.d.ts +101 -7
  175. package/dist/supabase/validate.d.ts.map +1 -1
  176. package/dist/supabase/validate.js +117 -8
  177. package/dist/supabase/validate.js.map +1 -1
  178. package/dist/sw/build/vite-plugin.d.ts +74 -0
  179. package/dist/sw/build/vite-plugin.d.ts.map +1 -0
  180. package/dist/sw/build/vite-plugin.js +183 -0
  181. package/dist/sw/build/vite-plugin.js.map +1 -0
  182. package/dist/sw/sw.js +669 -0
  183. package/dist/types.d.ts +150 -45
  184. package/dist/types.d.ts.map +1 -1
  185. package/dist/types.js +12 -10
  186. package/dist/types.js.map +1 -1
  187. package/dist/utils.d.ts +55 -13
  188. package/dist/utils.d.ts.map +1 -1
  189. package/dist/utils.js +83 -22
  190. package/dist/utils.js.map +1 -1
  191. package/package.json +20 -22
  192. package/src/components/DeferredChangesBanner.svelte +477 -0
  193. package/src/components/SyncStatus.svelte +1732 -0
  194. package/dist/crdt/awareness.d.ts +0 -54
  195. package/dist/crdt/awareness.d.ts.map +0 -1
  196. package/dist/crdt/awareness.js +0 -219
  197. package/dist/crdt/awareness.js.map +0 -1
  198. package/dist/crdt/doc.d.ts +0 -56
  199. package/dist/crdt/doc.d.ts.map +0 -1
  200. package/dist/crdt/doc.js +0 -130
  201. package/dist/crdt/doc.js.map +0 -1
  202. package/dist/crdt/index.d.ts +0 -15
  203. package/dist/crdt/index.d.ts.map +0 -1
  204. package/dist/crdt/index.js +0 -20
  205. package/dist/crdt/index.js.map +0 -1
  206. package/dist/crdt/offline.d.ts +0 -91
  207. package/dist/crdt/offline.d.ts.map +0 -1
  208. package/dist/crdt/offline.js +0 -353
  209. package/dist/crdt/offline.js.map +0 -1
  210. package/dist/crdt/sync.d.ts +0 -58
  211. package/dist/crdt/sync.d.ts.map +0 -1
  212. package/dist/crdt/sync.js +0 -399
  213. package/dist/crdt/sync.js.map +0 -1
  214. package/dist/crdt/types.d.ts +0 -62
  215. package/dist/crdt/types.d.ts.map +0 -1
  216. package/dist/crdt/types.js +0 -7
  217. package/dist/crdt/types.js.map +0 -1
  218. package/dist/email/sendEmail.d.ts +0 -31
  219. package/dist/email/sendEmail.d.ts.map +0 -1
  220. package/dist/email/sendEmail.js +0 -39
  221. package/dist/email/sendEmail.js.map +0 -1
  222. package/dist/email/validateSmtp.d.ts +0 -18
  223. package/dist/email/validateSmtp.d.ts.map +0 -1
  224. package/dist/email/validateSmtp.js +0 -33
  225. package/dist/email/validateSmtp.js.map +0 -1
  226. package/dist/entries/crdt.d.ts +0 -3
  227. package/dist/entries/crdt.d.ts.map +0 -1
  228. package/dist/entries/crdt.js +0 -13
  229. package/dist/entries/crdt.js.map +0 -1
  230. package/dist/entries/email.d.ts +0 -4
  231. package/dist/entries/email.d.ts.map +0 -1
  232. package/dist/entries/email.js +0 -4
  233. package/dist/entries/email.js.map +0 -1
  234. package/dist/kit/authPresets.d.ts +0 -28
  235. package/dist/kit/authPresets.d.ts.map +0 -1
  236. package/dist/kit/authPresets.js +0 -23
  237. package/dist/kit/authPresets.js.map +0 -1
  238. package/dist/kit/configEndpoint.d.ts +0 -18
  239. package/dist/kit/configEndpoint.d.ts.map +0 -1
  240. package/dist/kit/configEndpoint.js +0 -27
  241. package/dist/kit/configEndpoint.js.map +0 -1
  242. package/dist/kit/deployEndpoint.d.ts +0 -22
  243. package/dist/kit/deployEndpoint.d.ts.map +0 -1
  244. package/dist/kit/deployEndpoint.js +0 -79
  245. package/dist/kit/deployEndpoint.js.map +0 -1
  246. package/dist/kit/layoutLoad.d.ts +0 -23
  247. package/dist/kit/layoutLoad.d.ts.map +0 -1
  248. package/dist/kit/layoutLoad.js +0 -41
  249. package/dist/kit/layoutLoad.js.map +0 -1
  250. package/dist/kit/protectedLoad.d.ts +0 -16
  251. package/dist/kit/protectedLoad.d.ts.map +0 -1
  252. package/dist/kit/protectedLoad.js +0 -28
  253. package/dist/kit/protectedLoad.js.map +0 -1
  254. package/dist/kit/setupLoad.d.ts +0 -11
  255. package/dist/kit/setupLoad.d.ts.map +0 -1
  256. package/dist/kit/setupLoad.js +0 -28
  257. package/dist/kit/setupLoad.js.map +0 -1
  258. package/dist/kit/validateEndpoint.d.ts +0 -9
  259. package/dist/kit/validateEndpoint.d.ts.map +0 -1
  260. package/dist/kit/validateEndpoint.js +0 -25
  261. package/dist/kit/validateEndpoint.js.map +0 -1
  262. package/dist/kit/vercelApi.d.ts +0 -6
  263. package/dist/kit/vercelApi.d.ts.map +0 -1
  264. package/dist/kit/vercelApi.js +0 -48
  265. package/dist/kit/vercelApi.js.map +0 -1
package/dist/data.js CHANGED
@@ -1,8 +1,31 @@
1
1
  /**
2
- * Generic CRUD and Query Operations
2
+ * @fileoverview Generic CRUD and Query Operations for the Stellar Sync Engine
3
3
  *
4
- * Replaces repository infrastructure boilerplate.
5
- * Uses Supabase table names as the API surface, internally resolves to Dexie table names.
4
+ * This module serves as the primary data access layer for the sync engine,
5
+ * replacing per-entity repository boilerplate with a unified, table-driven API.
6
+ *
7
+ * Architecture:
8
+ * - Callers reference tables by their **Supabase** name (the remote/canonical name).
9
+ * - Internally, every operation resolves that name to the corresponding **Dexie**
10
+ * (IndexedDB) table name via the configured table map.
11
+ * - All write operations (create, update, delete, increment, batch) follow the
12
+ * same transactional pattern:
13
+ * 1. Open a Dexie read-write transaction spanning the target table + syncQueue.
14
+ * 2. Apply the mutation locally.
15
+ * 3. Enqueue the corresponding sync operation for eventual push to Supabase.
16
+ * 4. After commit, mark the entity as modified and schedule a sync push.
17
+ * - All read operations query Dexie first, with an optional remote fallback that
18
+ * fetches from Supabase when the local store is empty and the device is online.
19
+ *
20
+ * This dual-layer design enables full offline-first functionality: the app works
21
+ * against the local Dexie store, and the sync queue ensures changes propagate to
22
+ * the server when connectivity is available.
23
+ *
24
+ * @see {@link ./config} for table map and column configuration
25
+ * @see {@link ./database} for Dexie database instance management
26
+ * @see {@link ./queue} for sync queue enqueueing operations
27
+ * @see {@link ./engine} for sync push scheduling and entity modification tracking
28
+ * @see {@link ./conflicts} for conflict resolution during sync pull
6
29
  */
7
30
  import { getTableMap, getTableColumns } from './config';
8
31
  import { getDb } from './database';
@@ -11,37 +34,106 @@ import { markEntityModified, scheduleSyncPush } from './engine';
11
34
  import { generateId, now } from './utils';
12
35
  import { debugError } from './debug';
13
36
  import { supabase } from './supabase/client';
14
- // ============================================================
37
+ // =============================================================================
15
38
  // HELPERS
16
- // ============================================================
39
+ // =============================================================================
40
+ /**
41
+ * Resolve a Supabase (remote) table name to its corresponding Dexie (local) table name.
42
+ *
43
+ * The table map is defined by the host application at initialization time via
44
+ * `configureStellarEngine`. If no mapping exists for the given name, the
45
+ * Supabase name is returned as-is (identity mapping), which is the common case
46
+ * when local and remote table names are identical.
47
+ *
48
+ * @param supabaseName - The canonical Supabase table name used by the caller.
49
+ * @returns The corresponding Dexie table name for local IndexedDB operations.
50
+ *
51
+ * @see {@link ./config} for `getTableMap` and engine configuration
52
+ */
17
53
  function getDexieTableName(supabaseName) {
18
54
  const map = getTableMap();
19
55
  return map[supabaseName] || supabaseName;
20
56
  }
21
- // ============================================================
57
+ // =============================================================================
22
58
  // SINGLE-ENTITY WRITE OPERATIONS
23
- // ============================================================
59
+ // =============================================================================
24
60
  /**
25
- * Create a new entity. Auto: transaction + queue + markModified + schedulePush.
26
- * Caller provides all fields including id, timestamps, etc.
61
+ * Create a new entity in the local store and enqueue it for remote sync.
62
+ *
63
+ * This is the primary entry point for all entity creation. It performs the
64
+ * following steps atomically within a single Dexie transaction:
65
+ * 1. Inserts the entity into the local Dexie table.
66
+ * 2. Enqueues a `create` operation in the sync queue.
67
+ *
68
+ * After the transaction commits, it marks the entity as modified (for reactive
69
+ * UI updates) and schedules a sync push to propagate the change to Supabase.
70
+ *
71
+ * The caller is responsible for providing all required fields (including
72
+ * timestamps like `created_at` and `updated_at`). If `data.id` is omitted,
73
+ * a new UUID is generated automatically.
74
+ *
75
+ * @param table - The Supabase table name (resolved internally to a Dexie table).
76
+ * @param data - The full entity payload. May include `id`; if absent, one is generated.
77
+ * @returns The created entity payload (with `id` guaranteed to be present).
78
+ *
79
+ * @throws {Dexie.ConstraintError} If an entity with the same `id` already exists.
80
+ *
81
+ * @example
82
+ * ```ts
83
+ * const task = await engineCreate('tasks', {
84
+ * title: 'Write docs',
85
+ * user_id: currentUserId,
86
+ * created_at: now(),
87
+ * updated_at: now(),
88
+ * });
89
+ * console.log(task.id); // auto-generated UUID
90
+ * ```
91
+ *
92
+ * @see {@link engineBatchWrite} for creating multiple entities atomically
93
+ * @see {@link queueCreateOperation} for the sync queue entry format
27
94
  */
28
95
  export async function engineCreate(table, data) {
29
96
  const db = getDb();
30
97
  const dexieTable = getDexieTableName(table);
31
98
  const entityId = data.id || generateId();
32
99
  const payload = { ...data, id: entityId };
33
- // Separate out id for the queue payload (queue stores id separately)
100
+ /* The queue stores `id` as a separate column, so we strip it from the payload
101
+ to avoid duplicating it in the serialized operation data. */
34
102
  const { id: _id, ...queuePayload } = payload;
35
103
  await db.transaction('rw', [db.table(dexieTable), db.table('syncQueue')], async () => {
36
104
  await db.table(dexieTable).add(payload);
37
105
  await queueCreateOperation(table, entityId, queuePayload);
38
106
  });
107
+ /* Post-transaction side effects: these are intentionally outside the transaction
108
+ because they are non-critical (UI reactivity + debounced network push). */
39
109
  markEntityModified(entityId);
40
110
  scheduleSyncPush();
41
111
  return payload;
42
112
  }
43
113
  /**
44
- * Update an entity's fields. Auto-sets updated_at, queues sync, marks modified.
114
+ * Update specific fields on an existing entity.
115
+ *
116
+ * Automatically sets `updated_at` to the current timestamp, enqueues a `set`
117
+ * sync operation, and notifies the engine of the modification. The update and
118
+ * queue entry are wrapped in a single transaction for atomicity.
119
+ *
120
+ * If the entity does not exist (e.g., it was deleted between the caller's
121
+ * check and this call), the sync operation is skipped and `undefined` is
122
+ * returned -- no orphan queue entries are created.
123
+ *
124
+ * @param table - The Supabase table name.
125
+ * @param id - The primary key of the entity to update.
126
+ * @param fields - A partial record of fields to merge into the entity.
127
+ * @returns The fully updated entity record, or `undefined` if the entity was not found.
128
+ *
129
+ * @example
130
+ * ```ts
131
+ * const updated = await engineUpdate('tasks', taskId, { title: 'New title' });
132
+ * // updated.updated_at is automatically set
133
+ * ```
134
+ *
135
+ * @see {@link engineIncrement} for numeric field increments with conflict-safe semantics
136
+ * @see {@link queueSyncOperation} for the `set` operation queue format
45
137
  */
46
138
  export async function engineUpdate(table, id, fields) {
47
139
  const db = getDb();
@@ -51,6 +143,8 @@ export async function engineUpdate(table, id, fields) {
51
143
  let updated;
52
144
  await db.transaction('rw', [db.table(dexieTable), db.table('syncQueue')], async () => {
53
145
  await db.table(dexieTable).update(id, updateFields);
146
+ /* Re-read the entity after update to return the complete merged record,
147
+ and to verify the entity actually existed before queuing a sync op. */
54
148
  updated = await db.table(dexieTable).get(id);
55
149
  if (updated) {
56
150
  await queueSyncOperation({
@@ -68,7 +162,28 @@ export async function engineUpdate(table, id, fields) {
68
162
  return updated;
69
163
  }
70
164
  /**
71
- * Soft-delete an entity. Sets deleted=true, queues delete op.
165
+ * Soft-delete an entity by setting `deleted: true`.
166
+ *
167
+ * The engine uses soft deletes rather than hard deletes so that the deletion
168
+ * can be synced to other devices and to the server. The sync queue receives a
169
+ * `delete` operation, which the push logic translates into a Supabase update
170
+ * that sets `deleted = true` on the remote row.
171
+ *
172
+ * The entity remains in the local Dexie store (with `deleted: true`) until a
173
+ * future compaction or full re-sync removes it.
174
+ *
175
+ * @param table - The Supabase table name.
176
+ * @param id - The primary key of the entity to soft-delete.
177
+ * @returns Resolves when the local update and queue entry are committed.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * await engineDelete('tasks', taskId);
182
+ * // The task still exists locally with deleted: true
183
+ * // It will be synced as a deletion on the next push
184
+ * ```
185
+ *
186
+ * @see {@link queueDeleteOperation} for the delete queue entry format
72
187
  */
73
188
  export async function engineDelete(table, id) {
74
189
  const db = getDb();
@@ -82,13 +197,43 @@ export async function engineDelete(table, id) {
82
197
  scheduleSyncPush();
83
198
  }
84
199
  /**
85
- * Execute multiple write operations in a single atomic transaction.
86
- * All ops succeed or all roll back.
200
+ * Execute multiple write operations in a single atomic Dexie transaction.
201
+ *
202
+ * This is the preferred way to perform related mutations that must succeed or
203
+ * fail together (e.g., creating a parent entity and its children, or moving an
204
+ * item from one list to another). All operations share a single `updated_at`
205
+ * timestamp for consistency.
206
+ *
207
+ * Transaction scope is dynamically computed: only the Dexie tables referenced
208
+ * by the operations (plus `syncQueue`) are locked, minimizing contention.
209
+ *
210
+ * After the transaction commits, all modified entity IDs are marked as modified
211
+ * in a single pass, and a single sync push is scheduled (not one per operation).
212
+ *
213
+ * @param operations - An ordered array of create/update/delete operations.
214
+ * @returns Resolves when all operations have been committed.
215
+ *
216
+ * @throws {Dexie.AbortError} If any operation fails, the entire batch is rolled back.
217
+ *
218
+ * @example
219
+ * ```ts
220
+ * await engineBatchWrite([
221
+ * { type: 'create', table: 'tasks', data: { title: 'Subtask 1', parent_id: parentId } },
222
+ * { type: 'create', table: 'tasks', data: { title: 'Subtask 2', parent_id: parentId } },
223
+ * { type: 'update', table: 'projects', id: projectId, fields: { task_count: newCount } },
224
+ * ]);
225
+ * ```
226
+ *
227
+ * @see {@link engineCreate} for single-entity create semantics
228
+ * @see {@link engineUpdate} for single-entity update semantics
229
+ * @see {@link engineDelete} for single-entity delete semantics
87
230
  */
88
231
  export async function engineBatchWrite(operations) {
89
232
  const db = getDb();
90
233
  const timestamp = now();
91
- // Collect all unique Dexie table names needed for the transaction scope
234
+ /* Collect all unique Dexie table names needed for the transaction scope.
235
+ We pre-compute this so Dexie can acquire the minimal set of table locks
236
+ upfront, avoiding deadlocks with concurrent transactions. */
92
237
  const tableNames = new Set();
93
238
  tableNames.add('syncQueue');
94
239
  for (const op of operations) {
@@ -130,18 +275,50 @@ export async function engineBatchWrite(operations) {
130
275
  }
131
276
  }
132
277
  });
278
+ /* Batch-notify all modified entities after the transaction commits.
279
+ A single scheduleSyncPush() call is sufficient because the push logic
280
+ drains the entire queue, not just one entry. */
133
281
  for (const id of modifiedIds) {
134
282
  markEntityModified(id);
135
283
  }
136
284
  scheduleSyncPush();
137
285
  }
138
- // ============================================================
286
+ // =============================================================================
139
287
  // INCREMENT OPERATION
140
- // ============================================================
288
+ // =============================================================================
141
289
  /**
142
- * Increment a numeric field on an entity, preserving increment intent for conflict resolution.
143
- * Uses increment operationType in the sync queue so multi-device increments are additive.
144
- * Optionally sets additional fields (e.g., completed, updated_at) alongside the increment.
290
+ * Atomically increment a numeric field on an entity.
291
+ *
292
+ * Unlike a plain `engineUpdate` with a computed value, this function preserves
293
+ * the **increment intent** in the sync queue (operationType: 'increment').
294
+ * This is critical for correct multi-device conflict resolution: when two
295
+ * devices each increment a counter by 1, the server can apply both increments
296
+ * additively (+2) rather than last-write-wins (which would yield +1).
297
+ *
298
+ * The local Dexie value is updated immediately (read-modify-write inside a
299
+ * transaction to prevent TOCTOU races). If additional fields need to be set
300
+ * alongside the increment (e.g., a `completed` flag), they are queued as a
301
+ * separate `set` operation so the increment and set semantics remain distinct.
302
+ *
303
+ * @param table - The Supabase table name.
304
+ * @param id - The primary key of the entity to increment.
305
+ * @param field - The name of the numeric field to increment.
306
+ * @param amount - The increment delta (can be negative for decrements).
307
+ * @param additionalFields - Optional extra fields to set alongside the increment
308
+ * (e.g., `{ completed: true }`). These are queued as a
309
+ * separate `set` operation.
310
+ * @returns The fully updated entity record, or `undefined` if the entity was not found.
311
+ *
312
+ * @example
313
+ * ```ts
314
+ * // Increment a task's focus_count by 1 and mark it as touched
315
+ * const updated = await engineIncrement('tasks', taskId, 'focus_count', 1, {
316
+ * last_focused_at: now(),
317
+ * });
318
+ * ```
319
+ *
320
+ * @see {@link engineUpdate} for non-increment field updates
321
+ * @see {@link ./conflicts} for how increment operations are resolved during sync
145
322
  */
146
323
  export async function engineIncrement(table, id, field, amount, additionalFields) {
147
324
  const db = getDb();
@@ -149,7 +326,9 @@ export async function engineIncrement(table, id, field, amount, additionalFields
149
326
  const timestamp = now();
150
327
  let updated;
151
328
  await db.transaction('rw', [db.table(dexieTable), db.table('syncQueue')], async () => {
152
- // Read current value inside transaction to prevent TOCTOU race
329
+ /* Read current value inside the transaction to prevent TOCTOU race:
330
+ another tab or transaction could modify the value between our read
331
+ and write if we read outside the transaction boundary. */
153
332
  const current = await db.table(dexieTable).get(id);
154
333
  if (!current)
155
334
  return;
@@ -163,6 +342,8 @@ export async function engineIncrement(table, id, field, amount, additionalFields
163
342
  await db.table(dexieTable).update(id, updateFields);
164
343
  updated = await db.table(dexieTable).get(id);
165
344
  if (updated) {
345
+ /* Queue the increment as its own operation type so the sync push
346
+ can send it as an RPC / SQL increment rather than a flat set. */
166
347
  await queueSyncOperation({
167
348
  table,
168
349
  entityId: id,
@@ -170,7 +351,9 @@ export async function engineIncrement(table, id, field, amount, additionalFields
170
351
  field,
171
352
  value: amount
172
353
  });
173
- // Queue additional fields as a separate set operation if present
354
+ /* Queue additional fields as a separate set operation if present.
355
+ Keeping them separate ensures the increment semantic is not
356
+ conflated with plain field overwrites during conflict resolution. */
174
357
  if (additionalFields && Object.keys(additionalFields).length > 0) {
175
358
  await queueSyncOperation({
176
359
  table,
@@ -187,11 +370,39 @@ export async function engineIncrement(table, id, field, amount, additionalFields
187
370
  }
188
371
  return updated;
189
372
  }
190
- // ============================================================
373
+ // =============================================================================
191
374
  // QUERY OPERATIONS
192
- // ============================================================
375
+ // =============================================================================
193
376
  /**
194
- * Get a single entity by ID. Optional remote fallback if not found locally.
377
+ * Retrieve a single entity by its primary key.
378
+ *
379
+ * Queries the local Dexie store first. If the entity is not found locally and
380
+ * `remoteFallback` is enabled (and the device is online), a single-row fetch
381
+ * is made from Supabase. The remote result is cached locally in Dexie for
382
+ * subsequent offline access.
383
+ *
384
+ * The remote fallback filters out soft-deleted rows (`deleted IS NULL OR deleted = false`)
385
+ * to avoid resurrecting deleted entities.
386
+ *
387
+ * @param table - The Supabase table name.
388
+ * @param id - The primary key of the entity to retrieve.
389
+ * @param opts - Optional configuration.
390
+ * @param opts.remoteFallback - If `true`, fall back to a Supabase query when
391
+ * the entity is not found locally. Defaults to `false`.
392
+ * @returns The entity record, or `null` if not found (locally or remotely).
393
+ *
394
+ * @example
395
+ * ```ts
396
+ * // Local-only lookup (fast, offline-safe)
397
+ * const task = await engineGet('tasks', taskId);
398
+ *
399
+ * // With remote fallback for cache misses
400
+ * const task = await engineGet('tasks', taskId, { remoteFallback: true });
401
+ * ```
402
+ *
403
+ * @see {@link engineGetAll} for retrieving all entities from a table
404
+ * @see {@link engineQuery} for index-based filtered queries
405
+ * @see {@link getTableColumns} for column projection on remote queries
195
406
  */
196
407
  export async function engineGet(table, id, opts) {
197
408
  const db = getDb();
@@ -199,6 +410,8 @@ export async function engineGet(table, id, opts) {
199
410
  const local = await db.table(dexieTable).get(id);
200
411
  if (local)
201
412
  return local;
413
+ /* Remote fallback: only attempted when explicitly opted in AND the browser
414
+ reports online status. This avoids unnecessary network errors in offline mode. */
202
415
  if (opts?.remoteFallback && typeof navigator !== 'undefined' && navigator.onLine) {
203
416
  try {
204
417
  const columns = getTableColumns(table);
@@ -209,6 +422,7 @@ export async function engineGet(table, id, opts) {
209
422
  .or('deleted.is.null,deleted.eq.false')
210
423
  .maybeSingle();
211
424
  if (!error && data) {
425
+ /* Cache the remote result locally so future reads are instant and offline-safe. */
212
426
  await db.table(dexieTable).put(data);
213
427
  return data;
214
428
  }
@@ -220,7 +434,35 @@ export async function engineGet(table, id, opts) {
220
434
  return null;
221
435
  }
222
436
  /**
223
- * Get all entities from a table. Optional ordering and remote fallback.
437
+ * Retrieve all entities from a table, with optional ordering and remote fallback.
438
+ *
439
+ * Returns the full (non-filtered) contents of the local Dexie table. If the
440
+ * local table is empty and `remoteFallback` is enabled, a bulk fetch from
441
+ * Supabase is performed and results are cached locally via `bulkPut`.
442
+ *
443
+ * Note: This does NOT filter out soft-deleted entities locally. Callers that
444
+ * need to exclude deleted records should filter the results themselves. The
445
+ * remote fallback, however, does exclude deleted rows to avoid pulling down
446
+ * tombstones.
447
+ *
448
+ * @param table - The Supabase table name.
449
+ * @param opts - Optional configuration.
450
+ * @param opts.orderBy - A Dexie-indexed field name to sort results by.
451
+ * @param opts.remoteFallback - If `true`, fall back to Supabase when the local
452
+ * table is empty. Defaults to `false`.
453
+ * @returns An array of entity records (may be empty).
454
+ *
455
+ * @example
456
+ * ```ts
457
+ * // Get all tasks ordered by creation date
458
+ * const tasks = await engineGetAll('tasks', { orderBy: 'created_at' });
459
+ *
460
+ * // Bootstrap from remote on first load
461
+ * const tasks = await engineGetAll('tasks', { remoteFallback: true });
462
+ * ```
463
+ *
464
+ * @see {@link engineGet} for single-entity retrieval
465
+ * @see {@link engineQuery} for filtered queries by index
224
466
  */
225
467
  export async function engineGetAll(table, opts) {
226
468
  const db = getDb();
@@ -232,6 +474,9 @@ export async function engineGetAll(table, opts) {
232
474
  else {
233
475
  results = await db.table(dexieTable).toArray();
234
476
  }
477
+ /* Remote fallback only fires when the local table is completely empty.
478
+ This handles the "first device" or "fresh install" scenario where no
479
+ data has been synced down yet. */
235
480
  if (results.length === 0 &&
236
481
  opts?.remoteFallback &&
237
482
  typeof navigator !== 'undefined' &&
@@ -244,6 +489,8 @@ export async function engineGetAll(table, opts) {
244
489
  .or('deleted.is.null,deleted.eq.false');
245
490
  if (!error && data && data.length > 0) {
246
491
  await db.table(dexieTable).bulkPut(data);
492
+ /* If ordering was requested, re-read from Dexie to get proper index-based
493
+ ordering rather than relying on Supabase's default sort order. */
247
494
  if (opts?.orderBy) {
248
495
  results = await db.table(dexieTable).orderBy(opts.orderBy).toArray();
249
496
  }
@@ -259,7 +506,31 @@ export async function engineGetAll(table, opts) {
259
506
  return results;
260
507
  }
261
508
  /**
262
- * Query entities by index value (WHERE index = value).
509
+ * Query entities by a single indexed field value (equivalent to `WHERE index = value`).
510
+ *
511
+ * Uses Dexie's indexed `where().equals()` for efficient local lookups. If no
512
+ * results are found locally and `remoteFallback` is enabled, a filtered query
513
+ * is made against Supabase and results are cached locally.
514
+ *
515
+ * @param table - The Supabase table name.
516
+ * @param index - The name of the indexed field to filter on.
517
+ * @param value - The value to match against the indexed field.
518
+ * @param opts - Optional configuration.
519
+ * @param opts.remoteFallback - If `true`, fall back to Supabase when no local
520
+ * results are found. Defaults to `false`.
521
+ * @returns An array of matching entity records.
522
+ *
523
+ * @example
524
+ * ```ts
525
+ * // Get all tasks belonging to a specific project
526
+ * const tasks = await engineQuery('tasks', 'project_id', projectId);
527
+ *
528
+ * // With remote fallback for initial sync scenarios
529
+ * const tasks = await engineQuery('tasks', 'user_id', userId, { remoteFallback: true });
530
+ * ```
531
+ *
532
+ * @see {@link engineQueryRange} for range-based queries (BETWEEN)
533
+ * @see {@link engineGetAll} for unfiltered table scans
263
534
  */
264
535
  export async function engineQuery(table, index, value, opts) {
265
536
  const db = getDb();
@@ -281,6 +552,9 @@ export async function engineQuery(table, index, value, opts) {
281
552
  .eq(index, value)
282
553
  .or('deleted.is.null,deleted.eq.false');
283
554
  if (!error && data && data.length > 0) {
555
+ /* Cache remote results locally for future offline access. bulkPut is
556
+ used instead of bulkAdd to handle the case where some records may
557
+ already exist locally (e.g., partial sync). */
284
558
  await db.table(dexieTable).bulkPut(data);
285
559
  results = data;
286
560
  }
@@ -292,11 +566,39 @@ export async function engineQuery(table, index, value, opts) {
292
566
  return results;
293
567
  }
294
568
  /**
295
- * Range query (WHERE index BETWEEN lower AND upper).
569
+ * Query entities where an indexed field falls within an inclusive range.
570
+ *
571
+ * Equivalent to `WHERE index BETWEEN lower AND upper` (inclusive on both ends).
572
+ * Useful for date-range queries (e.g., "all tasks due this week") or numeric
573
+ * range filters.
574
+ *
575
+ * Like other query functions, supports an optional remote fallback for when the
576
+ * local store has no matching results.
577
+ *
578
+ * @param table - The Supabase table name.
579
+ * @param index - The name of the indexed field to filter on.
580
+ * @param lower - The inclusive lower bound of the range.
581
+ * @param upper - The inclusive upper bound of the range.
582
+ * @param opts - Optional configuration.
583
+ * @param opts.remoteFallback - If `true`, fall back to Supabase when no local
584
+ * results are found. Defaults to `false`.
585
+ * @returns An array of matching entity records within the range.
586
+ *
587
+ * @example
588
+ * ```ts
589
+ * // Get all tasks due between Monday and Friday
590
+ * const tasks = await engineQueryRange('tasks', 'due_date', mondayISO, fridayISO);
591
+ *
592
+ * // Get focus sessions within a score range
593
+ * const sessions = await engineQueryRange('focus_sessions', 'score', 80, 100);
594
+ * ```
595
+ *
596
+ * @see {@link engineQuery} for exact-match queries
296
597
  */
297
598
  export async function engineQueryRange(table, index, lower, upper, opts) {
298
599
  const db = getDb();
299
600
  const dexieTable = getDexieTableName(table);
601
+ /* The `true, true` arguments make both bounds inclusive (closed interval). */
300
602
  let results = await db.table(dexieTable).where(index).between(lower, upper, true, true).toArray();
301
603
  if (results.length === 0 &&
302
604
  opts?.remoteFallback &&
@@ -321,23 +623,69 @@ export async function engineQueryRange(table, index, lower, upper, opts) {
321
623
  }
322
624
  return results;
323
625
  }
626
+ // =============================================================================
627
+ // GET-OR-CREATE (SINGLETON PATTERN)
628
+ // =============================================================================
324
629
  /**
325
- * Singleton get-or-create with optional remote check.
326
- * Used for patterns like focus_settings where one record per user exists.
630
+ * Retrieve an existing entity by index, or create one with defaults if none exists.
631
+ *
632
+ * Implements the singleton/get-or-create pattern commonly used for per-user
633
+ * settings records (e.g., `focus_settings`) where exactly one row per user
634
+ * should exist. The lookup uses an indexed field (typically `user_id`) rather
635
+ * than the primary key.
636
+ *
637
+ * Resolution order:
638
+ * 1. **Local lookup** -- query Dexie by the given index. If a non-deleted
639
+ * match is found, return it immediately.
640
+ * 2. **Remote check** (optional) -- if `checkRemote` is true and online,
641
+ * query Supabase for an existing record. If found, cache it locally and
642
+ * return it. This handles the case where the record exists on the server
643
+ * but hasn't been synced down to this device yet.
644
+ * 3. **Local create** -- if neither local nor remote has a match, create a
645
+ * new entity with the provided defaults, queue it for sync, and return it.
646
+ *
647
+ * @param table - The Supabase table name.
648
+ * @param index - The indexed field to search on (e.g., `'user_id'`).
649
+ * @param value - The value to match against the index (e.g., the current user's ID).
650
+ * @param defaults - Default field values for the newly created entity (excluding
651
+ * `id`, `created_at`, and `updated_at`, which are auto-generated).
652
+ * @param opts - Optional configuration.
653
+ * @param opts.checkRemote - If `true`, check Supabase before creating locally.
654
+ * Prevents duplicate creation when the record exists
655
+ * on another device but hasn't synced down yet.
656
+ * Defaults to `false`.
657
+ * @returns The existing or newly created entity record.
658
+ *
659
+ * @example
660
+ * ```ts
661
+ * // Get or create user-specific focus settings
662
+ * const settings = await engineGetOrCreate(
663
+ * 'focus_settings',
664
+ * 'user_id',
665
+ * currentUserId,
666
+ * { user_id: currentUserId, pomodoro_minutes: 25, break_minutes: 5 },
667
+ * { checkRemote: true }
668
+ * );
669
+ * ```
670
+ *
671
+ * @see {@link engineCreate} for the underlying create logic
672
+ * @see {@link engineQuery} for index-based queries without auto-creation
327
673
  */
328
674
  export async function engineGetOrCreate(table, index, value, defaults, opts) {
329
675
  const db = getDb();
330
676
  const dexieTable = getDexieTableName(table);
331
- // Check local first
677
+ /* Step 1: Check local first -- fast path, no network needed. */
332
678
  const localResults = await db
333
679
  .table(dexieTable)
334
680
  .where(index)
335
681
  .equals(value)
336
682
  .toArray();
683
+ /* Filter out soft-deleted records so we don't return a tombstone as the
684
+ "existing" entity -- that would prevent a valid re-creation. */
337
685
  const existing = localResults.find((r) => !r.deleted);
338
686
  if (existing)
339
687
  return existing;
340
- // Check remote if requested
688
+ /* Step 2: Check remote if requested -- prevents duplicate creation across devices. */
341
689
  if (opts?.checkRemote && typeof navigator !== 'undefined' && navigator.onLine) {
342
690
  try {
343
691
  const columns = getTableColumns(table);
@@ -348,15 +696,18 @@ export async function engineGetOrCreate(table, index, value, defaults, opts) {
348
696
  .is('deleted', null)
349
697
  .maybeSingle();
350
698
  if (data) {
699
+ /* Cache the remote record locally for offline access. */
351
700
  await db.table(dexieTable).put(data);
352
701
  return data;
353
702
  }
354
703
  }
355
704
  catch {
356
- // Offline or network error - fall through to local create
705
+ /* Offline or network error -- fall through to local create.
706
+ This is intentionally swallowed: creating a local record is
707
+ always safe, and duplicate resolution happens during sync. */
357
708
  }
358
709
  }
359
- // Create new
710
+ /* Step 3: Create new entity -- no existing record found anywhere. */
360
711
  const entityId = generateId();
361
712
  const timestamp = now();
362
713
  const payload = {