@evolu/common 8.12.0 → 8.13.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 (59) hide show
  1. package/dist/src/Callbacks.d.ts +12 -1
  2. package/dist/src/Callbacks.d.ts.map +1 -1
  3. package/dist/src/Callbacks.js +3 -0
  4. package/dist/src/Error.d.ts +10 -4
  5. package/dist/src/Error.d.ts.map +1 -1
  6. package/dist/src/Error.js +10 -4
  7. package/dist/src/Object.d.ts +65 -0
  8. package/dist/src/Object.d.ts.map +1 -1
  9. package/dist/src/Object.js +142 -0
  10. package/dist/src/Resource.d.ts +0 -5
  11. package/dist/src/Resource.d.ts.map +1 -1
  12. package/dist/src/Resource.js +6 -13
  13. package/dist/src/Sqlite.d.ts.map +1 -1
  14. package/dist/src/Sqlite.js +7 -0
  15. package/dist/src/Task.d.ts +10 -8
  16. package/dist/src/Task.d.ts.map +1 -1
  17. package/dist/src/Task.js +41 -5
  18. package/dist/src/Worker.d.ts +3 -3
  19. package/dist/src/local-first/Db.d.ts +8 -3
  20. package/dist/src/local-first/Db.d.ts.map +1 -1
  21. package/dist/src/local-first/Db.js +56 -19
  22. package/dist/src/local-first/Evolu.d.ts +81 -37
  23. package/dist/src/local-first/Evolu.d.ts.map +1 -1
  24. package/dist/src/local-first/Evolu.js +31 -6
  25. package/dist/src/local-first/Owner.d.ts +9 -0
  26. package/dist/src/local-first/Owner.d.ts.map +1 -1
  27. package/dist/src/local-first/Owner.js +9 -0
  28. package/dist/src/local-first/Protocol.d.ts +6 -4
  29. package/dist/src/local-first/Protocol.d.ts.map +1 -1
  30. package/dist/src/local-first/Protocol.js +1 -6
  31. package/dist/src/local-first/Schema.d.ts +7 -1
  32. package/dist/src/local-first/Schema.d.ts.map +1 -1
  33. package/dist/src/local-first/Shared.d.ts +523 -133
  34. package/dist/src/local-first/Shared.d.ts.map +1 -1
  35. package/dist/src/local-first/Shared.js +696 -256
  36. package/dist/src/local-first/Storage.d.ts +16 -12
  37. package/dist/src/local-first/Storage.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/Callbacks.test.ts +20 -0
  40. package/src/Callbacks.ts +17 -1
  41. package/src/Error.ts +10 -4
  42. package/src/Object.test.ts +296 -0
  43. package/src/Object.ts +163 -0
  44. package/src/Resource.test.ts +20 -16
  45. package/src/Resource.ts +6 -20
  46. package/src/Sqlite.ts +7 -0
  47. package/src/Task.test.ts +233 -62
  48. package/src/Task.ts +47 -13
  49. package/src/Worker.ts +3 -3
  50. package/src/local-first/Db.ts +88 -18
  51. package/src/local-first/Evolu.test.ts +281 -1
  52. package/src/local-first/Evolu.ts +124 -46
  53. package/src/local-first/Owner.ts +9 -0
  54. package/src/local-first/Protocol.test.ts +42 -60
  55. package/src/local-first/Protocol.ts +7 -9
  56. package/src/local-first/Schema.ts +8 -2
  57. package/src/local-first/Shared.test.ts +2633 -646
  58. package/src/local-first/Shared.ts +1192 -364
  59. package/src/local-first/Storage.ts +18 -19
@@ -950,7 +950,7 @@ describe("E2E errors", () => {
950
950
  );
951
951
  });
952
952
 
953
- it("rejected relay writes report quota and other causes distinctly", async () => {
953
+ it("reports a rejected relay write as a quota and a thrown one as a write failure", async () => {
954
954
  const deps = testCreateDeps();
955
955
  const initiatorMessage = createProtocolMessageFromCrdtMessages(deps)(
956
956
  testAppOwner,
@@ -961,50 +961,48 @@ describe("E2E errors", () => {
961
961
  },
962
962
  ],
963
963
  );
964
- /** Returns the relay's response and what it logged for the rejection. */
965
- const relayResponseFor = async (error: StorageWriteMessagesError) => {
964
+ /** Returns the relay's response and what it logged for the write. */
965
+ const relayResponseFor = async (
966
+ writeMessages: StorageDep["storage"]["writeMessages"],
967
+ ) => {
966
968
  await using run = testCreateRun({
967
969
  storage: {
968
970
  ...shouldNotBeCalledStorageDep.storage,
969
971
  validateWriteKey: () => true,
970
- writeMessages: () => () => err(error),
972
+ writeMessages,
971
973
  },
972
974
  } satisfies StorageDep);
973
975
  const { message } = await run.orThrow(
974
976
  applyProtocolMessageAsRelay(initiatorMessage),
975
977
  );
976
- return {
977
- message,
978
- logged: run.deps.console
979
- .getEntriesSnapshot()
980
- .map(({ method, args }) => ({ method, args })),
981
- };
978
+ return { message, logged: run.deps.console.getEntriesSnapshot() };
982
979
  };
983
980
 
984
981
  await using run = testCreateRun(shouldNotBeCalledStorageDep);
985
982
  // A quota rejection is expected, so the relay does not log it.
986
- const quota = await relayResponseFor({
987
- type: "StorageQuotaError",
988
- ownerId: testAppOwner.id,
989
- });
983
+ const quota = await relayResponseFor(
984
+ () => () => err({ type: "StorageQuotaError", ownerId: testAppOwner.id }),
985
+ );
990
986
  assertEqual(
991
987
  await run(applyProtocolMessageAsClient(quota.message)),
992
988
  err({ type: "ProtocolQuotaError", ownerId: testAppOwner.id }),
993
989
  );
994
990
  assertEqual(quota.logged, []);
995
- // Any other storage rejection is a relay write failure, not a quota, and
996
- // the relay logs its cause.
997
- const mismatch: StorageWriteMessagesError = {
998
- type: "ProtocolTimestampMismatchError",
999
- expected: timestampBytesToTimestamp(testTimestampsAsc[0]),
1000
- timestamp: timestampBytesToTimestamp(testTimestampsAsc[1]),
1001
- };
1002
- const other = await relayResponseFor(mismatch);
991
+ // A thrown write is a relay write failure, not a quota, and the relay
992
+ // logs it.
993
+ const failure = new Error("write failed");
994
+ const thrown = await relayResponseFor(() => {
995
+ throw failure;
996
+ });
1003
997
  assertEqual(
1004
- await run(applyProtocolMessageAsClient(other.message)),
998
+ await run(applyProtocolMessageAsClient(thrown.message)),
1005
999
  err({ type: "ProtocolWriteError", ownerId: testAppOwner.id }),
1006
1000
  );
1007
- assertEqual(other.logged, [{ method: "error", args: [mismatch] }]);
1001
+ assertEqual(
1002
+ thrown.logged.map(({ method }) => method),
1003
+ ["error"],
1004
+ );
1005
+ assertSame(thrown.logged[0]?.args[0], failure);
1008
1006
  });
1009
1007
  });
1010
1008
 
@@ -1239,7 +1237,7 @@ describe("applyProtocolMessageAsClient results", () => {
1239
1237
  assertOk(result, { type: "Readonly" });
1240
1238
  });
1241
1239
 
1242
- it("preserves expected storage write rejection causes", async () => {
1240
+ it("preserves a storage write rejection", async () => {
1243
1241
  const deps = testCreateDeps();
1244
1242
  const input = createResponse();
1245
1243
  input.addMessage(
@@ -1250,42 +1248,26 @@ describe("applyProtocolMessageAsClient results", () => {
1250
1248
  );
1251
1249
  const message = input.unwrap();
1252
1250
 
1253
- const errors: ReadonlyArray<StorageWriteMessagesError> = [
1254
- {
1255
- type: "DecryptWithXChaCha20Poly1305Error",
1256
- error: new Error("decryption failed"),
1257
- },
1258
- {
1259
- type: "ProtocolInvalidDataError",
1260
- data: Uint8Array.of(255),
1261
- error: new Error("decoding failed"),
1262
- },
1263
- {
1264
- type: "ProtocolTimestampMismatchError",
1265
- expected: timestampBytesToTimestamp(testTimestampsAsc[0]),
1266
- timestamp: timestampBytesToTimestamp(testTimestampsAsc[1]),
1251
+ const error: StorageWriteMessagesError = {
1252
+ type: "StorageQuotaError",
1253
+ ownerId: testAppOwner.id,
1254
+ };
1255
+ await using run = testCreateRun({
1256
+ storage: {
1257
+ ...shouldNotBeCalledStorageDep.storage,
1258
+ writeMessages: () => () => err(error),
1267
1259
  },
1268
- { type: "StorageQuotaError", ownerId: testAppOwner.id },
1269
- ];
1270
-
1271
- for (const error of errors) {
1272
- await using run = testCreateRun({
1273
- storage: {
1274
- ...shouldNotBeCalledStorageDep.storage,
1275
- writeMessages: () => () => err(error),
1276
- },
1277
- } satisfies StorageDep);
1278
- const task = applyProtocolMessageAsClient(message, {
1279
- writeKey: testAppOwner.writeKey,
1280
- });
1281
- assertType<
1282
- InferTaskErr<typeof task>,
1283
- ProtocolError | StorageWriteMessagesError
1284
- >();
1285
- const result = await run(task);
1286
- assertErr(result);
1287
- assertSame(result.error, error);
1288
- }
1260
+ } satisfies StorageDep);
1261
+ const task = applyProtocolMessageAsClient(message, {
1262
+ writeKey: testAppOwner.writeKey,
1263
+ });
1264
+ assertType<
1265
+ InferTaskErr<typeof task>,
1266
+ ProtocolError | StorageWriteMessagesError
1267
+ >();
1268
+ const result = await run(task);
1269
+ assertErr(result);
1270
+ assertSame(result.error, error);
1289
1271
  });
1290
1272
 
1291
1273
  it("reports a thrown write as failed", async () => {
@@ -465,8 +465,9 @@ export interface ProtocolWriteKeyError
465
465
  extends OwnerError, Typed<"ProtocolWriteKeyError"> {}
466
466
 
467
467
  /**
468
- * Error indicating a serious relay-side write failure. Clients should log this
469
- * error and show a generic sync error to the user.
468
+ * Error indicating a serious relay-side write failure. Sync state shows it as
469
+ * the failure of that relay's route; apps show a generic sync error for the
470
+ * owner's `Error` status.
470
471
  */
471
472
  export interface ProtocolWriteError
472
473
  extends OwnerError, Typed<"ProtocolWriteError"> {}
@@ -489,8 +490,9 @@ export interface ProtocolQuotaError
489
490
  extends OwnerError, Typed<"ProtocolQuotaError"> {}
490
491
 
491
492
  /**
492
- * Error indicating a serious relay-side synchronization failure. Clients should
493
- * log this error and show a generic sync error to the user.
493
+ * Error indicating a serious relay-side synchronization failure. Sync state
494
+ * shows it as the failure of that relay's route; apps show a generic sync error
495
+ * for the owner's `Error` status.
494
496
  */
495
497
  export interface ProtocolSyncError
496
498
  extends OwnerError, Typed<"ProtocolSyncError"> {}
@@ -1286,13 +1288,9 @@ export const applyProtocolMessageAsRelay =
1286
1288
  );
1287
1289
 
1288
1290
  if (!result.ok) {
1289
- const isQuotaError = result.error.type === "StorageQuotaError";
1290
- if (!isQuotaError) run.deps.console.error(result.error);
1291
1291
  const message = createProtocolMessageBuffer(ownerId, {
1292
1292
  messageType: MessageType.Response,
1293
- errorCode: isQuotaError
1294
- ? ProtocolErrorCode.QuotaError
1295
- : ProtocolErrorCode.WriteError,
1293
+ errorCode: ProtocolErrorCode.QuotaError,
1296
1294
  }).unwrap();
1297
1295
  return ok({ type: "Response", message });
1298
1296
  }
@@ -43,7 +43,7 @@ import {
43
43
  type withDefault,
44
44
  } from "../Type.ts";
45
45
  import type { CompileTimeError, Simplify } from "../Types.ts";
46
- import type { Evolu, maxMutationSize } from "./Evolu.ts";
46
+ import type { Evolu, EvoluErrorDep, maxMutationSize } from "./Evolu.ts";
47
47
  import type { AppOwner, OwnerIdBytes } from "./Owner.ts";
48
48
  import {
49
49
  OwnerEncryptionKey,
@@ -514,7 +514,13 @@ export interface MutationOptions {
514
514
  * Called after the mutation's changes are stored and subscribed queries
515
515
  * reflect them. Useful for follow-up work (e.g., notifications, navigation)
516
516
  * after insert, update, or upsert. It never runs when the database is
517
- * unavailable.
517
+ * unavailable or the mutation could not be stored, which
518
+ * {@link EvoluErrorDep.evoluError} reports.
519
+ *
520
+ * An Evolu instance stores its mutations in batches, each in one transaction.
521
+ * A batch holds the mutations made before Evolu sends it in a microtask,
522
+ * usually one synchronous block, and {@link Evolu.requestSync} sends it early.
523
+ * When one mutation cannot be stored, none of its batch is.
518
524
  *
519
525
  * Stored does not always mean visible. A change quarantined for
520
526
  * {@link QuarantineReason.TimestampDrift} is stored in