@evolu/common 8.13.0 → 8.15.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 (205) hide show
  1. package/dist/src/Crypto.d.ts +2 -1
  2. package/dist/src/Crypto.d.ts.map +1 -1
  3. package/dist/src/Redacted.d.ts +19 -39
  4. package/dist/src/Redacted.d.ts.map +1 -1
  5. package/dist/src/Redacted.js +18 -33
  6. package/dist/src/Type.d.ts +161 -0
  7. package/dist/src/Type.d.ts.map +1 -1
  8. package/dist/src/Type.js +152 -2
  9. package/dist/src/WebSocket.d.ts +16 -1
  10. package/dist/src/WebSocket.d.ts.map +1 -1
  11. package/dist/src/WebSocket.js +14 -2
  12. package/dist/src/intl/_en.d.ts +5 -1
  13. package/dist/src/intl/_en.d.ts.map +1 -1
  14. package/dist/src/intl/_en.js +4 -0
  15. package/dist/src/intl/ar.d.ts +5 -1
  16. package/dist/src/intl/ar.d.ts.map +1 -1
  17. package/dist/src/intl/ar.js +4 -0
  18. package/dist/src/intl/bn.d.ts +5 -1
  19. package/dist/src/intl/bn.d.ts.map +1 -1
  20. package/dist/src/intl/bn.js +4 -0
  21. package/dist/src/intl/ca.d.ts +5 -1
  22. package/dist/src/intl/ca.d.ts.map +1 -1
  23. package/dist/src/intl/ca.js +4 -0
  24. package/dist/src/intl/cs.d.ts +5 -1
  25. package/dist/src/intl/cs.d.ts.map +1 -1
  26. package/dist/src/intl/cs.js +4 -0
  27. package/dist/src/intl/da.d.ts +5 -1
  28. package/dist/src/intl/da.d.ts.map +1 -1
  29. package/dist/src/intl/da.js +4 -0
  30. package/dist/src/intl/de.d.ts +5 -1
  31. package/dist/src/intl/de.d.ts.map +1 -1
  32. package/dist/src/intl/de.js +4 -0
  33. package/dist/src/intl/el.d.ts +5 -1
  34. package/dist/src/intl/el.d.ts.map +1 -1
  35. package/dist/src/intl/el.js +4 -0
  36. package/dist/src/intl/es.d.ts +5 -1
  37. package/dist/src/intl/es.d.ts.map +1 -1
  38. package/dist/src/intl/es.js +4 -0
  39. package/dist/src/intl/fa.d.ts +5 -1
  40. package/dist/src/intl/fa.d.ts.map +1 -1
  41. package/dist/src/intl/fa.js +4 -0
  42. package/dist/src/intl/fi.d.ts +5 -1
  43. package/dist/src/intl/fi.d.ts.map +1 -1
  44. package/dist/src/intl/fi.js +4 -0
  45. package/dist/src/intl/fil.d.ts +5 -1
  46. package/dist/src/intl/fil.d.ts.map +1 -1
  47. package/dist/src/intl/fil.js +4 -0
  48. package/dist/src/intl/fr.d.ts +5 -1
  49. package/dist/src/intl/fr.d.ts.map +1 -1
  50. package/dist/src/intl/fr.js +4 -0
  51. package/dist/src/intl/he.d.ts +5 -1
  52. package/dist/src/intl/he.d.ts.map +1 -1
  53. package/dist/src/intl/he.js +4 -0
  54. package/dist/src/intl/hi.d.ts +5 -1
  55. package/dist/src/intl/hi.d.ts.map +1 -1
  56. package/dist/src/intl/hi.js +4 -0
  57. package/dist/src/intl/hr.d.ts +5 -1
  58. package/dist/src/intl/hr.d.ts.map +1 -1
  59. package/dist/src/intl/hr.js +4 -0
  60. package/dist/src/intl/hu.d.ts +3 -1
  61. package/dist/src/intl/hu.d.ts.map +1 -1
  62. package/dist/src/intl/hu.js +2 -0
  63. package/dist/src/intl/id.d.ts +5 -1
  64. package/dist/src/intl/id.d.ts.map +1 -1
  65. package/dist/src/intl/id.js +4 -0
  66. package/dist/src/intl/it.d.ts +5 -1
  67. package/dist/src/intl/it.d.ts.map +1 -1
  68. package/dist/src/intl/it.js +4 -0
  69. package/dist/src/intl/ja.d.ts +5 -1
  70. package/dist/src/intl/ja.d.ts.map +1 -1
  71. package/dist/src/intl/ja.js +4 -0
  72. package/dist/src/intl/ko.d.ts +5 -1
  73. package/dist/src/intl/ko.d.ts.map +1 -1
  74. package/dist/src/intl/ko.js +4 -0
  75. package/dist/src/intl/ml.d.ts +5 -1
  76. package/dist/src/intl/ml.d.ts.map +1 -1
  77. package/dist/src/intl/ml.js +4 -0
  78. package/dist/src/intl/mr.d.ts +5 -1
  79. package/dist/src/intl/mr.d.ts.map +1 -1
  80. package/dist/src/intl/mr.js +4 -0
  81. package/dist/src/intl/ms.d.ts +5 -1
  82. package/dist/src/intl/ms.d.ts.map +1 -1
  83. package/dist/src/intl/ms.js +4 -0
  84. package/dist/src/intl/nb.d.ts +3 -1
  85. package/dist/src/intl/nb.d.ts.map +1 -1
  86. package/dist/src/intl/nb.js +2 -0
  87. package/dist/src/intl/nl.d.ts +5 -1
  88. package/dist/src/intl/nl.d.ts.map +1 -1
  89. package/dist/src/intl/nl.js +4 -0
  90. package/dist/src/intl/pa.d.ts +5 -1
  91. package/dist/src/intl/pa.d.ts.map +1 -1
  92. package/dist/src/intl/pa.js +4 -0
  93. package/dist/src/intl/pl.d.ts +4 -0
  94. package/dist/src/intl/pl.d.ts.map +1 -1
  95. package/dist/src/intl/pl.js +4 -0
  96. package/dist/src/intl/pt-BR.d.ts +5 -1
  97. package/dist/src/intl/pt-BR.d.ts.map +1 -1
  98. package/dist/src/intl/pt-BR.js +4 -0
  99. package/dist/src/intl/pt.d.ts +5 -1
  100. package/dist/src/intl/pt.d.ts.map +1 -1
  101. package/dist/src/intl/pt.js +4 -0
  102. package/dist/src/intl/ro.d.ts +5 -1
  103. package/dist/src/intl/ro.d.ts.map +1 -1
  104. package/dist/src/intl/ro.js +4 -0
  105. package/dist/src/intl/sk.d.ts +5 -1
  106. package/dist/src/intl/sk.d.ts.map +1 -1
  107. package/dist/src/intl/sk.js +4 -0
  108. package/dist/src/intl/sl.d.ts +5 -1
  109. package/dist/src/intl/sl.d.ts.map +1 -1
  110. package/dist/src/intl/sl.js +4 -0
  111. package/dist/src/intl/sv.d.ts +5 -1
  112. package/dist/src/intl/sv.d.ts.map +1 -1
  113. package/dist/src/intl/sv.js +4 -0
  114. package/dist/src/intl/sw.d.ts +2 -0
  115. package/dist/src/intl/sw.d.ts.map +1 -1
  116. package/dist/src/intl/sw.js +2 -0
  117. package/dist/src/intl/ta.d.ts +5 -1
  118. package/dist/src/intl/ta.d.ts.map +1 -1
  119. package/dist/src/intl/ta.js +4 -0
  120. package/dist/src/intl/te.d.ts +5 -1
  121. package/dist/src/intl/te.d.ts.map +1 -1
  122. package/dist/src/intl/te.js +4 -0
  123. package/dist/src/intl/th.d.ts +5 -1
  124. package/dist/src/intl/th.d.ts.map +1 -1
  125. package/dist/src/intl/th.js +4 -0
  126. package/dist/src/intl/tr.d.ts +5 -1
  127. package/dist/src/intl/tr.d.ts.map +1 -1
  128. package/dist/src/intl/tr.js +4 -0
  129. package/dist/src/intl/uk.d.ts +5 -1
  130. package/dist/src/intl/uk.d.ts.map +1 -1
  131. package/dist/src/intl/uk.js +4 -0
  132. package/dist/src/intl/ur.d.ts +5 -1
  133. package/dist/src/intl/ur.d.ts.map +1 -1
  134. package/dist/src/intl/ur.js +4 -0
  135. package/dist/src/intl/vi.d.ts +5 -1
  136. package/dist/src/intl/vi.d.ts.map +1 -1
  137. package/dist/src/intl/vi.js +4 -0
  138. package/dist/src/intl/zh-CN.d.ts +5 -1
  139. package/dist/src/intl/zh-CN.d.ts.map +1 -1
  140. package/dist/src/intl/zh-CN.js +4 -0
  141. package/dist/src/intl/zh-TW.d.ts +5 -1
  142. package/dist/src/intl/zh-TW.d.ts.map +1 -1
  143. package/dist/src/intl/zh-TW.js +4 -0
  144. package/dist/src/local-first/Evolu.d.ts +66 -2
  145. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  146. package/dist/src/local-first/Evolu.js +28 -4
  147. package/dist/src/local-first/Shared.d.ts +46 -39
  148. package/dist/src/local-first/Shared.d.ts.map +1 -1
  149. package/dist/src/local-first/Shared.js +30 -19
  150. package/package.json +1 -1
  151. package/src/Crypto.ts +2 -1
  152. package/src/Redacted.test.ts +231 -0
  153. package/src/Redacted.ts +36 -47
  154. package/src/Type.test.ts +166 -0
  155. package/src/Type.ts +198 -2
  156. package/src/WebSocket.ts +33 -4
  157. package/src/intl/_en.ts +10 -0
  158. package/src/intl/ar.ts +8 -0
  159. package/src/intl/bn.ts +10 -0
  160. package/src/intl/ca.ts +10 -0
  161. package/src/intl/cs.ts +10 -0
  162. package/src/intl/da.ts +10 -0
  163. package/src/intl/de.ts +10 -0
  164. package/src/intl/el.ts +10 -0
  165. package/src/intl/es.ts +10 -0
  166. package/src/intl/fa.ts +10 -0
  167. package/src/intl/fi.ts +10 -0
  168. package/src/intl/fil.ts +10 -0
  169. package/src/intl/fr.ts +10 -0
  170. package/src/intl/he.ts +10 -0
  171. package/src/intl/hi.ts +10 -0
  172. package/src/intl/hr.ts +10 -0
  173. package/src/intl/hu.ts +6 -0
  174. package/src/intl/id.ts +10 -0
  175. package/src/intl/intl.test.ts +88 -0
  176. package/src/intl/it.ts +10 -0
  177. package/src/intl/ja.ts +10 -0
  178. package/src/intl/ko.ts +10 -0
  179. package/src/intl/ml.ts +10 -0
  180. package/src/intl/mr.ts +10 -0
  181. package/src/intl/ms.ts +8 -0
  182. package/src/intl/nb.ts +6 -0
  183. package/src/intl/nl.ts +10 -0
  184. package/src/intl/pa.ts +10 -0
  185. package/src/intl/pl.ts +12 -0
  186. package/src/intl/pt-BR.ts +10 -0
  187. package/src/intl/pt.ts +8 -0
  188. package/src/intl/ro.ts +10 -0
  189. package/src/intl/sk.ts +8 -0
  190. package/src/intl/sl.ts +10 -0
  191. package/src/intl/sv.ts +10 -0
  192. package/src/intl/sw.ts +4 -0
  193. package/src/intl/ta.ts +10 -0
  194. package/src/intl/te.ts +10 -0
  195. package/src/intl/th.ts +10 -0
  196. package/src/intl/tr.ts +10 -0
  197. package/src/intl/uk.ts +10 -0
  198. package/src/intl/ur.ts +8 -0
  199. package/src/intl/vi.ts +8 -0
  200. package/src/intl/zh-CN.ts +10 -0
  201. package/src/intl/zh-TW.ts +10 -0
  202. package/src/local-first/Evolu.test.ts +162 -18
  203. package/src/local-first/Evolu.ts +102 -6
  204. package/src/local-first/Shared.test.ts +113 -30
  205. package/src/local-first/Shared.ts +73 -57
@@ -385,6 +385,37 @@ export interface Evolu<
385
385
  /** {@link AppOwner}. */
386
386
  readonly appOwner: AppOwner;
387
387
 
388
+ /**
389
+ * Whether this device keeps the database, resolved once the shared worker
390
+ * serves it, or `Unknown` if this instance is disposed first.
391
+ *
392
+ * Apps tell the user when it is `NotPersisted`, because then data that has
393
+ * not synced is lost when the database closes. Apps must not tell the user
394
+ * that data is saved on the device when it is `Unknown`.
395
+ *
396
+ * ### Example
397
+ *
398
+ * ```ts
399
+ * import { assertEqual } from "@evolu/common";
400
+ * import type { DevicePersistence } from "@evolu/common/local-first";
401
+ *
402
+ * // The notice for `await evolu.devicePersistence`, or null for none.
403
+ * const persistenceNotice = (
404
+ * devicePersistence: DevicePersistence,
405
+ * ): string | null =>
406
+ * devicePersistence === "NotPersisted"
407
+ * ? "Your data isn't kept on this device. Changes that haven't synced are lost when you close this tab."
408
+ * : null;
409
+ *
410
+ * assertEqual(persistenceNotice("Unknown"), null);
411
+ * assertEqual(
412
+ * persistenceNotice("NotPersisted"),
413
+ * "Your data isn't kept on this device. Changes that haven't synced are lost when you close this tab.",
414
+ * );
415
+ * ```
416
+ */
417
+ readonly devicePersistence: Promise<DevicePersistence>;
418
+
388
419
  /**
389
420
  * Inserts a row and returns the generated {@link Id}.
390
421
  *
@@ -873,6 +904,28 @@ export interface Evolu<
873
904
  */
874
905
  export type UnuseOwner = () => void;
875
906
 
907
+ /**
908
+ * Whether this device keeps an {@link Evolu} database, stating only what Evolu
909
+ * knows for sure.
910
+ *
911
+ * - `Persisted`: the platform stores it in the app's own file, as React Native
912
+ * does.
913
+ * - `NotPersisted`: it is kept in memory, because {@link EvoluConfig.memoryOnly}
914
+ * asks for that or the platform offers no persistent storage, as a browser
915
+ * does in Safari's Private Browsing or a Firefox private window. Data that
916
+ * has not synced is lost when the database closes.
917
+ * - `Unknown`: it is stored, but the platform may delete it. A browser can keep
918
+ * storage only for a private session, as Chrome's incognito does without
919
+ * telling the app, delete it after a while or when disk space runs low, and
920
+ * let the user clear it.
921
+ *
922
+ * See [Will my data stay on the
923
+ * device?](https://www.evolu.dev/docs/faq#will-my-data-stay-on-the-device).
924
+ *
925
+ * @group Core
926
+ */
927
+ export type DevicePersistence = "Persisted" | "NotPersisted" | "Unknown";
928
+
876
929
  /**
877
930
  * Represents app-level errors that can occur in {@link Evolu}.
878
931
  *
@@ -1125,6 +1178,20 @@ export interface SyncStateDep {
1125
1178
  readonly syncState: ReadonlyStore<SyncState | null>;
1126
1179
  }
1127
1180
 
1181
+ /**
1182
+ * Asks the platform to keep stored databases until the user deletes them.
1183
+ *
1184
+ * Only unsynced local changes exist nowhere else, so each {@link Evolu} instance
1185
+ * calls it after its first local mutation, when its
1186
+ * {@link Evolu.devicePersistence} is `Unknown`. A platform provides it when its
1187
+ * storage can be deleted without the user, as a browser's can.
1188
+ *
1189
+ * @group Construction
1190
+ */
1191
+ export interface RequestPersistentStorageDep {
1192
+ readonly requestPersistentStorage: () => void;
1193
+ }
1194
+
1128
1195
  /**
1129
1196
  * Shared platform dependencies for creating {@link Evolu} instances.
1130
1197
  *
@@ -1143,7 +1210,7 @@ export type EvoluDeps = EvoluPlatformDeps &
1143
1210
  * Platform-specific dependencies required to create {@link EvoluDeps}.
1144
1211
  *
1145
1212
  * Provides worker and channel adapters plus optional platform integrations for
1146
- * logging and synchronous UI flush.
1213
+ * logging, synchronous UI flush, and persistent storage requests.
1147
1214
  *
1148
1215
  * @group Construction
1149
1216
  */
@@ -1154,7 +1221,8 @@ export type EvoluPlatformDeps = CreateDbWorkerDep &
1154
1221
  ReloadAppDep &
1155
1222
  SharedWorkerDep &
1156
1223
  Partial<ConsoleDep> &
1157
- Partial<FlushSyncDep>;
1224
+ Partial<FlushSyncDep> &
1225
+ Partial<RequestPersistentStorageDep>;
1158
1226
 
1159
1227
  /**
1160
1228
  * Creates shared dependencies used by all {@link createEvolu} instances on a
@@ -1234,9 +1302,7 @@ export const createEvoluDeps = (deps: EvoluPlatformDeps): EvoluDeps => {
1234
1302
  break;
1235
1303
 
1236
1304
  case "Waiting":
1237
- case "StorageUnavailable":
1238
- // Platform adapters act on these; see Builds and Storage in the Shared
1239
- // module.
1305
+ // Platform adapters act on it; see Builds in the Shared module.
1240
1306
  break;
1241
1307
 
1242
1308
  case "Connected": {
@@ -1423,6 +1489,13 @@ export const createEvolu =
1423
1489
  exportDatabasePending = null;
1424
1490
  });
1425
1491
 
1492
+ const devicePersistence = Promise.withResolvers<DevicePersistence>();
1493
+ // A disposed instance never hears from its tenant, so awaiters settle.
1494
+ disposer.defer(() => {
1495
+ devicePersistence.resolve("Unknown");
1496
+ });
1497
+ let isPersistentStorageRequested = false;
1498
+
1426
1499
  let postMessage: (input: EvoluInput) => void;
1427
1500
 
1428
1501
  // Scope worker/channel wiring and keep only postMessage outside.
@@ -1512,6 +1585,10 @@ export const createEvolu =
1512
1585
  break;
1513
1586
  }
1514
1587
 
1588
+ case "OnDevicePersistence":
1589
+ devicePersistence.resolve(message.devicePersistence);
1590
+ break;
1591
+
1515
1592
  default:
1516
1593
  exhaustiveCheck(message);
1517
1594
  }
@@ -1639,6 +1716,18 @@ export const createEvolu =
1639
1716
  );
1640
1717
  }
1641
1718
 
1719
+ // Only unsynced local changes exist nowhere else, so the first one is
1720
+ // when asking the platform to keep them starts to matter. Asking on
1721
+ // load would show Firefox's permission prompt to every visitor.
1722
+ if (!isPersistentStorageRequested) {
1723
+ isPersistentStorageRequested = true;
1724
+ void devicePersistence.promise.then((persistence) => {
1725
+ if (persistence === "Unknown" && !disposed) {
1726
+ run.deps.requestPersistentStorage?.();
1727
+ }
1728
+ });
1729
+ }
1730
+
1642
1731
  mutateBatch.push({
1643
1732
  change: { ...dbChange, ownerId: options?.ownerId ?? appOwner.id },
1644
1733
  onComplete: options?.onComplete,
@@ -1683,8 +1772,14 @@ export const createEvolu =
1683
1772
  "useOwner requires explicit non-empty transports when config.transports is empty.",
1684
1773
  );
1685
1774
 
1775
+ // Post only what workers use. Forwarding the caller's object would copy
1776
+ // an AppOwner's mnemonic into every worker.
1777
+ const { id, encryptionKey } = owner;
1686
1778
  const syncOwner: SyncOwner = {
1687
- owner,
1779
+ owner:
1780
+ "writeKey" in owner
1781
+ ? { id, encryptionKey, writeKey: owner.writeKey }
1782
+ : { id, encryptionKey },
1688
1783
  transports: effectiveTransports,
1689
1784
  };
1690
1785
 
@@ -1722,6 +1817,7 @@ export const createEvolu =
1722
1817
  {
1723
1818
  name,
1724
1819
  appOwner,
1820
+ devicePersistence: devicePersistence.promise,
1725
1821
 
1726
1822
  insert: createMutation("insert"),
1727
1823
  update: createMutation("update"),
@@ -42,6 +42,7 @@ import {
42
42
  type ProtocolTimestampMismatchError,
43
43
  SubscriptionFlags,
44
44
  } from "./Protocol.ts";
45
+ import type { DevicePersistence } from "./Evolu.ts";
45
46
  import {
46
47
  createQueryBuilder,
47
48
  type EvoluSchema,
@@ -170,11 +171,11 @@ const setupSharedWorker = async ({
170
171
  createWebSocket = testCreateWebSocket({ throwOnCreate: true }),
171
172
  time,
172
173
  seed,
173
- isPersistentStorageAvailable,
174
+ getDevicePersistence = () => Promise.resolve("Unknown"),
174
175
  }: {
175
176
  createWebSocket?: CreateWebSocket;
176
177
  time?: TestTime;
177
- isPersistentStorageAvailable?: () => Promise<boolean>;
178
+ getDevicePersistence?: () => Promise<DevicePersistence>;
178
179
  /**
179
180
  * Seeds the worker's random bytes, which are otherwise the same for every
180
181
  * worker.
@@ -214,7 +215,7 @@ const setupSharedWorker = async ({
214
215
  createWebSocket,
215
216
  ...(time && { time }),
216
217
  ...(seed && { randomBytes: testCreateDeps({ seed }).randomBytes }),
217
- ...(isPersistentStorageAvailable && { isPersistentStorageAvailable }),
218
+ getDevicePersistence,
218
219
  }),
219
220
  );
220
221
 
@@ -239,8 +240,10 @@ const setupSharedWorker = async ({
239
240
  releaseDbWorkerLeaderOnDispose = true,
240
241
  autoDispose = true,
241
242
  initialClock = createTimestamp(),
243
+ memoryOnly = false,
242
244
  }: {
243
245
  tenantName?: Name;
246
+ memoryOnly?: boolean;
244
247
  evoluChannel?: TestMessageChannel<EvoluOutput, EvoluInput>;
245
248
  releaseDbWorkerLeaderOnDispose?: boolean;
246
249
  autoDispose?: boolean;
@@ -289,7 +292,7 @@ const setupSharedWorker = async ({
289
292
  consoleLevel: "debug",
290
293
  sqliteSchema: testSqliteSchema,
291
294
  encryptionKey: testAppOwner.encryptionKey,
292
- memoryOnly: false,
295
+ memoryOnly,
293
296
  evoluPort: evoluChannel.port1.native,
294
297
  };
295
298
 
@@ -339,7 +342,25 @@ const setupSharedWorker = async ({
339
342
  ) => {
340
343
  const instance = await createEvoluBeforeDbWorkerLeader(options);
341
344
  await instance.acquireDbWorkerLeader();
342
- return instance;
345
+
346
+ // Once the DbWorker starts, the tenant adds the instance and tells it its
347
+ // device persistence first, so tests see only the outputs that follow.
348
+ const firstOutputs: Array<EvoluOutput> = [];
349
+ instance.evoluChannel.port2.onMessage = (output) => {
350
+ firstOutputs.push(output);
351
+ };
352
+ await testWaitForWorkerMessage();
353
+ instance.evoluChannel.port2.onMessage = null;
354
+ assertLength(firstOutputs, 1);
355
+ const [devicePersistenceOutput] = firstOutputs;
356
+ assert(
357
+ devicePersistenceOutput.type === "OnDevicePersistence",
358
+ "Expected the device persistence first.",
359
+ );
360
+
361
+ return Object.assign(instance, {
362
+ devicePersistence: devicePersistenceOutput.devicePersistence,
363
+ });
343
364
  };
344
365
 
345
366
  return {
@@ -607,17 +628,30 @@ describe("storage", () => {
607
628
  const isMemoryOnly = (output: SharedWorkerOutput | undefined): boolean =>
608
629
  getDbWorkerInit(output).memoryOnly;
609
630
 
631
+ /** Every device persistence the tenant sent to the instance so far. */
632
+ const receiveDevicePersistences = async (
633
+ evoluChannel: TestMessageChannel<EvoluOutput, EvoluInput>,
634
+ ): Promise<ReadonlyArray<DevicePersistence>> => {
635
+ const devicePersistences: Array<DevicePersistence> = [];
636
+ evoluChannel.port2.onMessage = (output) => {
637
+ if (output.type === "OnDevicePersistence") {
638
+ devicePersistences.push(output.devicePersistence);
639
+ }
640
+ };
641
+ await testWaitForWorkerMessage();
642
+ return devicePersistences;
643
+ };
644
+
610
645
  it("keeps every database in memory, replacements included, without persistent storage", async () => {
611
- const isPersistentStorageAvailable = mock.fn(() => Promise.resolve(false));
612
- await using setup = await setupSharedWorker({
613
- isPersistentStorageAvailable,
614
- });
646
+ const getDevicePersistence = mock.fn(() =>
647
+ Promise.resolve<DevicePersistence>("NotPersisted"),
648
+ );
649
+ await using setup = await setupSharedWorker({ getDevicePersistence });
615
650
  using disposer = new DisposableStack();
616
651
 
617
- // The first tab heard it right after Connected.
618
- assertEqual(setup.sharedWorkerOutputs, [{ type: "StorageUnavailable" }]);
619
652
  const first = await setup.createEvolu();
620
- assertTrue(isMemoryOnly(setup.sharedWorkerOutputs.at(1)));
653
+ assertTrue(isMemoryOnly(setup.sharedWorkerOutputs.at(0)));
654
+ assertSame(first.devicePersistence, "NotPersisted");
621
655
 
622
656
  // A replacement DbWorker, as after its tab closed, keeps it in memory too.
623
657
  await first.releaseDbWorkerLeader();
@@ -625,8 +659,7 @@ describe("storage", () => {
625
659
  tab.port.postMessage({ type: "AnnounceTabLeader", consoleLevel: "debug" });
626
660
  await testWaitForWorkerMessage();
627
661
  await testWaitForWorkerMessage();
628
- assertSame(tab.outputs.at(0)?.type, "StorageUnavailable");
629
- const replacement = getDbWorkerInit(tab.outputs.at(1));
662
+ const replacement = getDbWorkerInit(tab.outputs.at(0));
630
663
  assertTrue(replacement.memoryOnly);
631
664
  using leaderPort = testCreateMessagePort<DbWorkerOutput, DbWorkerInput>(
632
665
  replacement.port,
@@ -638,29 +671,62 @@ describe("storage", () => {
638
671
  });
639
672
  await testWaitForWorkerMessage();
640
673
 
641
- assertSame(isPersistentStorageAvailable.mock.callCount(), 1);
674
+ assertSame(getDevicePersistence.mock.callCount(), 1);
642
675
  });
643
676
 
644
- it("keeps databases where the app configured them with persistent storage", async () => {
677
+ for (const devicePersistence of ["Persisted", "Unknown"] as const) {
678
+ it(`stores databases and tells instances ${devicePersistence} where the platform promises it`, async () => {
679
+ await using setup = await setupSharedWorker({
680
+ getDevicePersistence: () => Promise.resolve(devicePersistence),
681
+ });
682
+
683
+ const instance = await setup.createEvolu();
684
+
685
+ assertEqual(
686
+ setup.sharedWorkerOutputs.map(({ type }) => type),
687
+ ["DbWorkerInit"],
688
+ );
689
+ assertFalse(isMemoryOnly(setup.sharedWorkerOutputs.at(0)));
690
+ assertSame(instance.devicePersistence, devicePersistence);
691
+ });
692
+ }
693
+
694
+ it("tells an instance configured memoryOnly that its database is not persisted", async () => {
645
695
  await using setup = await setupSharedWorker({
646
- isPersistentStorageAvailable: () => Promise.resolve(true),
696
+ getDevicePersistence: () => Promise.resolve("Persisted"),
647
697
  });
648
698
 
649
- await setup.createEvolu();
699
+ const instance = await setup.createEvolu({ memoryOnly: true });
650
700
 
651
- assertEqual(
652
- setup.sharedWorkerOutputs.map(({ type }) => type),
653
- ["DbWorkerInit"],
654
- );
655
- assertFalse(isMemoryOnly(setup.sharedWorkerOutputs.at(0)));
701
+ assertTrue(isMemoryOnly(setup.sharedWorkerOutputs.at(0)));
702
+ assertSame(instance.devicePersistence, "NotPersisted");
656
703
  });
657
704
 
658
- it("keeps databases where the app configured them on platforms without the check", async () => {
659
- await using setup = await setupSharedWorker();
705
+ it("tells a later instance of the same name the mode the first one chose", async () => {
706
+ await using setup = await setupSharedWorker({
707
+ getDevicePersistence: () => Promise.resolve("Persisted"),
708
+ });
709
+ const { run, worker } = setup;
710
+ await setup.createEvolu({ memoryOnly: true });
660
711
 
661
- await setup.createEvolu();
712
+ const secondId: EvoluInstanceId = createId(run.deps);
713
+ await using _secondInstanceLock = await run.ok(acquireLeaderLock(secondId));
714
+ using secondChannel = testCreateMessageChannel<EvoluOutput, EvoluInput>();
715
+ worker.port.postMessage({
716
+ type: "CreateEvolu",
717
+ name: testName,
718
+ id: secondId,
719
+ consoleLevel: "debug",
720
+ sqliteSchema: testSqliteSchema,
721
+ encryptionKey: testAppOwner.encryptionKey,
722
+ memoryOnly: false,
723
+ evoluPort: secondChannel.port1.native,
724
+ });
725
+ await testWaitForWorkerMessage();
662
726
 
663
- assertFalse(isMemoryOnly(setup.sharedWorkerOutputs.at(0)));
727
+ assertEqual(await receiveDevicePersistences(secondChannel), [
728
+ "NotPersisted",
729
+ ]);
664
730
  });
665
731
  });
666
732
 
@@ -681,6 +747,7 @@ describe("builds", () => {
681
747
  lockManager,
682
748
  createMessagePort: testCreateMessagePort,
683
749
  createWebSocket: testCreateWebSocket({ throwOnCreate: true }),
750
+ getDevicePersistence: () => Promise.resolve("Unknown" as const),
684
751
  }),
685
752
  );
686
753
  const outputs: Array<SharedWorkerOutput> = [];
@@ -7265,7 +7332,14 @@ describe("with one evolu instance", () => {
7265
7332
  memoryOnly: false,
7266
7333
  evoluPort: siblingChannel.port1.native,
7267
7334
  });
7335
+ const siblingOutputs: Array<EvoluOutput> = [];
7336
+ siblingChannel.port2.onMessage = (output) => {
7337
+ siblingOutputs.push(output);
7338
+ };
7268
7339
  await testWaitForWorkerMessage();
7340
+ assertEqual(siblingOutputs, [
7341
+ { type: "OnDevicePersistence", devicePersistence: "Unknown" },
7342
+ ]);
7269
7343
  siblingChannel.port2.postMessage({
7270
7344
  type: "Mutate",
7271
7345
  changes: [
@@ -9644,6 +9718,10 @@ describe("with one evolu instance", () => {
9644
9718
  [siblingChannel.port1.native],
9645
9719
  );
9646
9720
  await testWaitForWorkerMessage();
9721
+ // The tenant keeps the first instance's persistent database.
9722
+ assertEqual(siblingOutputs.splice(0), [
9723
+ { type: "OnDevicePersistence", devicePersistence: "Unknown" },
9724
+ ]);
9647
9725
 
9648
9726
  const bytes = new Uint8Array([1, 2, 3]);
9649
9727
  const mutation: ExtractTyped<EvoluInput, "Mutate"> = {
@@ -10811,10 +10889,13 @@ describe("startup refusal", () => {
10811
10889
  });
10812
10890
  await testWaitForWorkerMessage();
10813
10891
  // The tab is told through its own connection, not the broadcast channel,
10814
- // and its instance is not answered: pending work stays pending.
10892
+ // and its instance learns only its device persistence: pending work stays
10893
+ // pending.
10815
10894
  assertEqual(firstTabOutputs(), [{ type: "Error", error: refusal }]);
10816
10895
  assertEqual(broadcasts, []);
10817
- assertEqual(evoluOutputs, []);
10896
+ assertEqual(evoluOutputs.splice(0), [
10897
+ { type: "OnDevicePersistence", devicePersistence: "Unknown" },
10898
+ ]);
10818
10899
 
10819
10900
  // Later requests are dropped without a response.
10820
10901
  instance.evoluChannel.port2.postMessage({
@@ -10852,7 +10933,9 @@ describe("startup refusal", () => {
10852
10933
  );
10853
10934
  await testWaitForWorkerMessage();
10854
10935
  assertEqual(firstTabOutputs(), [{ type: "Error", error: refusal }]);
10855
- assertEqual(secondOutputs, []);
10936
+ assertEqual(secondOutputs, [
10937
+ { type: "OnDevicePersistence", devicePersistence: "Unknown" },
10938
+ ]);
10856
10939
 
10857
10940
  // A tab that connects later is told once, through its own connection.
10858
10941
  const laterTab = setupTab(setup, disposer);