@evolu/common 6.0.1-preview.9 → 7.1.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/README.md +27 -29
- package/dist/src/Array.d.ts +317 -12
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +219 -9
- package/dist/src/Assert.d.ts +0 -13
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +0 -15
- package/dist/src/Brand.d.ts +75 -0
- package/dist/src/Brand.d.ts.map +1 -0
- package/dist/src/Brand.js +1 -0
- package/dist/src/Buffer.d.ts +1 -1
- package/dist/src/Buffer.d.ts.map +1 -1
- package/dist/src/Buffer.js +8 -7
- package/dist/src/Cache.d.ts +39 -0
- package/dist/src/Cache.d.ts.map +1 -0
- package/dist/src/Cache.js +47 -0
- package/dist/src/Callbacks.d.ts +45 -12
- package/dist/src/Callbacks.d.ts.map +1 -1
- package/dist/src/Callbacks.js +14 -7
- package/dist/src/Console.d.ts +32 -7
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +73 -10
- package/dist/src/Crypto.d.ts +56 -42
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +45 -54
- package/dist/src/Evolu/Db.d.ts +162 -74
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +284 -702
- package/dist/src/Evolu/Diff.d.ts +3 -3
- package/dist/src/Evolu/Diff.d.ts.map +1 -1
- package/dist/src/Evolu/Diff.js +7 -5
- package/dist/src/Evolu/Evolu.d.ts +213 -133
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +189 -180
- package/dist/src/Evolu/Internal.d.ts +2 -4
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +2 -4
- package/dist/src/Evolu/LocalAuth.d.ts +150 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +174 -0
- package/dist/src/Evolu/Owner.d.ts +273 -120
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +130 -104
- package/dist/src/Evolu/Platform.d.ts +9 -7
- package/dist/src/Evolu/Platform.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.d.ts +268 -240
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +595 -447
- package/dist/src/Evolu/Public.d.ts +7 -9
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +3 -4
- package/dist/src/Evolu/PublicKysely.d.ts.map +1 -1
- package/dist/src/Evolu/PublicKysely.js +3 -4
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Query.js +1 -1
- package/dist/src/Evolu/Relay.d.ts +91 -8
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +214 -88
- package/dist/src/Evolu/Schema.d.ts +125 -47
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +175 -31
- package/dist/src/Evolu/Storage.d.ts +249 -27
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +198 -92
- package/dist/src/Evolu/Sync.d.ts +68 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +469 -20
- package/dist/src/Evolu/Timestamp.d.ts +83 -30
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +79 -34
- package/dist/src/Identicon.d.ts +35 -0
- package/dist/src/Identicon.d.ts.map +1 -0
- package/dist/src/Identicon.js +143 -0
- package/dist/src/Instances.d.ts +34 -0
- package/dist/src/Instances.d.ts.map +1 -0
- package/dist/src/Instances.js +44 -0
- package/dist/src/Number.d.ts +4 -3
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +5 -4
- package/dist/src/Object.d.ts +10 -4
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +9 -3
- package/dist/src/Order.d.ts +0 -5
- package/dist/src/Order.d.ts.map +1 -1
- package/dist/src/Order.js +0 -5
- package/dist/src/Platform.d.ts +15 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +17 -0
- package/dist/src/Random.d.ts +3 -7
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +0 -5
- package/dist/src/Ref.d.ts.map +1 -1
- package/dist/src/Relation.d.ts +79 -0
- package/dist/src/Relation.d.ts.map +1 -0
- package/dist/src/Relation.js +127 -0
- package/dist/src/Resources.d.ts +118 -0
- package/dist/src/Resources.d.ts.map +1 -0
- package/dist/src/Resources.js +197 -0
- package/dist/src/Result.d.ts +189 -59
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +30 -241
- package/dist/src/Skiplist.js +2 -1
- package/dist/src/Sqlite.d.ts +89 -5
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +136 -9
- package/dist/src/Store.d.ts +0 -5
- package/dist/src/Store.d.ts.map +1 -1
- package/dist/src/Store.js +0 -5
- package/dist/src/Task.d.ts +580 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +464 -0
- package/dist/src/Time.d.ts +66 -6
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +96 -7
- package/dist/src/Type.d.ts +677 -344
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +718 -467
- package/dist/src/Types.d.ts +38 -75
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -7
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -23
- package/dist/src/Worker.d.ts +39 -16
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -9
- package/dist/src/index.d.ts +8 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +8 -3
- package/package.json +14 -13
- package/src/Array.ts +396 -19
- package/src/Assert.ts +0 -35
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +7 -7
- package/src/Cache.ts +79 -0
- package/src/Callbacks.ts +62 -22
- package/src/Console.ts +92 -12
- package/src/Crypto.ts +99 -91
- package/src/Evolu/Db.ts +519 -1026
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +476 -360
- package/src/Evolu/Internal.ts +2 -4
- package/src/Evolu/LocalAuth.ts +463 -0
- package/src/Evolu/Owner.ts +355 -228
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +846 -742
- package/src/Evolu/Public.ts +8 -15
- package/src/Evolu/PublicKysely.ts +4 -5
- package/src/Evolu/Query.ts +3 -2
- package/src/Evolu/Relay.ts +406 -103
- package/src/Evolu/Schema.ts +323 -91
- package/src/Evolu/Storage.ts +559 -137
- package/src/Evolu/Sync.ts +819 -36
- package/src/Evolu/Timestamp.ts +90 -58
- package/src/Identicon.ts +197 -0
- package/src/Instances.ts +90 -0
- package/src/Number.ts +6 -10
- package/src/Object.ts +13 -5
- package/src/Order.ts +0 -6
- package/src/Platform.ts +20 -0
- package/src/Random.ts +3 -8
- package/src/Ref.ts +0 -5
- package/src/Relation.ts +234 -0
- package/src/Resources.ts +367 -0
- package/src/Result.ts +196 -61
- package/src/Skiplist.ts +1 -1
- package/src/Sqlite.ts +152 -17
- package/src/Store.ts +0 -5
- package/src/Task.ts +894 -0
- package/src/Time.ts +179 -10
- package/src/Type.ts +1136 -731
- package/src/Types.ts +43 -77
- package/src/WebSocket.ts +27 -31
- package/src/Worker.ts +72 -29
- package/src/index.ts +8 -3
- package/dist/src/Evolu/Config.d.ts +0 -69
- package/dist/src/Evolu/Config.d.ts.map +0 -1
- package/dist/src/Evolu/Config.js +0 -9
- package/dist/src/Evolu/Kysely.d.ts +0 -6
- package/dist/src/Evolu/Kysely.d.ts.map +0 -1
- package/dist/src/Evolu/Kysely.js +0 -21
- package/dist/src/ManyToManyMap.d.ts +0 -26
- package/dist/src/ManyToManyMap.d.ts.map +0 -1
- package/dist/src/ManyToManyMap.js +0 -92
- package/dist/src/NanoId.d.ts +0 -27
- package/dist/src/NanoId.d.ts.map +0 -1
- package/dist/src/NanoId.js +0 -6
- package/dist/src/Promise.d.ts +0 -180
- package/dist/src/Promise.d.ts.map +0 -1
- package/dist/src/Promise.js +0 -176
- package/src/Evolu/Config.ts +0 -83
- package/src/Evolu/Kysely.ts +0 -38
- package/src/ManyToManyMap.ts +0 -140
- package/src/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/src/Evolu/Storage.ts
CHANGED
|
@@ -1,10 +1,319 @@
|
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
2
|
+
import {
|
|
3
|
+
firstInArray,
|
|
4
|
+
isNonEmptyReadonlyArray,
|
|
5
|
+
NonEmptyReadonlyArray,
|
|
6
|
+
} from "../Array.js";
|
|
7
|
+
import { assert } from "../Assert.js";
|
|
8
|
+
import { Brand } from "../Brand.js";
|
|
9
|
+
import { concatBytes } from "../Buffer.js";
|
|
10
|
+
import { decrement } from "../Number.js";
|
|
11
|
+
import { RandomDep } from "../Random.js";
|
|
12
|
+
import { err, ok, Result } from "../Result.js";
|
|
13
|
+
import { sql, SqliteDep, SqliteError, SqliteValue } from "../Sqlite.js";
|
|
14
|
+
import { MaybeAsync } from "../Task.js";
|
|
15
|
+
import {
|
|
16
|
+
Boolean,
|
|
17
|
+
brand,
|
|
18
|
+
Id,
|
|
19
|
+
Int64String,
|
|
20
|
+
NonNegativeInt,
|
|
21
|
+
nullOr,
|
|
22
|
+
object,
|
|
23
|
+
PositiveInt,
|
|
24
|
+
record,
|
|
25
|
+
String,
|
|
26
|
+
TypeError,
|
|
27
|
+
} from "../Type.js";
|
|
28
|
+
import {
|
|
29
|
+
BaseOwnerError,
|
|
30
|
+
Owner,
|
|
31
|
+
OwnerId,
|
|
32
|
+
OwnerIdBytes,
|
|
33
|
+
OwnerWriteKey,
|
|
34
|
+
} from "./Owner.js";
|
|
35
|
+
import { systemColumns } from "./Schema.js";
|
|
36
|
+
import { orderTimestampBytes, Timestamp, TimestampBytes } from "./Timestamp.js";
|
|
37
|
+
|
|
38
|
+
export interface StorageConfig {
|
|
39
|
+
/**
|
|
40
|
+
* Callback called before an attempt to write, to check if an {@link OwnerId}
|
|
41
|
+
* has sufficient quota for the write.
|
|
42
|
+
*
|
|
43
|
+
* The callback receives the {@link OwnerId} and the total bytes that would be
|
|
44
|
+
* stored after the write (current stored bytes plus incoming bytes), and
|
|
45
|
+
* returns a {@link MaybeAsync} boolean: `true` to allow the write, or `false`
|
|
46
|
+
* to deny it due to quota limits.
|
|
47
|
+
*
|
|
48
|
+
* The callback can be synchronous (for SQLite or in-memory checks) or
|
|
49
|
+
* asynchronous (for calling remote APIs).
|
|
50
|
+
*
|
|
51
|
+
* The callback returns a boolean rather than an error because error handling
|
|
52
|
+
* and logging are the responsibility of the callback implementation.
|
|
53
|
+
*
|
|
54
|
+
* ### Example
|
|
55
|
+
*
|
|
56
|
+
* ```ts
|
|
57
|
+
* // Client
|
|
58
|
+
* // evolu.subscribeError
|
|
59
|
+
*
|
|
60
|
+
* // Relay
|
|
61
|
+
* isOwnerWithinQuota: (ownerId, requiredBytes) => {
|
|
62
|
+
* console.log(ownerId, requiredBytes);
|
|
63
|
+
* // Check error via evolu.subscribeError
|
|
64
|
+
* return true;
|
|
65
|
+
* };
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
readonly isOwnerWithinQuota: (
|
|
69
|
+
ownerId: OwnerId,
|
|
70
|
+
requiredBytes: PositiveInt,
|
|
71
|
+
) => MaybeAsync<boolean>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Evolu Storage
|
|
76
|
+
*
|
|
77
|
+
* Evolu protocol using Storage is agnostic to storage implementation
|
|
78
|
+
* details—any storage can be plugged in, as long as it implements this
|
|
79
|
+
* interface. Implementations must handle their own errors; return values only
|
|
80
|
+
* indicate overall success or failure.
|
|
81
|
+
*
|
|
82
|
+
* The Storage API is synchronous because SQLite's synchronous API is the
|
|
83
|
+
* fastest way to use SQLite. Synchronous bindings (like better-sqlite3) call
|
|
84
|
+
* SQLite's C API directly with no context switching between the event loop and
|
|
85
|
+
* native code, and no promise microtasks or await overhead.
|
|
86
|
+
*
|
|
87
|
+
* The only exception is {@link Storage#writeMessages}, which is async to allow
|
|
88
|
+
* for async validation logic before writing to storage. The write operation
|
|
89
|
+
* itself remains synchronous.
|
|
90
|
+
*/
|
|
91
|
+
export interface Storage {
|
|
92
|
+
readonly getSize: (ownerId: OwnerIdBytes) => NonNegativeInt | null;
|
|
93
|
+
|
|
94
|
+
readonly fingerprint: (
|
|
95
|
+
ownerId: OwnerIdBytes,
|
|
96
|
+
begin: NonNegativeInt,
|
|
97
|
+
end: NonNegativeInt,
|
|
98
|
+
) => Fingerprint | null;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Computes fingerprints with their upper bounds in one call.
|
|
102
|
+
*
|
|
103
|
+
* This function can be replaced with many fingerprint/findLowerBound calls,
|
|
104
|
+
* but implementations can leverage it for batching and more efficient
|
|
105
|
+
* fingerprint computation.
|
|
106
|
+
*/
|
|
107
|
+
readonly fingerprintRanges: (
|
|
108
|
+
ownerId: OwnerIdBytes,
|
|
109
|
+
buckets: ReadonlyArray<NonNegativeInt>,
|
|
110
|
+
upperBound?: RangeUpperBound,
|
|
111
|
+
) => ReadonlyArray<FingerprintRange> | null;
|
|
112
|
+
|
|
113
|
+
readonly findLowerBound: (
|
|
114
|
+
ownerId: OwnerIdBytes,
|
|
115
|
+
begin: NonNegativeInt,
|
|
116
|
+
end: NonNegativeInt,
|
|
117
|
+
upperBound: RangeUpperBound,
|
|
118
|
+
) => NonNegativeInt | null;
|
|
119
|
+
|
|
120
|
+
readonly iterate: (
|
|
121
|
+
ownerId: OwnerIdBytes,
|
|
122
|
+
begin: NonNegativeInt,
|
|
123
|
+
end: NonNegativeInt,
|
|
124
|
+
callback: (timestamp: TimestampBytes, index: NonNegativeInt) => boolean,
|
|
125
|
+
) => void;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Validates the {@link OwnerWriteKey} for the given {@link Owner}.
|
|
129
|
+
*
|
|
130
|
+
* Returns `true` if the write key is valid, `false` otherwise.
|
|
131
|
+
*/
|
|
132
|
+
readonly validateWriteKey: (
|
|
133
|
+
ownerId: OwnerIdBytes,
|
|
134
|
+
writeKey: OwnerWriteKey,
|
|
135
|
+
) => boolean;
|
|
136
|
+
|
|
137
|
+
/** Sets the {@link OwnerWriteKey} for the given {@link Owner}. */
|
|
138
|
+
readonly setWriteKey: (
|
|
139
|
+
ownerId: OwnerIdBytes,
|
|
140
|
+
writeKey: OwnerWriteKey,
|
|
141
|
+
) => boolean;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Write encrypted {@link CrdtMessage}s to storage.
|
|
145
|
+
*
|
|
146
|
+
* Must use a mutex per ownerId to ensure sequential processing and proper
|
|
147
|
+
* protocol logic handling during sync operations.
|
|
148
|
+
*
|
|
149
|
+
* TODO: Use MaybeAsync
|
|
150
|
+
*/
|
|
151
|
+
readonly writeMessages: (
|
|
152
|
+
ownerIdBytes: OwnerIdBytes,
|
|
153
|
+
messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>,
|
|
154
|
+
) => MaybeAsync<Result<void, StorageWriteError | StorageQuotaError>>;
|
|
155
|
+
|
|
156
|
+
/** Read encrypted {@link DbChange}s from storage. */
|
|
157
|
+
readonly readDbChange: (
|
|
158
|
+
ownerId: OwnerIdBytes,
|
|
159
|
+
timestamp: TimestampBytes,
|
|
160
|
+
) => EncryptedDbChange | null;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Delete all data for the given {@link Owner}.
|
|
164
|
+
*
|
|
165
|
+
* Returns `true` on success, `false` on failure.
|
|
166
|
+
*/
|
|
167
|
+
readonly deleteOwner: (ownerId: OwnerIdBytes) => boolean;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface StorageDep {
|
|
171
|
+
readonly storage: Storage;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Error indicating a serious write failure. */
|
|
175
|
+
export interface StorageWriteError extends BaseOwnerError {
|
|
176
|
+
readonly type: "StorageWriteError";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Error when storage or billing quota is exceeded. */
|
|
180
|
+
export interface StorageQuotaError extends BaseOwnerError {
|
|
181
|
+
readonly type: "StorageQuotaError";
|
|
182
|
+
}
|
|
183
|
+
|
|
1
184
|
/**
|
|
2
|
-
*
|
|
185
|
+
* A cryptographic hash used for efficiently comparing collections of
|
|
186
|
+
* {@link TimestampBytes}s.
|
|
187
|
+
*
|
|
188
|
+
* It consists of the first {@link fingerprintSize} bytes of the SHA-256 hash of
|
|
189
|
+
* one or more timestamps.
|
|
190
|
+
*/
|
|
191
|
+
export type Fingerprint = Uint8Array & Brand<"Fingerprint">;
|
|
192
|
+
|
|
193
|
+
export const fingerprintSize = NonNegativeInt.orThrow(12);
|
|
194
|
+
|
|
195
|
+
/** A fingerprint of an empty range. */
|
|
196
|
+
export const zeroFingerprint = new Uint8Array(fingerprintSize) as Fingerprint;
|
|
197
|
+
|
|
198
|
+
export interface BaseRange {
|
|
199
|
+
readonly upperBound: RangeUpperBound;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Union type for Range's upperBound: either a {@link TimestampBytes} or
|
|
204
|
+
* {@link InfiniteUpperBound}.
|
|
205
|
+
*/
|
|
206
|
+
export type RangeUpperBound = TimestampBytes | InfiniteUpperBound;
|
|
207
|
+
|
|
208
|
+
export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
|
|
209
|
+
export type InfiniteUpperBound = typeof InfiniteUpperBound;
|
|
210
|
+
|
|
211
|
+
export const RangeType = {
|
|
212
|
+
Fingerprint: 1,
|
|
213
|
+
Skip: 0,
|
|
214
|
+
Timestamps: 2,
|
|
215
|
+
} as const;
|
|
216
|
+
|
|
217
|
+
export type RangeType = (typeof RangeType)[keyof typeof RangeType];
|
|
218
|
+
|
|
219
|
+
export interface SkipRange extends BaseRange {
|
|
220
|
+
readonly type: typeof RangeType.Skip;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface FingerprintRange extends BaseRange {
|
|
224
|
+
readonly type: typeof RangeType.Fingerprint;
|
|
225
|
+
readonly fingerprint: Fingerprint;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface TimestampsRange extends BaseRange {
|
|
229
|
+
readonly type: typeof RangeType.Timestamps;
|
|
230
|
+
readonly timestamps: ReadonlyArray<TimestampBytes>;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export type Range = SkipRange | FingerprintRange | TimestampsRange;
|
|
234
|
+
|
|
235
|
+
/** An encrypted {@link CrdtMessage}. */
|
|
236
|
+
export interface EncryptedCrdtMessage {
|
|
237
|
+
readonly timestamp: Timestamp;
|
|
238
|
+
readonly change: EncryptedDbChange;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Encrypted DbChange */
|
|
242
|
+
export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* A CRDT message combining a unique {@link Timestamp} with a {@link DbChange}.
|
|
246
|
+
*
|
|
247
|
+
* Used in Evolu's sync protocol to replicate data changes across devices. Evolu
|
|
248
|
+
* operates as a durable queue, providing exactly-once delivery guarantees for
|
|
249
|
+
* reliable synchronization across application restarts and network failures.
|
|
250
|
+
*/
|
|
251
|
+
export interface CrdtMessage {
|
|
252
|
+
readonly timestamp: Timestamp;
|
|
253
|
+
readonly change: DbChange;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export const DbChangeValues = record(String, SqliteValue);
|
|
257
|
+
export type DbChangeValues = typeof DbChangeValues.Type;
|
|
258
|
+
|
|
259
|
+
// "createdAt" and "updatedAt" are derived from Timestamp.
|
|
260
|
+
// "id" and "isDeleted" are encoded separately to save bytes
|
|
261
|
+
// "ownerId" is part of the protocol message
|
|
262
|
+
const forbiddenSystemColumns = systemColumns.concat("id");
|
|
263
|
+
|
|
264
|
+
export const ValidDbChangeValues = brand(
|
|
265
|
+
"ValidDbChangeValues",
|
|
266
|
+
DbChangeValues,
|
|
267
|
+
(value) => {
|
|
268
|
+
const invalidColumns = forbiddenSystemColumns.filter((key) => key in value);
|
|
269
|
+
if (invalidColumns.length > 0)
|
|
270
|
+
return err<ValidDbChangeValuesError>({
|
|
271
|
+
type: "ValidDbChangeValues",
|
|
272
|
+
value,
|
|
273
|
+
invalidColumns,
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
return ok(value);
|
|
277
|
+
},
|
|
278
|
+
);
|
|
279
|
+
export type ValidDbChangeValues = typeof ValidDbChangeValues.Type;
|
|
280
|
+
|
|
281
|
+
export interface ValidDbChangeValuesError
|
|
282
|
+
extends TypeError<"ValidDbChangeValues"> {
|
|
283
|
+
readonly invalidColumns: ReadonlyArray<string>;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* A DbChange is a change to a table row. Together with a unique
|
|
288
|
+
* {@link Timestamp}, it forms a {@link CrdtMessage}.
|
|
289
|
+
*/
|
|
290
|
+
export const DbChange = object({
|
|
291
|
+
table: String,
|
|
292
|
+
id: Id,
|
|
293
|
+
values: ValidDbChangeValues,
|
|
294
|
+
isInsert: Boolean,
|
|
295
|
+
isDelete: nullOr(Boolean),
|
|
296
|
+
});
|
|
297
|
+
export type DbChange = typeof DbChange.Type;
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Common interface for both client and relay SQLite storages.
|
|
3
301
|
*
|
|
4
302
|
* Evolu uses a Skiplist, which leverages SQLite indexes. The core logic is
|
|
5
303
|
* implemented in SQL, so it doesn't have to make roundtrips to the DB.
|
|
6
304
|
*
|
|
7
|
-
*
|
|
305
|
+
* While the SQL implementation may look sophisticated, it's conceptually simple
|
|
306
|
+
* and LLMs can explain how it works. The Skiplist data structure is well
|
|
307
|
+
* explained in [this Stack Overflow
|
|
308
|
+
* answer](https://stackoverflow.com/questions/61944198/what-is-a-zip-tree-and-how-does-it-work).
|
|
309
|
+
* The logic resembles [Negentropy's C++
|
|
310
|
+
* storage](https://github.com/hoytech/negentropy), except we use a Skiplist to
|
|
311
|
+
* leverage SQLite indexes, which makes the code simpler.
|
|
312
|
+
*
|
|
313
|
+
* Note: A paid review by the SQLite team is planned, as they use the same
|
|
314
|
+
* algorithm for their rsync tool.
|
|
315
|
+
*
|
|
316
|
+
* The ideal storage for a Relay should use an architecture like
|
|
8
317
|
* [strfry](https://github.com/hoytech/strfry) (a KV storage), but with Skiplist
|
|
9
318
|
* to ensure that insertion order doesn't matter (local-first apps can often
|
|
10
319
|
* write in the past.)
|
|
@@ -19,117 +328,104 @@
|
|
|
19
328
|
* each other, if necessary. One relay should handle hundreds of thousands of
|
|
20
329
|
* users, and when it goes down, nothing happens, because it will be
|
|
21
330
|
* synchronized later.
|
|
22
|
-
*
|
|
23
|
-
* @module
|
|
24
331
|
*/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
binaryOwnerIdToOwnerId,
|
|
37
|
-
binaryTimestampToFingerprint,
|
|
38
|
-
Fingerprint,
|
|
39
|
-
FingerprintRange,
|
|
40
|
-
InfiniteUpperBound,
|
|
41
|
-
RangeType,
|
|
42
|
-
RangeUpperBound,
|
|
43
|
-
Storage,
|
|
44
|
-
zeroFingerprint,
|
|
45
|
-
} from "./Protocol.js";
|
|
46
|
-
import { BinaryTimestamp, orderBinaryTimestamp } from "./Timestamp.js";
|
|
47
|
-
|
|
48
|
-
/** Common interface for both client and relay SQLite storages. */
|
|
49
|
-
export interface SqliteStorageBase {
|
|
332
|
+
export interface BaseSqliteStorage
|
|
333
|
+
extends Pick<
|
|
334
|
+
Storage,
|
|
335
|
+
| "getSize"
|
|
336
|
+
| "fingerprint"
|
|
337
|
+
| "fingerprintRanges"
|
|
338
|
+
| "findLowerBound"
|
|
339
|
+
| "iterate"
|
|
340
|
+
| "deleteOwner"
|
|
341
|
+
> {
|
|
342
|
+
/** Inserts a timestamp for an owner into the skiplist-based storage. */
|
|
50
343
|
readonly insertTimestamp: (
|
|
51
|
-
ownerId:
|
|
52
|
-
timestamp:
|
|
344
|
+
ownerId: OwnerIdBytes,
|
|
345
|
+
timestamp: TimestampBytes,
|
|
346
|
+
strategy: StorageInsertTimestampStrategy,
|
|
53
347
|
) => Result<void, SqliteError>;
|
|
54
348
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
readonly
|
|
60
|
-
|
|
349
|
+
/**
|
|
350
|
+
* Efficiently checks which timestamps already exist in the database using a
|
|
351
|
+
* single CTE query instead of N individual queries.
|
|
352
|
+
*/
|
|
353
|
+
readonly getExistingTimestamps: (
|
|
354
|
+
ownerIdBytes: OwnerIdBytes,
|
|
355
|
+
timestampsBytes: NonEmptyReadonlyArray<TimestampBytes>,
|
|
356
|
+
) => Result<ReadonlyArray<TimestampBytes>, SqliteError>;
|
|
61
357
|
}
|
|
62
358
|
|
|
63
|
-
export interface
|
|
64
|
-
readonly storage:
|
|
359
|
+
export interface BaseSqliteStorageDep {
|
|
360
|
+
readonly storage: BaseSqliteStorage;
|
|
65
361
|
}
|
|
66
362
|
|
|
67
|
-
export type SqliteStorageDeps =
|
|
363
|
+
export type SqliteStorageDeps = RandomDep & SqliteDep;
|
|
68
364
|
|
|
69
|
-
export interface
|
|
365
|
+
export interface CreateBaseSqliteStorageConfig extends StorageConfig {
|
|
70
366
|
onStorageError: (error: SqliteError) => void;
|
|
71
367
|
}
|
|
72
368
|
|
|
73
|
-
|
|
369
|
+
/**
|
|
370
|
+
* Creates a {@link BaseSqliteStorage} implementation.
|
|
371
|
+
*
|
|
372
|
+
* # Stateless Design
|
|
373
|
+
*
|
|
374
|
+
* This implementation is fully stateless - it requires no in-memory state
|
|
375
|
+
* between invocations. All necessary metadata (timestamp bounds for insertion
|
|
376
|
+
* strategy optimization) is persisted in the evolu_usage table. This makes
|
|
377
|
+
* Evolu Relay suitable for stateless serverless environments like AWS Lambda,
|
|
378
|
+
* Cloudflare Workers with Durable Objects, and other platforms where memory
|
|
379
|
+
* doesn't persist between requests. While not extensively tested in all these
|
|
380
|
+
* environments yet, the stateless design should work well across them.
|
|
381
|
+
*/
|
|
382
|
+
export const createBaseSqliteStorage =
|
|
74
383
|
(deps: SqliteStorageDeps) =>
|
|
75
|
-
(
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
OwnerId,
|
|
83
|
-
{
|
|
84
|
-
minT: BinaryTimestamp;
|
|
85
|
-
maxT: BinaryTimestamp;
|
|
86
|
-
}
|
|
87
|
-
>();
|
|
88
|
-
|
|
89
|
-
return ok({
|
|
90
|
-
insertTimestamp: (ownerId: BinaryOwnerId, timestamp: BinaryTimestamp) => {
|
|
91
|
-
const ownerIdString = binaryOwnerIdToOwnerId(ownerId);
|
|
384
|
+
(config: CreateBaseSqliteStorageConfig): BaseSqliteStorage => {
|
|
385
|
+
return {
|
|
386
|
+
insertTimestamp: (
|
|
387
|
+
ownerId: OwnerIdBytes,
|
|
388
|
+
timestamp: TimestampBytes,
|
|
389
|
+
strategy: StorageInsertTimestampStrategy,
|
|
390
|
+
) => {
|
|
92
391
|
const level = randomSkiplistLevel(deps);
|
|
392
|
+
return insertTimestamp(deps)(ownerId, timestamp, level, strategy);
|
|
393
|
+
},
|
|
93
394
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
395
|
+
getExistingTimestamps: (ownerIdBytes, timestampsBytes) => {
|
|
396
|
+
const concatenatedTimestamps = concatBytes(...timestampsBytes);
|
|
397
|
+
|
|
398
|
+
const result = deps.sqlite.exec<{
|
|
399
|
+
timestampBytes: TimestampBytes;
|
|
400
|
+
}>(sql`
|
|
401
|
+
with recursive
|
|
402
|
+
split_timestamps(timestampBytes, pos) as (
|
|
403
|
+
select
|
|
404
|
+
substr(${concatenatedTimestamps}, 1, 16),
|
|
405
|
+
17 as pos
|
|
406
|
+
union all
|
|
407
|
+
select
|
|
408
|
+
substr(${concatenatedTimestamps}, pos, 16),
|
|
409
|
+
pos + 16
|
|
410
|
+
from split_timestamps
|
|
411
|
+
where pos <= length(${concatenatedTimestamps})
|
|
412
|
+
)
|
|
413
|
+
select s.timestampBytes
|
|
414
|
+
from
|
|
415
|
+
split_timestamps s
|
|
416
|
+
join evolu_timestamp t
|
|
417
|
+
on t.ownerId = ${ownerIdBytes} and s.timestampBytes = t.t;
|
|
418
|
+
`);
|
|
115
419
|
|
|
116
|
-
if (
|
|
117
|
-
strategy = "append";
|
|
118
|
-
stats.maxT = timestamp;
|
|
119
|
-
} else if (orderBinaryTimestamp(timestamp, stats.minT) === -1) {
|
|
120
|
-
strategy = "prepend";
|
|
121
|
-
stats.minT = timestamp;
|
|
122
|
-
} else {
|
|
123
|
-
strategy = "insert";
|
|
124
|
-
}
|
|
420
|
+
if (!result.ok) return result;
|
|
125
421
|
|
|
126
|
-
return
|
|
422
|
+
return ok(result.value.rows.map((row) => row.timestampBytes));
|
|
127
423
|
},
|
|
128
424
|
|
|
129
425
|
getSize: (ownerId) => {
|
|
130
426
|
const size = getSize(deps)(ownerId);
|
|
131
427
|
if (!size.ok) {
|
|
132
|
-
|
|
428
|
+
config.onStorageError(size.error);
|
|
133
429
|
return null;
|
|
134
430
|
}
|
|
135
431
|
return size.value;
|
|
@@ -139,7 +435,7 @@ export const createSqliteStorageBase =
|
|
|
139
435
|
assertBeginEnd(begin, end);
|
|
140
436
|
const result = fingerprint(deps)(ownerId, begin, end);
|
|
141
437
|
if (!result.ok) {
|
|
142
|
-
|
|
438
|
+
config.onStorageError(result.error);
|
|
143
439
|
return null;
|
|
144
440
|
}
|
|
145
441
|
return result.value;
|
|
@@ -148,7 +444,7 @@ export const createSqliteStorageBase =
|
|
|
148
444
|
fingerprintRanges: (ownerId, buckets, upperBound) => {
|
|
149
445
|
const ranges = fingerprintRanges(deps)(ownerId, buckets, upperBound);
|
|
150
446
|
if (!ranges.ok) {
|
|
151
|
-
|
|
447
|
+
config.onStorageError(ranges.error);
|
|
152
448
|
return null;
|
|
153
449
|
}
|
|
154
450
|
return ranges.value;
|
|
@@ -162,7 +458,7 @@ export const createSqliteStorageBase =
|
|
|
162
458
|
upperBound,
|
|
163
459
|
);
|
|
164
460
|
if (!lowerBound.ok) {
|
|
165
|
-
|
|
461
|
+
config.onStorageError(lowerBound.error);
|
|
166
462
|
return null;
|
|
167
463
|
}
|
|
168
464
|
return lowerBound.value;
|
|
@@ -176,7 +472,7 @@ export const createSqliteStorageBase =
|
|
|
176
472
|
// This is much faster than SQL limit with offset.
|
|
177
473
|
const first = getTimestampByIndex(deps)(ownerId, begin);
|
|
178
474
|
if (!first.ok) {
|
|
179
|
-
|
|
475
|
+
config.onStorageError(first.error);
|
|
180
476
|
return;
|
|
181
477
|
}
|
|
182
478
|
|
|
@@ -193,7 +489,7 @@ export const createSqliteStorageBase =
|
|
|
193
489
|
* implementing chunking, be sure to run performance tests (including
|
|
194
490
|
* fetching one by one).
|
|
195
491
|
*/
|
|
196
|
-
const result = deps.sqlite.exec<{ t:
|
|
492
|
+
const result = deps.sqlite.exec<{ t: TimestampBytes }>(sql`
|
|
197
493
|
select t
|
|
198
494
|
from evolu_timestamp
|
|
199
495
|
where ownerId = ${ownerId} and t > ${first.value}
|
|
@@ -201,12 +497,12 @@ export const createSqliteStorageBase =
|
|
|
201
497
|
limit ${length - 1};
|
|
202
498
|
`);
|
|
203
499
|
if (!result.ok) {
|
|
204
|
-
|
|
500
|
+
config.onStorageError(result.error);
|
|
205
501
|
return;
|
|
206
502
|
}
|
|
207
503
|
|
|
208
504
|
for (let i = 0; i < result.value.rows.length; i++) {
|
|
209
|
-
const index = (begin + 1 + i)
|
|
505
|
+
const index = NonNegativeInt.orThrow(begin + 1 + i);
|
|
210
506
|
if (!callback(result.value.rows[i].t, index)) return;
|
|
211
507
|
}
|
|
212
508
|
},
|
|
@@ -216,19 +512,21 @@ export const createSqliteStorageBase =
|
|
|
216
512
|
delete from evolu_timestamp where ownerId = ${ownerId};
|
|
217
513
|
`);
|
|
218
514
|
if (!result.ok) {
|
|
219
|
-
|
|
515
|
+
config.onStorageError(result.error);
|
|
220
516
|
return false;
|
|
221
517
|
}
|
|
222
518
|
return true;
|
|
223
519
|
},
|
|
224
|
-
}
|
|
520
|
+
};
|
|
225
521
|
};
|
|
226
522
|
|
|
227
523
|
const assertBeginEnd = (begin: NonNegativeInt, end: NonNegativeInt) => {
|
|
228
524
|
assert(begin <= end, "invalid begin or end");
|
|
229
525
|
};
|
|
230
526
|
|
|
231
|
-
const
|
|
527
|
+
export const createBaseSqliteStorageTables = (
|
|
528
|
+
deps: SqliteDep,
|
|
529
|
+
): Result<void, SqliteError> => {
|
|
232
530
|
for (const query of [
|
|
233
531
|
/**
|
|
234
532
|
* Creates the `evolu_timestamp` table for storing timestamps of multiple
|
|
@@ -241,19 +539,13 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
241
539
|
*
|
|
242
540
|
* Columns:
|
|
243
541
|
*
|
|
244
|
-
* - `t` –
|
|
542
|
+
* - `t` – TimestampBytes
|
|
245
543
|
* - `h1`/`h2` – 12-byte fingerprint split into two integers for fast XOR
|
|
246
544
|
* - `c` – incremental count
|
|
247
|
-
* - `l` – Skiplist level (1 to
|
|
248
|
-
*
|
|
249
|
-
* For scaling or isolation, sharding is possible—each owner can have a
|
|
250
|
-
* separate SQLite database.
|
|
251
|
-
*
|
|
252
|
-
* Maybe we could use an integer surrogate key for ownerId, but it's fast
|
|
253
|
-
* enough even without it.
|
|
545
|
+
* - `l` – Skiplist level (1 to 10)
|
|
254
546
|
*/
|
|
255
547
|
sql`
|
|
256
|
-
create table
|
|
548
|
+
create table evolu_timestamp (
|
|
257
549
|
"ownerId" blob not null,
|
|
258
550
|
"t" blob not null,
|
|
259
551
|
"h1" integer,
|
|
@@ -266,7 +558,7 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
266
558
|
`,
|
|
267
559
|
|
|
268
560
|
sql`
|
|
269
|
-
create index
|
|
561
|
+
create index evolu_timestamp_index on evolu_timestamp (
|
|
270
562
|
"ownerId",
|
|
271
563
|
"l",
|
|
272
564
|
"t",
|
|
@@ -275,6 +567,26 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
275
567
|
"c"
|
|
276
568
|
);
|
|
277
569
|
`,
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Creates the `evolu_usage` table for tracking data consumption per owner.
|
|
573
|
+
*
|
|
574
|
+
* Columns:
|
|
575
|
+
*
|
|
576
|
+
* - `ownerId` – OwnerIdBytes (primary key)
|
|
577
|
+
* - `storedBytes` – total bytes stored in database
|
|
578
|
+
* - `firstTimestamp` – for timestamp insertion strategies
|
|
579
|
+
* - `lastTimestamp` – for timestamp insertion strategies
|
|
580
|
+
*/
|
|
581
|
+
sql`
|
|
582
|
+
create table evolu_usage (
|
|
583
|
+
"ownerId" blob primary key,
|
|
584
|
+
"storedBytes" integer not null,
|
|
585
|
+
"firstTimestamp" blob,
|
|
586
|
+
"lastTimestamp" blob
|
|
587
|
+
)
|
|
588
|
+
strict;
|
|
589
|
+
`,
|
|
278
590
|
]) {
|
|
279
591
|
const result = deps.sqlite.exec(query);
|
|
280
592
|
if (!result.ok) return result;
|
|
@@ -282,26 +594,56 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
282
594
|
return ok();
|
|
283
595
|
};
|
|
284
596
|
|
|
285
|
-
type
|
|
597
|
+
export type StorageInsertTimestampStrategy = "append" | "prepend" | "insert";
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Determines the insertion strategy for a timestamp based on its position
|
|
601
|
+
* relative to the current first and last timestamps.
|
|
602
|
+
*
|
|
603
|
+
* Returns a tuple with the strategy and updated timestamp bounds.
|
|
604
|
+
*/
|
|
605
|
+
export const getTimestampInsertStrategy = (
|
|
606
|
+
timestamp: TimestampBytes,
|
|
607
|
+
firstTimestamp: TimestampBytes,
|
|
608
|
+
lastTimestamp: TimestampBytes,
|
|
609
|
+
): [
|
|
610
|
+
strategy: StorageInsertTimestampStrategy,
|
|
611
|
+
firstTimestamp: TimestampBytes,
|
|
612
|
+
lastTimestamp: TimestampBytes,
|
|
613
|
+
] => {
|
|
614
|
+
if (orderTimestampBytes(timestamp, lastTimestamp) === 1) {
|
|
615
|
+
return ["append", firstTimestamp, timestamp];
|
|
616
|
+
}
|
|
617
|
+
if (orderTimestampBytes(timestamp, firstTimestamp) === -1) {
|
|
618
|
+
return ["prepend", timestamp, lastTimestamp];
|
|
619
|
+
}
|
|
620
|
+
return ["insert", firstTimestamp, lastTimestamp];
|
|
621
|
+
};
|
|
286
622
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
623
|
+
/**
|
|
624
|
+
* AFAIK, we can't do both insert and update in one query, and that's probably
|
|
625
|
+
* why append is 2x faster than insert. Prepend also has to update parents, but
|
|
626
|
+
* it's constantly fast. Insert degrades for reversed (yet LIMIT X magically
|
|
627
|
+
* fixes that) but it's OK for append.
|
|
628
|
+
*
|
|
629
|
+
* Note: SQL operations are idempotent (using `on conflict do nothing` and
|
|
630
|
+
* `changes() > 0`), but this is no longer required here since we use
|
|
631
|
+
* {@link BaseSqliteStorage.getExistingTimestamps} to filter out duplicates
|
|
632
|
+
* before insertion, which we need for quota checks anyway.
|
|
633
|
+
*
|
|
634
|
+
* TODO: Remove idempotency (`on conflict do nothing` and `changes() > 0`) since
|
|
635
|
+
* duplicates are now filtered before insertion.
|
|
636
|
+
*/
|
|
295
637
|
const insertTimestamp =
|
|
296
638
|
(deps: SqliteDep) =>
|
|
297
639
|
(
|
|
298
|
-
ownerId:
|
|
299
|
-
timestamp:
|
|
640
|
+
ownerId: OwnerIdBytes,
|
|
641
|
+
timestamp: TimestampBytes,
|
|
300
642
|
level: PositiveInt,
|
|
301
|
-
strategy:
|
|
643
|
+
strategy: StorageInsertTimestampStrategy,
|
|
302
644
|
): Result<void, SqliteError> => {
|
|
303
645
|
const [h1, h2] = fingerprintToSqliteFingerprint(
|
|
304
|
-
|
|
646
|
+
timestampBytesToFingerprint(timestamp),
|
|
305
647
|
);
|
|
306
648
|
|
|
307
649
|
let queries: Array<ReturnType<typeof sql.prepared>> = [];
|
|
@@ -819,6 +1161,13 @@ const insertTimestamp =
|
|
|
819
1161
|
return ok();
|
|
820
1162
|
};
|
|
821
1163
|
|
|
1164
|
+
export const timestampBytesToFingerprint = (
|
|
1165
|
+
timestamp: TimestampBytes,
|
|
1166
|
+
): Fingerprint => {
|
|
1167
|
+
const hash = sha256(timestamp).slice(0, fingerprintSize);
|
|
1168
|
+
return hash as Fingerprint;
|
|
1169
|
+
};
|
|
1170
|
+
|
|
822
1171
|
/**
|
|
823
1172
|
* Generates a random skiplist level in the range [1, skiplistMaxLevel].
|
|
824
1173
|
* Probabilistic approach avoids the need for explicit tree balancing.
|
|
@@ -831,7 +1180,7 @@ const randomSkiplistLevel = (deps: RandomDep): PositiveInt => {
|
|
|
831
1180
|
) {
|
|
832
1181
|
level += 1;
|
|
833
1182
|
}
|
|
834
|
-
return level
|
|
1183
|
+
return PositiveInt.orThrow(level);
|
|
835
1184
|
};
|
|
836
1185
|
|
|
837
1186
|
/**
|
|
@@ -899,7 +1248,7 @@ const sqliteFingerprintToFingerprint = ([
|
|
|
899
1248
|
|
|
900
1249
|
const getSize =
|
|
901
1250
|
(deps: SqliteDep) =>
|
|
902
|
-
(ownerId:
|
|
1251
|
+
(ownerId: OwnerIdBytes): Result<NonNegativeInt, SqliteError> => {
|
|
903
1252
|
const result = deps.sqlite.exec<{ size: NonNegativeInt }>(sql.prepared`
|
|
904
1253
|
with
|
|
905
1254
|
ml(ml) as (
|
|
@@ -942,7 +1291,7 @@ const getSize =
|
|
|
942
1291
|
const findLowerBound =
|
|
943
1292
|
(deps: SqliteDep) =>
|
|
944
1293
|
(
|
|
945
|
-
ownerId:
|
|
1294
|
+
ownerId: OwnerIdBytes,
|
|
946
1295
|
begin: NonNegativeInt,
|
|
947
1296
|
end: NonNegativeInt,
|
|
948
1297
|
upperBound: RangeUpperBound,
|
|
@@ -954,7 +1303,7 @@ const findLowerBound =
|
|
|
954
1303
|
}
|
|
955
1304
|
|
|
956
1305
|
const result = deps.sqlite.exec<{
|
|
957
|
-
t:
|
|
1306
|
+
t: TimestampBytes;
|
|
958
1307
|
}>(sql.prepared`
|
|
959
1308
|
select t
|
|
960
1309
|
from evolu_timestamp
|
|
@@ -972,14 +1321,14 @@ const findLowerBound =
|
|
|
972
1321
|
if (!count.ok) return count;
|
|
973
1322
|
|
|
974
1323
|
// `decrement` converts a count to an index.
|
|
975
|
-
return ok(decrement(count.value)
|
|
1324
|
+
return ok(NonNegativeInt.orThrow(decrement(count.value)));
|
|
976
1325
|
};
|
|
977
1326
|
|
|
978
1327
|
const getTimestampCount =
|
|
979
1328
|
(deps: SqliteDep) =>
|
|
980
1329
|
(
|
|
981
|
-
ownerId:
|
|
982
|
-
timestamp:
|
|
1330
|
+
ownerId: OwnerIdBytes,
|
|
1331
|
+
timestamp: TimestampBytes,
|
|
983
1332
|
): Result<PositiveInt, SqliteError> => {
|
|
984
1333
|
const result = deps.sqlite.exec<{
|
|
985
1334
|
count: PositiveInt;
|
|
@@ -1036,7 +1385,7 @@ const getTimestampCount =
|
|
|
1036
1385
|
const fingerprint =
|
|
1037
1386
|
(deps: SqliteDep) =>
|
|
1038
1387
|
(
|
|
1039
|
-
ownerId:
|
|
1388
|
+
ownerId: OwnerIdBytes,
|
|
1040
1389
|
begin: NonNegativeInt,
|
|
1041
1390
|
end: NonNegativeInt,
|
|
1042
1391
|
): Result<Fingerprint, SqliteError> => {
|
|
@@ -1068,14 +1417,14 @@ const fingerprint =
|
|
|
1068
1417
|
const fingerprintRanges =
|
|
1069
1418
|
(deps: SqliteDep) =>
|
|
1070
1419
|
(
|
|
1071
|
-
ownerId:
|
|
1420
|
+
ownerId: OwnerIdBytes,
|
|
1072
1421
|
buckets: ReadonlyArray<NonNegativeInt>,
|
|
1073
1422
|
upperBound: RangeUpperBound = InfiniteUpperBound,
|
|
1074
1423
|
): Result<ReadonlyArray<FingerprintRange>, SqliteError> => {
|
|
1075
1424
|
const bucketsJson = JSON.stringify(buckets);
|
|
1076
1425
|
|
|
1077
1426
|
const result = deps.sqlite.exec<{
|
|
1078
|
-
b:
|
|
1427
|
+
b: TimestampBytes | null;
|
|
1079
1428
|
h1: Int64String;
|
|
1080
1429
|
h2: Int64String;
|
|
1081
1430
|
}>(sql.prepared`
|
|
@@ -1211,11 +1560,11 @@ const x = (a: string, b: string) => sql.raw(`(${a} | ${b}) - (${a} & ${b})`);
|
|
|
1211
1560
|
export const getTimestampByIndex =
|
|
1212
1561
|
(deps: SqliteDep) =>
|
|
1213
1562
|
(
|
|
1214
|
-
ownerId:
|
|
1563
|
+
ownerId: OwnerIdBytes,
|
|
1215
1564
|
index: NonNegativeInt,
|
|
1216
|
-
): Result<
|
|
1565
|
+
): Result<TimestampBytes, SqliteError> => {
|
|
1217
1566
|
const result = deps.sqlite.exec<{
|
|
1218
|
-
readonly pt:
|
|
1567
|
+
readonly pt: TimestampBytes;
|
|
1219
1568
|
}>(sql.prepared`
|
|
1220
1569
|
with
|
|
1221
1570
|
fi(b, cl, ic, pt, mt, nt, nc) as (
|
|
@@ -1291,3 +1640,76 @@ export const getTimestampByIndex =
|
|
|
1291
1640
|
if (!result.ok) return result;
|
|
1292
1641
|
return ok(result.value.rows[0].pt);
|
|
1293
1642
|
};
|
|
1643
|
+
|
|
1644
|
+
/** Retrieves usage information for an owner from the evolu_usage table. */
|
|
1645
|
+
export const getOwnerUsage =
|
|
1646
|
+
(deps: SqliteDep) =>
|
|
1647
|
+
(
|
|
1648
|
+
ownerIdBytes: OwnerIdBytes,
|
|
1649
|
+
initialTimestamp: TimestampBytes,
|
|
1650
|
+
): Result<
|
|
1651
|
+
{
|
|
1652
|
+
storedBytes: NonNegativeInt | null;
|
|
1653
|
+
firstTimestamp: TimestampBytes;
|
|
1654
|
+
lastTimestamp: TimestampBytes;
|
|
1655
|
+
},
|
|
1656
|
+
SqliteError
|
|
1657
|
+
> => {
|
|
1658
|
+
const result = deps.sqlite.exec<{
|
|
1659
|
+
storedBytes: NonNegativeInt;
|
|
1660
|
+
firstTimestamp: TimestampBytes | null;
|
|
1661
|
+
lastTimestamp: TimestampBytes | null;
|
|
1662
|
+
}>(sql`
|
|
1663
|
+
select storedBytes, firstTimestamp, lastTimestamp
|
|
1664
|
+
from evolu_usage
|
|
1665
|
+
where ownerId = ${ownerIdBytes};
|
|
1666
|
+
`);
|
|
1667
|
+
if (!result.ok) return result;
|
|
1668
|
+
|
|
1669
|
+
if (!isNonEmptyReadonlyArray(result.value.rows)) {
|
|
1670
|
+
return ok({
|
|
1671
|
+
storedBytes: null,
|
|
1672
|
+
firstTimestamp: initialTimestamp,
|
|
1673
|
+
lastTimestamp: initialTimestamp,
|
|
1674
|
+
});
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
const row = firstInArray(result.value.rows);
|
|
1678
|
+
assert(row.firstTimestamp, "not null");
|
|
1679
|
+
assert(row.lastTimestamp, "not null");
|
|
1680
|
+
|
|
1681
|
+
return ok({
|
|
1682
|
+
storedBytes: row.storedBytes,
|
|
1683
|
+
firstTimestamp: row.firstTimestamp,
|
|
1684
|
+
lastTimestamp: row.lastTimestamp,
|
|
1685
|
+
});
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1688
|
+
/**
|
|
1689
|
+
* Updates timestamp bounds in evolu_usage table.
|
|
1690
|
+
*
|
|
1691
|
+
* Used by both relay and client to maintain firstTimestamp/lastTimestamp after
|
|
1692
|
+
* processing messages.
|
|
1693
|
+
*/
|
|
1694
|
+
export const updateOwnerUsage =
|
|
1695
|
+
(deps: SqliteDep) =>
|
|
1696
|
+
(
|
|
1697
|
+
ownerIdBytes: OwnerIdBytes,
|
|
1698
|
+
storedBytes: PositiveInt,
|
|
1699
|
+
firstTimestamp: TimestampBytes,
|
|
1700
|
+
lastTimestamp: TimestampBytes,
|
|
1701
|
+
): Result<void, SqliteError> => {
|
|
1702
|
+
const result = deps.sqlite.exec(sql`
|
|
1703
|
+
insert into evolu_usage
|
|
1704
|
+
("ownerId", "storedBytes", "firstTimestamp", "lastTimestamp")
|
|
1705
|
+
values
|
|
1706
|
+
(${ownerIdBytes}, ${storedBytes}, ${firstTimestamp}, ${lastTimestamp})
|
|
1707
|
+
on conflict (ownerId) do update
|
|
1708
|
+
set
|
|
1709
|
+
storedBytes = ${storedBytes},
|
|
1710
|
+
firstTimestamp = ${firstTimestamp},
|
|
1711
|
+
lastTimestamp = ${lastTimestamp};
|
|
1712
|
+
`);
|
|
1713
|
+
if (!result.ok) return result;
|
|
1714
|
+
return ok();
|
|
1715
|
+
};
|