@evolu/common 8.11.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.
- package/dist/src/Bytes.d.ts +39 -2
- package/dist/src/Bytes.d.ts.map +1 -1
- package/dist/src/Bytes.js +50 -2
- package/dist/src/Callbacks.d.ts +12 -1
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +3 -0
- package/dist/src/Error.d.ts +10 -4
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +10 -4
- package/dist/src/Object.d.ts +65 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +142 -0
- package/dist/src/Resource.d.ts +0 -5
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +6 -13
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +7 -0
- package/dist/src/Task.d.ts +10 -8
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +41 -5
- package/dist/src/Worker.d.ts +3 -3
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/local-first/Db.d.ts +8 -3
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +56 -19
- package/dist/src/local-first/Evolu.d.ts +142 -24
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +109 -8
- package/dist/src/local-first/Owner.d.ts +9 -0
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +9 -0
- package/dist/src/local-first/Protocol.d.ts +18 -7
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +45 -28
- package/dist/src/local-first/Relay.d.ts.map +1 -1
- package/dist/src/local-first/Relay.js +4 -2
- package/dist/src/local-first/Schema.d.ts +18 -3
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Shared.d.ts +523 -133
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +696 -256
- package/dist/src/local-first/Storage.d.ts +19 -15
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +4 -2
- package/package.json +1 -1
- package/src/Bytes.test.ts +27 -0
- package/src/Bytes.ts +58 -2
- package/src/Callbacks.test.ts +20 -0
- package/src/Callbacks.ts +17 -1
- package/src/Error.ts +10 -4
- package/src/Object.test.ts +296 -0
- package/src/Object.ts +163 -0
- package/src/Resource.test.ts +20 -16
- package/src/Resource.ts +6 -20
- package/src/Sqlite.ts +7 -0
- package/src/Task.test.ts +233 -62
- package/src/Task.ts +47 -13
- package/src/Worker.ts +3 -3
- package/src/index.ts +6 -1
- package/src/local-first/Db.ts +88 -18
- package/src/local-first/Evolu.test.ts +589 -2
- package/src/local-first/Evolu.ts +285 -36
- package/src/local-first/Owner.ts +9 -0
- package/src/local-first/Protocol.test.ts +59 -60
- package/src/local-first/Protocol.ts +67 -47
- package/src/local-first/Relay.ts +4 -2
- package/src/local-first/Schema.ts +20 -3
- package/src/local-first/Shared.test.ts +2633 -646
- package/src/local-first/Shared.ts +1192 -364
- package/src/local-first/Storage.ts +24 -23
|
@@ -207,6 +207,23 @@ test("encodeSqliteValue/decodeSqliteValue preserves an own __proto__ JSON object
|
|
|
207
207
|
assertEqual(decodeSqliteValue(buffer), value);
|
|
208
208
|
});
|
|
209
209
|
|
|
210
|
+
test("encodeSqliteValue encodes JSON nested deeper than decoding allows as a string", () => {
|
|
211
|
+
for (const [depth, type] of [
|
|
212
|
+
[1_000, ProtocolValueType.Json],
|
|
213
|
+
[1_001, ProtocolValueType.String],
|
|
214
|
+
// Deep enough to overflow the stack of a recursive JSON.stringify.
|
|
215
|
+
[20_000, ProtocolValueType.String],
|
|
216
|
+
] as const) {
|
|
217
|
+
const value = "[".repeat(depth) + "]".repeat(depth);
|
|
218
|
+
const buffer = createBuffer();
|
|
219
|
+
|
|
220
|
+
encodeSqliteValue(buffer, value);
|
|
221
|
+
|
|
222
|
+
assertEqual(buffer.unwrap()[0], type);
|
|
223
|
+
assertEqual(decodeSqliteValue(buffer), value);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
210
227
|
test("encodeSqliteValue/decodeSqliteValue property tests", () => {
|
|
211
228
|
const deps = testCreateDeps();
|
|
212
229
|
// Property test: round-trip encoding/decoding should preserve the value
|
|
@@ -933,7 +950,7 @@ describe("E2E errors", () => {
|
|
|
933
950
|
);
|
|
934
951
|
});
|
|
935
952
|
|
|
936
|
-
it("rejected relay
|
|
953
|
+
it("reports a rejected relay write as a quota and a thrown one as a write failure", async () => {
|
|
937
954
|
const deps = testCreateDeps();
|
|
938
955
|
const initiatorMessage = createProtocolMessageFromCrdtMessages(deps)(
|
|
939
956
|
testAppOwner,
|
|
@@ -944,50 +961,48 @@ describe("E2E errors", () => {
|
|
|
944
961
|
},
|
|
945
962
|
],
|
|
946
963
|
);
|
|
947
|
-
/** Returns the relay's response and what it logged for the
|
|
948
|
-
const relayResponseFor = async (
|
|
964
|
+
/** Returns the relay's response and what it logged for the write. */
|
|
965
|
+
const relayResponseFor = async (
|
|
966
|
+
writeMessages: StorageDep["storage"]["writeMessages"],
|
|
967
|
+
) => {
|
|
949
968
|
await using run = testCreateRun({
|
|
950
969
|
storage: {
|
|
951
970
|
...shouldNotBeCalledStorageDep.storage,
|
|
952
971
|
validateWriteKey: () => true,
|
|
953
|
-
writeMessages
|
|
972
|
+
writeMessages,
|
|
954
973
|
},
|
|
955
974
|
} satisfies StorageDep);
|
|
956
975
|
const { message } = await run.orThrow(
|
|
957
976
|
applyProtocolMessageAsRelay(initiatorMessage),
|
|
958
977
|
);
|
|
959
|
-
return {
|
|
960
|
-
message,
|
|
961
|
-
logged: run.deps.console
|
|
962
|
-
.getEntriesSnapshot()
|
|
963
|
-
.map(({ method, args }) => ({ method, args })),
|
|
964
|
-
};
|
|
978
|
+
return { message, logged: run.deps.console.getEntriesSnapshot() };
|
|
965
979
|
};
|
|
966
980
|
|
|
967
981
|
await using run = testCreateRun(shouldNotBeCalledStorageDep);
|
|
968
982
|
// A quota rejection is expected, so the relay does not log it.
|
|
969
|
-
const quota = await relayResponseFor(
|
|
970
|
-
type: "StorageQuotaError",
|
|
971
|
-
|
|
972
|
-
});
|
|
983
|
+
const quota = await relayResponseFor(
|
|
984
|
+
() => () => err({ type: "StorageQuotaError", ownerId: testAppOwner.id }),
|
|
985
|
+
);
|
|
973
986
|
assertEqual(
|
|
974
987
|
await run(applyProtocolMessageAsClient(quota.message)),
|
|
975
988
|
err({ type: "ProtocolQuotaError", ownerId: testAppOwner.id }),
|
|
976
989
|
);
|
|
977
990
|
assertEqual(quota.logged, []);
|
|
978
|
-
//
|
|
979
|
-
//
|
|
980
|
-
const
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
};
|
|
985
|
-
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
|
+
});
|
|
986
997
|
assertEqual(
|
|
987
|
-
await run(applyProtocolMessageAsClient(
|
|
998
|
+
await run(applyProtocolMessageAsClient(thrown.message)),
|
|
988
999
|
err({ type: "ProtocolWriteError", ownerId: testAppOwner.id }),
|
|
989
1000
|
);
|
|
990
|
-
assertEqual(
|
|
1001
|
+
assertEqual(
|
|
1002
|
+
thrown.logged.map(({ method }) => method),
|
|
1003
|
+
["error"],
|
|
1004
|
+
);
|
|
1005
|
+
assertSame(thrown.logged[0]?.args[0], failure);
|
|
991
1006
|
});
|
|
992
1007
|
});
|
|
993
1008
|
|
|
@@ -1222,7 +1237,7 @@ describe("applyProtocolMessageAsClient results", () => {
|
|
|
1222
1237
|
assertOk(result, { type: "Readonly" });
|
|
1223
1238
|
});
|
|
1224
1239
|
|
|
1225
|
-
it("preserves
|
|
1240
|
+
it("preserves a storage write rejection", async () => {
|
|
1226
1241
|
const deps = testCreateDeps();
|
|
1227
1242
|
const input = createResponse();
|
|
1228
1243
|
input.addMessage(
|
|
@@ -1233,42 +1248,26 @@ describe("applyProtocolMessageAsClient results", () => {
|
|
|
1233
1248
|
);
|
|
1234
1249
|
const message = input.unwrap();
|
|
1235
1250
|
|
|
1236
|
-
const
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
{
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
error: new Error("decoding failed"),
|
|
1245
|
-
},
|
|
1246
|
-
{
|
|
1247
|
-
type: "ProtocolTimestampMismatchError",
|
|
1248
|
-
expected: timestampBytesToTimestamp(testTimestampsAsc[0]),
|
|
1249
|
-
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),
|
|
1250
1259
|
},
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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);
|
|
1271
|
-
}
|
|
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);
|
|
1272
1271
|
});
|
|
1273
1272
|
|
|
1274
1273
|
it("reports a thrown write as failed", async () => {
|
|
@@ -103,9 +103,9 @@
|
|
|
103
103
|
* fit within the limit, the protocol automatically continues synchronization in
|
|
104
104
|
* subsequent rounds using range-based reconciliation.
|
|
105
105
|
*
|
|
106
|
-
*
|
|
107
|
-
* message
|
|
108
|
-
*
|
|
106
|
+
* Each mutation is limited to {@link maxMutationSize}, so every change fits one
|
|
107
|
+
* message of {@link defaultProtocolMessageMaxSize} next to the largest ranges
|
|
108
|
+
* section.
|
|
109
109
|
*
|
|
110
110
|
* ## Why Binary?
|
|
111
111
|
*
|
|
@@ -190,6 +190,7 @@ import {
|
|
|
190
190
|
import type { Brand } from "../Brand.ts";
|
|
191
191
|
import {
|
|
192
192
|
type Buffer,
|
|
193
|
+
BufferError,
|
|
193
194
|
createBuffer,
|
|
194
195
|
createRunLengthEncoder,
|
|
195
196
|
decodeFlags,
|
|
@@ -248,7 +249,7 @@ import {
|
|
|
248
249
|
zeroNonNegativeInt,
|
|
249
250
|
} from "../Type.ts";
|
|
250
251
|
import type { Predicate } from "../Types.ts";
|
|
251
|
-
import type { Evolu } from "./Evolu.ts";
|
|
252
|
+
import type { Evolu, maxMutationSize } from "./Evolu.ts";
|
|
252
253
|
import {
|
|
253
254
|
type Owner,
|
|
254
255
|
type OwnerError,
|
|
@@ -464,8 +465,9 @@ export interface ProtocolWriteKeyError
|
|
|
464
465
|
extends OwnerError, Typed<"ProtocolWriteKeyError"> {}
|
|
465
466
|
|
|
466
467
|
/**
|
|
467
|
-
* Error indicating a serious relay-side write failure.
|
|
468
|
-
*
|
|
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.
|
|
469
471
|
*/
|
|
470
472
|
export interface ProtocolWriteError
|
|
471
473
|
extends OwnerError, Typed<"ProtocolWriteError"> {}
|
|
@@ -488,8 +490,9 @@ export interface ProtocolQuotaError
|
|
|
488
490
|
extends OwnerError, Typed<"ProtocolQuotaError"> {}
|
|
489
491
|
|
|
490
492
|
/**
|
|
491
|
-
* Error indicating a serious relay-side synchronization failure.
|
|
492
|
-
*
|
|
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.
|
|
493
496
|
*/
|
|
494
497
|
export interface ProtocolSyncError
|
|
495
498
|
extends OwnerError, Typed<"ProtocolSyncError"> {}
|
|
@@ -1285,13 +1288,9 @@ export const applyProtocolMessageAsRelay =
|
|
|
1285
1288
|
);
|
|
1286
1289
|
|
|
1287
1290
|
if (!result.ok) {
|
|
1288
|
-
const isQuotaError = result.error.type === "StorageQuotaError";
|
|
1289
|
-
if (!isQuotaError) run.deps.console.error(result.error);
|
|
1290
1291
|
const message = createProtocolMessageBuffer(ownerId, {
|
|
1291
1292
|
messageType: MessageType.Response,
|
|
1292
|
-
errorCode:
|
|
1293
|
-
? ProtocolErrorCode.QuotaError
|
|
1294
|
-
: ProtocolErrorCode.WriteError,
|
|
1293
|
+
errorCode: ProtocolErrorCode.QuotaError,
|
|
1295
1294
|
}).unwrap();
|
|
1296
1295
|
return ok({ type: "Response", message });
|
|
1297
1296
|
}
|
|
@@ -1855,30 +1854,7 @@ export const encodeAndEncryptDbChange =
|
|
|
1855
1854
|
(message: CrdtMessage, key: EncryptionKey): EncryptedDbChange => {
|
|
1856
1855
|
const buffer = createBuffer();
|
|
1857
1856
|
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
// Encode the timestamp to prevent tampering (e.g., a malicious relay
|
|
1861
|
-
// assigning this EncryptedDbChange to a different EncryptedCrdtMessage)
|
|
1862
|
-
buffer.extend(timestampToTimestampBytes(message.timestamp));
|
|
1863
|
-
|
|
1864
|
-
encodeFlags(buffer, [
|
|
1865
|
-
message.change.isInsert,
|
|
1866
|
-
// Encode nullable boolean as two flags: presence + value.
|
|
1867
|
-
message.change.isDelete != null,
|
|
1868
|
-
message.change.isDelete ?? false,
|
|
1869
|
-
]);
|
|
1870
|
-
|
|
1871
|
-
encodeString(buffer, message.change.table);
|
|
1872
|
-
buffer.extend(idToIdBytes(message.change.id));
|
|
1873
|
-
|
|
1874
|
-
const entries = objectToEntries(message.change.values);
|
|
1875
|
-
|
|
1876
|
-
encodeLength(buffer, entries);
|
|
1877
|
-
for (const [column, value] of entries) {
|
|
1878
|
-
assertNotUndefined(value);
|
|
1879
|
-
encodeString(buffer, column);
|
|
1880
|
-
encodeSqliteValue(buffer, value);
|
|
1881
|
-
}
|
|
1857
|
+
encodeDbChange(buffer, message);
|
|
1882
1858
|
|
|
1883
1859
|
// Add PADMÉ padding (ignored during decoding)
|
|
1884
1860
|
buffer.extend(createPadmePadding(buffer.getLength()));
|
|
@@ -1896,6 +1872,41 @@ export const encodeAndEncryptDbChange =
|
|
|
1896
1872
|
return buffer.unwrap() as EncryptedDbChange;
|
|
1897
1873
|
};
|
|
1898
1874
|
|
|
1875
|
+
/**
|
|
1876
|
+
* Encodes a {@link CrdtMessage} as {@link encodeAndEncryptDbChange} does before
|
|
1877
|
+
* padding and encryption.
|
|
1878
|
+
*
|
|
1879
|
+
* {@link Evolu.getMutationSize} measures mutations with this encoding, so
|
|
1880
|
+
* {@link maxMutationSize} limits exactly what is encoded, and every change
|
|
1881
|
+
* within it fits one protocol message.
|
|
1882
|
+
*/
|
|
1883
|
+
export const encodeDbChange = (buffer: Buffer, message: CrdtMessage): void => {
|
|
1884
|
+
encodeNonNegativeInt(buffer, protocolVersion);
|
|
1885
|
+
|
|
1886
|
+
// Encode the timestamp to prevent tampering (e.g., a malicious relay
|
|
1887
|
+
// assigning this EncryptedDbChange to a different EncryptedCrdtMessage)
|
|
1888
|
+
buffer.extend(timestampToTimestampBytes(message.timestamp));
|
|
1889
|
+
|
|
1890
|
+
encodeFlags(buffer, [
|
|
1891
|
+
message.change.isInsert,
|
|
1892
|
+
// Encode nullable boolean as two flags: presence + value.
|
|
1893
|
+
message.change.isDelete != null,
|
|
1894
|
+
message.change.isDelete ?? false,
|
|
1895
|
+
]);
|
|
1896
|
+
|
|
1897
|
+
encodeString(buffer, message.change.table);
|
|
1898
|
+
buffer.extend(idToIdBytes(message.change.id));
|
|
1899
|
+
|
|
1900
|
+
const entries = objectToEntries(message.change.values);
|
|
1901
|
+
|
|
1902
|
+
encodeLength(buffer, entries);
|
|
1903
|
+
for (const [column, value] of entries) {
|
|
1904
|
+
assertNotUndefined(value);
|
|
1905
|
+
encodeString(buffer, column);
|
|
1906
|
+
encodeSqliteValue(buffer, value);
|
|
1907
|
+
}
|
|
1908
|
+
};
|
|
1909
|
+
|
|
1899
1910
|
/**
|
|
1900
1911
|
* Decrypts and decodes an {@link EncryptedCrdtMessage} using the provided
|
|
1901
1912
|
* owner's encryption key. Verifies that the embedded timestamp matches the
|
|
@@ -2069,21 +2080,30 @@ export const encodeSqliteValue = (buffer: Buffer, value: SqliteValue): void => {
|
|
|
2069
2080
|
}
|
|
2070
2081
|
|
|
2071
2082
|
const json = Json.from.parent(value);
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
// which would cause data corruption if we don't verify round-trip safety.
|
|
2075
|
-
if (json.ok && JSON.stringify(jsonToJsonValue(json.value)) === value) {
|
|
2083
|
+
if (json.ok) {
|
|
2084
|
+
const jsonValue = jsonToJsonValue(json.value);
|
|
2076
2085
|
jsonBuffer.reset();
|
|
2077
2086
|
try {
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2087
|
+
// Encoding first rejects nesting deeper than decoding allows, before
|
|
2088
|
+
// the recursive JSON.stringify below could overflow the stack. Such
|
|
2089
|
+
// a value is encoded as a plain string.
|
|
2090
|
+
encodeJsonValue(jsonBuffer, jsonValue);
|
|
2091
|
+
// Only encode as Json if it survives JSON.parse/JSON.stringify
|
|
2092
|
+
// round-trip. Some valid JSON strings like "-0E0" get normalized to
|
|
2093
|
+
// "0" during parsing, which would cause data corruption if we don't
|
|
2094
|
+
// verify round-trip safety.
|
|
2095
|
+
if (JSON.stringify(jsonValue) === value) {
|
|
2096
|
+
const jsonBytes = jsonBuffer.unwrap();
|
|
2097
|
+
encodeNonNegativeInt(buffer, ProtocolValueType.Json);
|
|
2098
|
+
encodeLength(buffer, jsonBytes);
|
|
2099
|
+
buffer.extend(jsonBytes);
|
|
2100
|
+
return;
|
|
2101
|
+
}
|
|
2102
|
+
} catch (error) {
|
|
2103
|
+
if (!(error instanceof BufferError)) throw error;
|
|
2083
2104
|
} finally {
|
|
2084
2105
|
jsonBuffer.reset();
|
|
2085
2106
|
}
|
|
2086
|
-
return;
|
|
2087
2107
|
}
|
|
2088
2108
|
|
|
2089
2109
|
const base64Url = Base64Url.from.parent(value);
|
package/src/local-first/Relay.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { err, ok } from "../Result.ts";
|
|
|
17
17
|
import type { SqliteDep } from "../Sqlite.ts";
|
|
18
18
|
import { sql } from "../Sqlite.ts";
|
|
19
19
|
import { createMutexByKey } from "../Task.ts";
|
|
20
|
-
import { Name,
|
|
20
|
+
import { Name, NonNegativeInt, uint8ArrayToBase64Url } from "../Type.ts";
|
|
21
21
|
import { isPromiseLike, type Awaitable } from "../Types.ts";
|
|
22
22
|
import {
|
|
23
23
|
OwnerId,
|
|
@@ -246,7 +246,9 @@ export const createRelaySqliteStorage =
|
|
|
246
246
|
(sum, m) => sum + m.change.length,
|
|
247
247
|
0,
|
|
248
248
|
);
|
|
249
|
-
|
|
249
|
+
// A sum of lengths can be zero. Throwing here would panic the
|
|
250
|
+
// relay's shared Run.
|
|
251
|
+
const newStoredBytes = NonNegativeInt.orThrow(
|
|
250
252
|
(usage.storedBytes ?? 0) + incomingBytes,
|
|
251
253
|
);
|
|
252
254
|
|
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
IdBytes,
|
|
35
35
|
type InferType,
|
|
36
36
|
NonEmptyTrimmedString100,
|
|
37
|
+
type NonEmptyTrimmedString1000,
|
|
37
38
|
Null,
|
|
38
39
|
nullOr,
|
|
39
40
|
object,
|
|
@@ -42,6 +43,7 @@ import {
|
|
|
42
43
|
type withDefault,
|
|
43
44
|
} from "../Type.ts";
|
|
44
45
|
import type { CompileTimeError, Simplify } from "../Types.ts";
|
|
46
|
+
import type { Evolu, EvoluErrorDep, maxMutationSize } from "./Evolu.ts";
|
|
45
47
|
import type { AppOwner, OwnerIdBytes } from "./Owner.ts";
|
|
46
48
|
import {
|
|
47
49
|
OwnerEncryptionKey,
|
|
@@ -475,8 +477,17 @@ export type MutationKind = "insert" | "update" | "upsert";
|
|
|
475
477
|
* inadvertently generate a large volume of CRDT messages. Each mutation
|
|
476
478
|
* produces exactly one {@link CrdtMessage} containing all provided columns.
|
|
477
479
|
*
|
|
478
|
-
*
|
|
479
|
-
*
|
|
480
|
+
* Each mutation must fit within {@link maxMutationSize}. Give every column a
|
|
481
|
+
* Type with a maximum length, such as {@link NonEmptyTrimmedString1000} or
|
|
482
|
+
* `maxLength(100_000)(Uint8Array)`, so that a table's largest values add up to
|
|
483
|
+
* less than the limit and input that is too large is rejected where it enters
|
|
484
|
+
* the app. A larger mutation throws before anything is saved, so the code after
|
|
485
|
+
* it does not run. Check unbounded input with {@link Evolu.getMutationSize}.
|
|
486
|
+
* Large binary data, such as images or videos, does not belong in a single
|
|
487
|
+
* mutation; a chunked API for it is planned.
|
|
488
|
+
*
|
|
489
|
+
* Binary values are copied when the mutation is made, so later changes to a
|
|
490
|
+
* `Uint8Array` do not change what is saved.
|
|
480
491
|
*
|
|
481
492
|
* - **insert**: all non-nullable columns required, nullable columns optional,
|
|
482
493
|
* `id` omitted (auto-generated)
|
|
@@ -503,7 +514,13 @@ export interface MutationOptions {
|
|
|
503
514
|
* Called after the mutation's changes are stored and subscribed queries
|
|
504
515
|
* reflect them. Useful for follow-up work (e.g., notifications, navigation)
|
|
505
516
|
* after insert, update, or upsert. It never runs when the database is
|
|
506
|
-
* 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.
|
|
507
524
|
*
|
|
508
525
|
* Stored does not always mean visible. A change quarantined for
|
|
509
526
|
* {@link QuarantineReason.TimestampDrift} is stored in
|