@evolu/common 6.0.1-preview.3 → 6.0.1-preview.31
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/Array.d.ts +69 -5
- package/dist/src/Array.d.ts.map +1 -1
- package/dist/src/Array.js +64 -5
- package/dist/src/Assert.d.ts +6 -16
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +6 -18
- 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 +44 -0
- package/dist/src/Cache.d.ts.map +1 -0
- package/dist/src/Cache.js +52 -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 +31 -6
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +72 -9
- package/dist/src/Crypto.d.ts +61 -34
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +32 -45
- package/dist/src/Evolu/Db.d.ts +161 -65
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +286 -694
- 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 +208 -133
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +188 -183
- package/dist/src/Evolu/Internal.d.ts +0 -2
- package/dist/src/Evolu/Internal.d.ts.map +1 -1
- package/dist/src/Evolu/Internal.js +0 -2
- 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 +277 -232
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +603 -378
- package/dist/src/Evolu/Public.d.ts +6 -8
- package/dist/src/Evolu/Public.d.ts.map +1 -1
- package/dist/src/Evolu/Public.js +2 -3
- package/dist/src/Evolu/PublicKysely.js +3 -3
- package/dist/src/Evolu/Query.d.ts +2 -1
- package/dist/src/Evolu/Query.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.d.ts +92 -7
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +238 -76
- package/dist/src/Evolu/Schema.d.ts +129 -73
- package/dist/src/Evolu/Schema.d.ts.map +1 -1
- package/dist/src/Evolu/Schema.js +169 -89
- package/dist/src/Evolu/Storage.d.ts +240 -26
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +189 -91
- package/dist/src/Evolu/Sync.d.ts +67 -13
- package/dist/src/Evolu/Sync.d.ts.map +1 -1
- package/dist/src/Evolu/Sync.js +441 -20
- package/dist/src/Evolu/Timestamp.d.ts +85 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +77 -18
- 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/ManyToManyMap.d.ts +71 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- 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/Platform.d.ts +20 -0
- package/dist/src/Platform.d.ts.map +1 -0
- package/dist/src/Platform.js +22 -0
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- 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 +184 -52
- 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 +63 -5
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +110 -9
- package/dist/src/Task.d.ts +586 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +469 -0
- package/dist/src/Time.d.ts +66 -1
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +99 -5
- package/dist/src/Type.d.ts +622 -340
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +666 -464
- package/dist/src/Types.d.ts +1 -75
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/WebSocket.d.ts +5 -2
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +12 -18
- package/dist/src/Worker.d.ts +39 -11
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +22 -4
- package/dist/src/index.d.ts +7 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -2
- package/package.json +14 -13
- package/src/Array.ts +90 -11
- package/src/Assert.ts +6 -24
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +7 -7
- package/src/Cache.ts +85 -0
- package/src/Callbacks.ts +62 -22
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +97 -82
- package/src/Evolu/Db.ts +517 -1020
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +464 -355
- package/src/Evolu/Internal.ts +0 -2
- 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 +859 -676
- package/src/Evolu/Public.ts +7 -14
- package/src/Evolu/PublicKysely.ts +3 -3
- package/src/Evolu/Query.ts +2 -1
- package/src/Evolu/Relay.ts +437 -93
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +532 -135
- package/src/Evolu/Sync.ts +766 -37
- package/src/Evolu/Timestamp.ts +88 -35
- package/src/Identicon.ts +197 -0
- package/src/Instances.ts +90 -0
- package/src/ManyToManyMap.ts +124 -24
- package/src/Number.ts +6 -10
- package/src/Platform.ts +26 -0
- package/src/Random.ts +3 -2
- package/src/Resources.ts +367 -0
- package/src/Result.ts +191 -54
- package/src/Skiplist.ts +1 -1
- package/src/Sqlite.ts +122 -17
- package/src/Task.ts +901 -0
- package/src/Time.ts +180 -5
- package/src/Type.ts +1084 -727
- package/src/Types.ts +1 -77
- package/src/WebSocket.ts +27 -25
- package/src/Worker.ts +72 -23
- package/src/index.ts +7 -2
- 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/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/NanoId.ts +0 -39
- package/src/Promise.ts +0 -295
package/src/Evolu/Storage.ts
CHANGED
|
@@ -1,10 +1,282 @@
|
|
|
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 { ok, Result } from "../Result.js";
|
|
13
|
+
import { sql, SqliteDep, SqliteError, SqliteValue } from "../Sqlite.js";
|
|
14
|
+
import { MaybeAsync } from "../Task.js";
|
|
15
|
+
import {
|
|
16
|
+
Id,
|
|
17
|
+
Int64String,
|
|
18
|
+
NonNegativeInt,
|
|
19
|
+
object,
|
|
20
|
+
PositiveInt,
|
|
21
|
+
record,
|
|
22
|
+
String,
|
|
23
|
+
} from "../Type.js";
|
|
24
|
+
import {
|
|
25
|
+
BaseOwnerError,
|
|
26
|
+
Owner,
|
|
27
|
+
OwnerId,
|
|
28
|
+
OwnerIdBytes,
|
|
29
|
+
OwnerWriteKey,
|
|
30
|
+
} from "./Owner.js";
|
|
31
|
+
import { orderTimestampBytes, Timestamp, TimestampBytes } from "./Timestamp.js";
|
|
32
|
+
|
|
33
|
+
export interface StorageConfig {
|
|
34
|
+
/**
|
|
35
|
+
* Callback called before an attempt to write, to check if an {@link OwnerId}
|
|
36
|
+
* has sufficient quota for the write.
|
|
37
|
+
*
|
|
38
|
+
* The callback receives the {@link OwnerId} and the total bytes that would be
|
|
39
|
+
* stored after the write (current stored bytes plus incoming bytes), and
|
|
40
|
+
* returns a {@link MaybeAsync} boolean: `true` to allow the write, or `false`
|
|
41
|
+
* to deny it due to quota limits.
|
|
42
|
+
*
|
|
43
|
+
* The callback can be synchronous (for SQLite or in-memory checks) or
|
|
44
|
+
* asynchronous (for calling remote APIs).
|
|
45
|
+
*
|
|
46
|
+
* The callback returns a boolean rather than an error because error handling
|
|
47
|
+
* and logging are the responsibility of the callback implementation.
|
|
48
|
+
*
|
|
49
|
+
* ### Example
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* // Client
|
|
53
|
+
* // evolu.subscribeError
|
|
54
|
+
*
|
|
55
|
+
* // Relay
|
|
56
|
+
* isOwnerWithinQuota: (ownerId, requiredBytes) => {
|
|
57
|
+
* console.log(ownerId, requiredBytes);
|
|
58
|
+
* // Check error via evolu.subscribeError
|
|
59
|
+
* return true;
|
|
60
|
+
* };
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
readonly isOwnerWithinQuota: (
|
|
64
|
+
ownerId: OwnerId,
|
|
65
|
+
requiredBytes: PositiveInt,
|
|
66
|
+
) => MaybeAsync<boolean>;
|
|
67
|
+
}
|
|
68
|
+
|
|
1
69
|
/**
|
|
2
|
-
* Evolu Storage
|
|
70
|
+
* Evolu Storage
|
|
71
|
+
*
|
|
72
|
+
* Evolu protocol using Storage is agnostic to storage implementation
|
|
73
|
+
* details—any storage can be plugged in, as long as it implements this
|
|
74
|
+
* interface. Implementations must handle their own errors; return values only
|
|
75
|
+
* indicate overall success or failure.
|
|
76
|
+
*
|
|
77
|
+
* The Storage API is synchronous because SQLite's synchronous API is the
|
|
78
|
+
* fastest way to use SQLite. Synchronous bindings (like better-sqlite3) call
|
|
79
|
+
* SQLite's C API directly with no context switching between the event loop and
|
|
80
|
+
* native code, and no promise microtasks or await overhead.
|
|
81
|
+
*
|
|
82
|
+
* The only exception is {@link Storage#writeMessages}, which is async to allow
|
|
83
|
+
* for async validation logic before writing to storage. The write operation
|
|
84
|
+
* itself remains synchronous.
|
|
85
|
+
*/
|
|
86
|
+
export interface Storage {
|
|
87
|
+
readonly getSize: (ownerId: OwnerIdBytes) => NonNegativeInt | null;
|
|
88
|
+
|
|
89
|
+
readonly fingerprint: (
|
|
90
|
+
ownerId: OwnerIdBytes,
|
|
91
|
+
begin: NonNegativeInt,
|
|
92
|
+
end: NonNegativeInt,
|
|
93
|
+
) => Fingerprint | null;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Computes fingerprints with their upper bounds in one call.
|
|
97
|
+
*
|
|
98
|
+
* This function can be replaced with many fingerprint/findLowerBound calls,
|
|
99
|
+
* but implementations can leverage it for batching and more efficient
|
|
100
|
+
* fingerprint computation.
|
|
101
|
+
*/
|
|
102
|
+
readonly fingerprintRanges: (
|
|
103
|
+
ownerId: OwnerIdBytes,
|
|
104
|
+
buckets: ReadonlyArray<NonNegativeInt>,
|
|
105
|
+
upperBound?: RangeUpperBound,
|
|
106
|
+
) => ReadonlyArray<FingerprintRange> | null;
|
|
107
|
+
|
|
108
|
+
readonly findLowerBound: (
|
|
109
|
+
ownerId: OwnerIdBytes,
|
|
110
|
+
begin: NonNegativeInt,
|
|
111
|
+
end: NonNegativeInt,
|
|
112
|
+
upperBound: RangeUpperBound,
|
|
113
|
+
) => NonNegativeInt | null;
|
|
114
|
+
|
|
115
|
+
readonly iterate: (
|
|
116
|
+
ownerId: OwnerIdBytes,
|
|
117
|
+
begin: NonNegativeInt,
|
|
118
|
+
end: NonNegativeInt,
|
|
119
|
+
callback: (timestamp: TimestampBytes, index: NonNegativeInt) => boolean,
|
|
120
|
+
) => void;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validates the {@link OwnerWriteKey} for the given {@link Owner}.
|
|
124
|
+
*
|
|
125
|
+
* Returns `true` if the write key is valid, `false` otherwise.
|
|
126
|
+
*/
|
|
127
|
+
readonly validateWriteKey: (
|
|
128
|
+
ownerId: OwnerIdBytes,
|
|
129
|
+
writeKey: OwnerWriteKey,
|
|
130
|
+
) => boolean;
|
|
131
|
+
|
|
132
|
+
/** Sets the {@link OwnerWriteKey} for the given {@link Owner}. */
|
|
133
|
+
readonly setWriteKey: (
|
|
134
|
+
ownerId: OwnerIdBytes,
|
|
135
|
+
writeKey: OwnerWriteKey,
|
|
136
|
+
) => boolean;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Write encrypted {@link CrdtMessage}s to storage.
|
|
140
|
+
*
|
|
141
|
+
* Must use a mutex per ownerId to ensure sequential processing and proper
|
|
142
|
+
* protocol logic handling during sync operations.
|
|
143
|
+
*
|
|
144
|
+
* TODO: Use MaybeAsync
|
|
145
|
+
*/
|
|
146
|
+
readonly writeMessages: (
|
|
147
|
+
ownerIdBytes: OwnerIdBytes,
|
|
148
|
+
messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>,
|
|
149
|
+
) => MaybeAsync<Result<void, StorageWriteError | StorageQuotaError>>;
|
|
150
|
+
|
|
151
|
+
/** Read encrypted {@link DbChange}s from storage. */
|
|
152
|
+
readonly readDbChange: (
|
|
153
|
+
ownerId: OwnerIdBytes,
|
|
154
|
+
timestamp: TimestampBytes,
|
|
155
|
+
) => EncryptedDbChange | null;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Delete all data for the given {@link Owner}.
|
|
159
|
+
*
|
|
160
|
+
* Returns `true` on success, `false` on failure.
|
|
161
|
+
*/
|
|
162
|
+
readonly deleteOwner: (ownerId: OwnerIdBytes) => boolean;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface StorageDep {
|
|
166
|
+
readonly storage: Storage;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Error indicating a serious write failure. */
|
|
170
|
+
export interface StorageWriteError extends BaseOwnerError {
|
|
171
|
+
readonly type: "StorageWriteError";
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Error when storage or billing quota is exceeded. */
|
|
175
|
+
export interface StorageQuotaError extends BaseOwnerError {
|
|
176
|
+
readonly type: "StorageQuotaError";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* A cryptographic hash used for efficiently comparing collections of
|
|
181
|
+
* {@link TimestampBytes}s.
|
|
182
|
+
*
|
|
183
|
+
* It consists of the first {@link fingerprintSize} bytes of the SHA-256 hash of
|
|
184
|
+
* one or more timestamps.
|
|
185
|
+
*/
|
|
186
|
+
export type Fingerprint = Uint8Array & Brand<"Fingerprint">;
|
|
187
|
+
|
|
188
|
+
export const fingerprintSize = NonNegativeInt.orThrow(12);
|
|
189
|
+
|
|
190
|
+
/** A fingerprint of an empty range. */
|
|
191
|
+
export const zeroFingerprint = new Uint8Array(fingerprintSize) as Fingerprint;
|
|
192
|
+
|
|
193
|
+
export interface BaseRange {
|
|
194
|
+
readonly upperBound: RangeUpperBound;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Union type for Range's upperBound: either a {@link TimestampBytes} or
|
|
199
|
+
* {@link InfiniteUpperBound}.
|
|
200
|
+
*/
|
|
201
|
+
export type RangeUpperBound = TimestampBytes | InfiniteUpperBound;
|
|
202
|
+
|
|
203
|
+
export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
|
|
204
|
+
export type InfiniteUpperBound = typeof InfiniteUpperBound;
|
|
205
|
+
|
|
206
|
+
export const RangeType = {
|
|
207
|
+
Fingerprint: 1,
|
|
208
|
+
Skip: 0,
|
|
209
|
+
Timestamps: 2,
|
|
210
|
+
} as const;
|
|
211
|
+
|
|
212
|
+
export type RangeType = (typeof RangeType)[keyof typeof RangeType];
|
|
213
|
+
|
|
214
|
+
export interface SkipRange extends BaseRange {
|
|
215
|
+
readonly type: typeof RangeType.Skip;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export interface FingerprintRange extends BaseRange {
|
|
219
|
+
readonly type: typeof RangeType.Fingerprint;
|
|
220
|
+
readonly fingerprint: Fingerprint;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface TimestampsRange extends BaseRange {
|
|
224
|
+
readonly type: typeof RangeType.Timestamps;
|
|
225
|
+
readonly timestamps: ReadonlyArray<TimestampBytes>;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export type Range = SkipRange | FingerprintRange | TimestampsRange;
|
|
229
|
+
|
|
230
|
+
/** An encrypted {@link CrdtMessage}. */
|
|
231
|
+
export interface EncryptedCrdtMessage {
|
|
232
|
+
readonly timestamp: Timestamp;
|
|
233
|
+
readonly change: EncryptedDbChange;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Encrypted DbChange */
|
|
237
|
+
export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* A CRDT message combining a unique {@link Timestamp} with a {@link DbChange}.
|
|
241
|
+
*
|
|
242
|
+
* Used in Evolu's sync protocol to replicate data changes across devices. Evolu
|
|
243
|
+
* operates as a durable queue, providing exactly-once delivery guarantees for
|
|
244
|
+
* reliable synchronization across application restarts and network failures.
|
|
245
|
+
*/
|
|
246
|
+
export interface CrdtMessage {
|
|
247
|
+
readonly timestamp: Timestamp;
|
|
248
|
+
readonly change: DbChange;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* A DbChange is a change to a table row. Together with a unique
|
|
253
|
+
* {@link Timestamp}, it forms a {@link CrdtMessage}.
|
|
254
|
+
*/
|
|
255
|
+
export const DbChange = object({
|
|
256
|
+
table: String,
|
|
257
|
+
id: Id,
|
|
258
|
+
values: record(String, SqliteValue),
|
|
259
|
+
});
|
|
260
|
+
export type DbChange = typeof DbChange.Type;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Common interface for both client and relay SQLite storages.
|
|
3
264
|
*
|
|
4
265
|
* Evolu uses a Skiplist, which leverages SQLite indexes. The core logic is
|
|
5
266
|
* implemented in SQL, so it doesn't have to make roundtrips to the DB.
|
|
6
267
|
*
|
|
7
|
-
*
|
|
268
|
+
* While the SQL implementation may look sophisticated, it's conceptually simple
|
|
269
|
+
* and LLMs can explain how it works. The Skiplist data structure is well
|
|
270
|
+
* explained in [this Stack Overflow
|
|
271
|
+
* answer](https://stackoverflow.com/questions/61944198/what-is-a-zip-tree-and-how-does-it-work).
|
|
272
|
+
* The logic resembles [Negentropy's C++
|
|
273
|
+
* storage](https://github.com/hoytech/negentropy), except we use a Skiplist to
|
|
274
|
+
* leverage SQLite indexes, which makes the code simpler.
|
|
275
|
+
*
|
|
276
|
+
* Note: A paid review by the SQLite team is planned, as they use the same
|
|
277
|
+
* algorithm for their rsync tool.
|
|
278
|
+
*
|
|
279
|
+
* The ideal storage for a Relay should use an architecture like
|
|
8
280
|
* [strfry](https://github.com/hoytech/strfry) (a KV storage), but with Skiplist
|
|
9
281
|
* to ensure that insertion order doesn't matter (local-first apps can often
|
|
10
282
|
* write in the past.)
|
|
@@ -19,116 +291,104 @@
|
|
|
19
291
|
* each other, if necessary. One relay should handle hundreds of thousands of
|
|
20
292
|
* users, and when it goes down, nothing happens, because it will be
|
|
21
293
|
* synchronized later.
|
|
22
|
-
*
|
|
23
|
-
* @module
|
|
24
294
|
*/
|
|
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 {
|
|
295
|
+
export interface BaseSqliteStorage
|
|
296
|
+
extends Pick<
|
|
297
|
+
Storage,
|
|
298
|
+
| "getSize"
|
|
299
|
+
| "fingerprint"
|
|
300
|
+
| "fingerprintRanges"
|
|
301
|
+
| "findLowerBound"
|
|
302
|
+
| "iterate"
|
|
303
|
+
| "deleteOwner"
|
|
304
|
+
> {
|
|
305
|
+
/** Inserts a timestamp for an owner into the skiplist-based storage. */
|
|
50
306
|
readonly insertTimestamp: (
|
|
51
|
-
ownerId:
|
|
52
|
-
timestamp:
|
|
307
|
+
ownerId: OwnerIdBytes,
|
|
308
|
+
timestamp: TimestampBytes,
|
|
309
|
+
strategy: StorageInsertTimestampStrategy,
|
|
53
310
|
) => Result<void, SqliteError>;
|
|
54
311
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
readonly
|
|
312
|
+
/**
|
|
313
|
+
* Efficiently checks which timestamps already exist in the database using a
|
|
314
|
+
* single CTE query instead of N individual queries.
|
|
315
|
+
*/
|
|
316
|
+
readonly getExistingTimestamps: (
|
|
317
|
+
ownerIdBytes: OwnerIdBytes,
|
|
318
|
+
timestampsBytes: NonEmptyReadonlyArray<TimestampBytes>,
|
|
319
|
+
) => Result<ReadonlyArray<TimestampBytes>, SqliteError>;
|
|
60
320
|
}
|
|
61
321
|
|
|
62
|
-
export interface
|
|
63
|
-
readonly storage:
|
|
322
|
+
export interface BaseSqliteStorageDep {
|
|
323
|
+
readonly storage: BaseSqliteStorage;
|
|
64
324
|
}
|
|
65
325
|
|
|
66
|
-
export type SqliteStorageDeps =
|
|
326
|
+
export type SqliteStorageDeps = RandomDep & SqliteDep;
|
|
67
327
|
|
|
68
|
-
export interface
|
|
328
|
+
export interface CreateBaseSqliteStorageConfig extends StorageConfig {
|
|
69
329
|
onStorageError: (error: SqliteError) => void;
|
|
70
330
|
}
|
|
71
331
|
|
|
72
|
-
|
|
332
|
+
/**
|
|
333
|
+
* Creates a {@link BaseSqliteStorage} implementation.
|
|
334
|
+
*
|
|
335
|
+
* # Stateless Design
|
|
336
|
+
*
|
|
337
|
+
* This implementation is fully stateless - it requires no in-memory state
|
|
338
|
+
* between invocations. All necessary metadata (timestamp bounds for insertion
|
|
339
|
+
* strategy optimization) is persisted in the evolu_usage table. This makes
|
|
340
|
+
* Evolu Relay suitable for stateless serverless environments like AWS Lambda,
|
|
341
|
+
* Cloudflare Workers with Durable Objects, and other platforms where memory
|
|
342
|
+
* doesn't persist between requests. While not extensively tested in all these
|
|
343
|
+
* environments yet, the stateless design should work well across them.
|
|
344
|
+
*/
|
|
345
|
+
export const createBaseSqliteStorage =
|
|
73
346
|
(deps: SqliteStorageDeps) =>
|
|
74
|
-
(
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
OwnerId,
|
|
82
|
-
{
|
|
83
|
-
minT: BinaryTimestamp;
|
|
84
|
-
maxT: BinaryTimestamp;
|
|
85
|
-
}
|
|
86
|
-
>();
|
|
87
|
-
|
|
88
|
-
return ok({
|
|
89
|
-
insertTimestamp: (ownerId: BinaryOwnerId, timestamp: BinaryTimestamp) => {
|
|
90
|
-
const ownerIdString = binaryOwnerIdToOwnerId(ownerId);
|
|
347
|
+
(config: CreateBaseSqliteStorageConfig): BaseSqliteStorage => {
|
|
348
|
+
return {
|
|
349
|
+
insertTimestamp: (
|
|
350
|
+
ownerId: OwnerIdBytes,
|
|
351
|
+
timestamp: TimestampBytes,
|
|
352
|
+
strategy: StorageInsertTimestampStrategy,
|
|
353
|
+
) => {
|
|
91
354
|
const level = randomSkiplistLevel(deps);
|
|
355
|
+
return insertTimestamp(deps)(ownerId, timestamp, level, strategy);
|
|
356
|
+
},
|
|
92
357
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
358
|
+
getExistingTimestamps: (ownerIdBytes, timestampsBytes) => {
|
|
359
|
+
const concatenatedTimestamps = concatBytes(...timestampsBytes);
|
|
360
|
+
|
|
361
|
+
const result = deps.sqlite.exec<{
|
|
362
|
+
timestampBytes: TimestampBytes;
|
|
363
|
+
}>(sql`
|
|
364
|
+
with recursive
|
|
365
|
+
split_timestamps(timestampBytes, pos) as (
|
|
366
|
+
select
|
|
367
|
+
substr(${concatenatedTimestamps}, 1, 16),
|
|
368
|
+
17 as pos
|
|
369
|
+
union all
|
|
370
|
+
select
|
|
371
|
+
substr(${concatenatedTimestamps}, pos, 16),
|
|
372
|
+
pos + 16
|
|
373
|
+
from split_timestamps
|
|
374
|
+
where pos <= length(${concatenatedTimestamps})
|
|
375
|
+
)
|
|
376
|
+
select s.timestampBytes
|
|
377
|
+
from
|
|
378
|
+
split_timestamps s
|
|
379
|
+
join evolu_timestamp t
|
|
380
|
+
on t.ownerId = ${ownerIdBytes} and s.timestampBytes = t.t;
|
|
381
|
+
`);
|
|
114
382
|
|
|
115
|
-
if (
|
|
116
|
-
strategy = "append";
|
|
117
|
-
stats.maxT = timestamp;
|
|
118
|
-
} else if (orderBinaryTimestamp(timestamp, stats.minT) === -1) {
|
|
119
|
-
strategy = "prepend";
|
|
120
|
-
stats.minT = timestamp;
|
|
121
|
-
} else {
|
|
122
|
-
strategy = "insert";
|
|
123
|
-
}
|
|
383
|
+
if (!result.ok) return result;
|
|
124
384
|
|
|
125
|
-
return
|
|
385
|
+
return ok(result.value.rows.map((row) => row.timestampBytes));
|
|
126
386
|
},
|
|
127
387
|
|
|
128
388
|
getSize: (ownerId) => {
|
|
129
389
|
const size = getSize(deps)(ownerId);
|
|
130
390
|
if (!size.ok) {
|
|
131
|
-
|
|
391
|
+
config.onStorageError(size.error);
|
|
132
392
|
return null;
|
|
133
393
|
}
|
|
134
394
|
return size.value;
|
|
@@ -138,7 +398,7 @@ export const createSqliteStorageBase =
|
|
|
138
398
|
assertBeginEnd(begin, end);
|
|
139
399
|
const result = fingerprint(deps)(ownerId, begin, end);
|
|
140
400
|
if (!result.ok) {
|
|
141
|
-
|
|
401
|
+
config.onStorageError(result.error);
|
|
142
402
|
return null;
|
|
143
403
|
}
|
|
144
404
|
return result.value;
|
|
@@ -147,7 +407,7 @@ export const createSqliteStorageBase =
|
|
|
147
407
|
fingerprintRanges: (ownerId, buckets, upperBound) => {
|
|
148
408
|
const ranges = fingerprintRanges(deps)(ownerId, buckets, upperBound);
|
|
149
409
|
if (!ranges.ok) {
|
|
150
|
-
|
|
410
|
+
config.onStorageError(ranges.error);
|
|
151
411
|
return null;
|
|
152
412
|
}
|
|
153
413
|
return ranges.value;
|
|
@@ -161,7 +421,7 @@ export const createSqliteStorageBase =
|
|
|
161
421
|
upperBound,
|
|
162
422
|
);
|
|
163
423
|
if (!lowerBound.ok) {
|
|
164
|
-
|
|
424
|
+
config.onStorageError(lowerBound.error);
|
|
165
425
|
return null;
|
|
166
426
|
}
|
|
167
427
|
return lowerBound.value;
|
|
@@ -175,7 +435,7 @@ export const createSqliteStorageBase =
|
|
|
175
435
|
// This is much faster than SQL limit with offset.
|
|
176
436
|
const first = getTimestampByIndex(deps)(ownerId, begin);
|
|
177
437
|
if (!first.ok) {
|
|
178
|
-
|
|
438
|
+
config.onStorageError(first.error);
|
|
179
439
|
return;
|
|
180
440
|
}
|
|
181
441
|
|
|
@@ -192,7 +452,7 @@ export const createSqliteStorageBase =
|
|
|
192
452
|
* implementing chunking, be sure to run performance tests (including
|
|
193
453
|
* fetching one by one).
|
|
194
454
|
*/
|
|
195
|
-
const result = deps.sqlite.exec<{ t:
|
|
455
|
+
const result = deps.sqlite.exec<{ t: TimestampBytes }>(sql`
|
|
196
456
|
select t
|
|
197
457
|
from evolu_timestamp
|
|
198
458
|
where ownerId = ${ownerId} and t > ${first.value}
|
|
@@ -200,23 +460,36 @@ export const createSqliteStorageBase =
|
|
|
200
460
|
limit ${length - 1};
|
|
201
461
|
`);
|
|
202
462
|
if (!result.ok) {
|
|
203
|
-
|
|
463
|
+
config.onStorageError(result.error);
|
|
204
464
|
return;
|
|
205
465
|
}
|
|
206
466
|
|
|
207
467
|
for (let i = 0; i < result.value.rows.length; i++) {
|
|
208
|
-
const index = (begin + 1 + i)
|
|
468
|
+
const index = NonNegativeInt.orThrow(begin + 1 + i);
|
|
209
469
|
if (!callback(result.value.rows[i].t, index)) return;
|
|
210
470
|
}
|
|
211
471
|
},
|
|
212
|
-
|
|
472
|
+
|
|
473
|
+
deleteOwner: (ownerId) => {
|
|
474
|
+
const result = deps.sqlite.exec(sql`
|
|
475
|
+
delete from evolu_timestamp where ownerId = ${ownerId};
|
|
476
|
+
`);
|
|
477
|
+
if (!result.ok) {
|
|
478
|
+
config.onStorageError(result.error);
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
return true;
|
|
482
|
+
},
|
|
483
|
+
};
|
|
213
484
|
};
|
|
214
485
|
|
|
215
486
|
const assertBeginEnd = (begin: NonNegativeInt, end: NonNegativeInt) => {
|
|
216
487
|
assert(begin <= end, "invalid begin or end");
|
|
217
488
|
};
|
|
218
489
|
|
|
219
|
-
const
|
|
490
|
+
export const createBaseSqliteStorageTables = (
|
|
491
|
+
deps: SqliteDep,
|
|
492
|
+
): Result<void, SqliteError> => {
|
|
220
493
|
for (const query of [
|
|
221
494
|
/**
|
|
222
495
|
* Creates the `evolu_timestamp` table for storing timestamps of multiple
|
|
@@ -229,19 +502,13 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
229
502
|
*
|
|
230
503
|
* Columns:
|
|
231
504
|
*
|
|
232
|
-
* - `t` –
|
|
505
|
+
* - `t` – TimestampBytes
|
|
233
506
|
* - `h1`/`h2` – 12-byte fingerprint split into two integers for fast XOR
|
|
234
507
|
* - `c` – incremental count
|
|
235
|
-
* - `l` – Skiplist level (1 to
|
|
236
|
-
*
|
|
237
|
-
* For scaling or isolation, sharding is possible—each owner can have a
|
|
238
|
-
* separate SQLite database.
|
|
239
|
-
*
|
|
240
|
-
* Maybe we could use an integer surrogate key for ownerId, but it's fast
|
|
241
|
-
* enough even without it.
|
|
508
|
+
* - `l` – Skiplist level (1 to 10)
|
|
242
509
|
*/
|
|
243
510
|
sql`
|
|
244
|
-
create table
|
|
511
|
+
create table evolu_timestamp (
|
|
245
512
|
"ownerId" blob not null,
|
|
246
513
|
"t" blob not null,
|
|
247
514
|
"h1" integer,
|
|
@@ -254,7 +521,7 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
254
521
|
`,
|
|
255
522
|
|
|
256
523
|
sql`
|
|
257
|
-
create index
|
|
524
|
+
create index evolu_timestamp_index on evolu_timestamp (
|
|
258
525
|
"ownerId",
|
|
259
526
|
"l",
|
|
260
527
|
"t",
|
|
@@ -263,6 +530,26 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
263
530
|
"c"
|
|
264
531
|
);
|
|
265
532
|
`,
|
|
533
|
+
|
|
534
|
+
/**
|
|
535
|
+
* Creates the `evolu_usage` table for tracking data consumption per owner.
|
|
536
|
+
*
|
|
537
|
+
* Columns:
|
|
538
|
+
*
|
|
539
|
+
* - `ownerId` – OwnerIdBytes (primary key)
|
|
540
|
+
* - `storedBytes` – total bytes stored in database
|
|
541
|
+
* - `firstTimestamp` – for timestamp insertion strategies
|
|
542
|
+
* - `lastTimestamp` – for timestamp insertion strategies
|
|
543
|
+
*/
|
|
544
|
+
sql`
|
|
545
|
+
create table evolu_usage (
|
|
546
|
+
"ownerId" blob primary key,
|
|
547
|
+
"storedBytes" integer not null,
|
|
548
|
+
"firstTimestamp" blob,
|
|
549
|
+
"lastTimestamp" blob
|
|
550
|
+
)
|
|
551
|
+
strict;
|
|
552
|
+
`,
|
|
266
553
|
]) {
|
|
267
554
|
const result = deps.sqlite.exec(query);
|
|
268
555
|
if (!result.ok) return result;
|
|
@@ -270,26 +557,56 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
270
557
|
return ok();
|
|
271
558
|
};
|
|
272
559
|
|
|
273
|
-
type
|
|
560
|
+
export type StorageInsertTimestampStrategy = "append" | "prepend" | "insert";
|
|
274
561
|
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
562
|
+
/**
|
|
563
|
+
* Determines the insertion strategy for a timestamp based on its position
|
|
564
|
+
* relative to the current first and last timestamps.
|
|
565
|
+
*
|
|
566
|
+
* Returns a tuple with the strategy and updated timestamp bounds.
|
|
567
|
+
*/
|
|
568
|
+
export const getTimestampInsertStrategy = (
|
|
569
|
+
timestamp: TimestampBytes,
|
|
570
|
+
firstTimestamp: TimestampBytes,
|
|
571
|
+
lastTimestamp: TimestampBytes,
|
|
572
|
+
): [
|
|
573
|
+
strategy: StorageInsertTimestampStrategy,
|
|
574
|
+
firstTimestamp: TimestampBytes,
|
|
575
|
+
lastTimestamp: TimestampBytes,
|
|
576
|
+
] => {
|
|
577
|
+
if (orderTimestampBytes(timestamp, lastTimestamp) === 1) {
|
|
578
|
+
return ["append", firstTimestamp, timestamp];
|
|
579
|
+
}
|
|
580
|
+
if (orderTimestampBytes(timestamp, firstTimestamp) === -1) {
|
|
581
|
+
return ["prepend", timestamp, lastTimestamp];
|
|
582
|
+
}
|
|
583
|
+
return ["insert", firstTimestamp, lastTimestamp];
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* AFAIK, we can't do both insert and update in one query, and that's probably
|
|
588
|
+
* why append is 2x faster than insert. Prepend also has to update parents, but
|
|
589
|
+
* it's constantly fast. Insert degrades for reversed (yet LIMIT X magically
|
|
590
|
+
* fixes that) but it's OK for append.
|
|
591
|
+
*
|
|
592
|
+
* Note: SQL operations are idempotent (using `on conflict do nothing` and
|
|
593
|
+
* `changes() > 0`), but this is no longer required here since we use
|
|
594
|
+
* {@link BaseSqliteStorage.getExistingTimestamps} to filter out duplicates
|
|
595
|
+
* before insertion, which we need for quota checks anyway.
|
|
596
|
+
*
|
|
597
|
+
* TODO: Remove idempotency (`on conflict do nothing` and `changes() > 0`) since
|
|
598
|
+
* duplicates are now filtered before insertion.
|
|
599
|
+
*/
|
|
283
600
|
const insertTimestamp =
|
|
284
601
|
(deps: SqliteDep) =>
|
|
285
602
|
(
|
|
286
|
-
ownerId:
|
|
287
|
-
timestamp:
|
|
603
|
+
ownerId: OwnerIdBytes,
|
|
604
|
+
timestamp: TimestampBytes,
|
|
288
605
|
level: PositiveInt,
|
|
289
|
-
strategy:
|
|
606
|
+
strategy: StorageInsertTimestampStrategy,
|
|
290
607
|
): Result<void, SqliteError> => {
|
|
291
608
|
const [h1, h2] = fingerprintToSqliteFingerprint(
|
|
292
|
-
|
|
609
|
+
timestampBytesToFingerprint(timestamp),
|
|
293
610
|
);
|
|
294
611
|
|
|
295
612
|
let queries: Array<ReturnType<typeof sql.prepared>> = [];
|
|
@@ -807,6 +1124,13 @@ const insertTimestamp =
|
|
|
807
1124
|
return ok();
|
|
808
1125
|
};
|
|
809
1126
|
|
|
1127
|
+
export const timestampBytesToFingerprint = (
|
|
1128
|
+
timestamp: TimestampBytes,
|
|
1129
|
+
): Fingerprint => {
|
|
1130
|
+
const hash = sha256(timestamp).slice(0, fingerprintSize);
|
|
1131
|
+
return hash as Fingerprint;
|
|
1132
|
+
};
|
|
1133
|
+
|
|
810
1134
|
/**
|
|
811
1135
|
* Generates a random skiplist level in the range [1, skiplistMaxLevel].
|
|
812
1136
|
* Probabilistic approach avoids the need for explicit tree balancing.
|
|
@@ -819,7 +1143,7 @@ const randomSkiplistLevel = (deps: RandomDep): PositiveInt => {
|
|
|
819
1143
|
) {
|
|
820
1144
|
level += 1;
|
|
821
1145
|
}
|
|
822
|
-
return level
|
|
1146
|
+
return PositiveInt.orThrow(level);
|
|
823
1147
|
};
|
|
824
1148
|
|
|
825
1149
|
/**
|
|
@@ -887,7 +1211,7 @@ const sqliteFingerprintToFingerprint = ([
|
|
|
887
1211
|
|
|
888
1212
|
const getSize =
|
|
889
1213
|
(deps: SqliteDep) =>
|
|
890
|
-
(ownerId:
|
|
1214
|
+
(ownerId: OwnerIdBytes): Result<NonNegativeInt, SqliteError> => {
|
|
891
1215
|
const result = deps.sqlite.exec<{ size: NonNegativeInt }>(sql.prepared`
|
|
892
1216
|
with
|
|
893
1217
|
ml(ml) as (
|
|
@@ -930,7 +1254,7 @@ const getSize =
|
|
|
930
1254
|
const findLowerBound =
|
|
931
1255
|
(deps: SqliteDep) =>
|
|
932
1256
|
(
|
|
933
|
-
ownerId:
|
|
1257
|
+
ownerId: OwnerIdBytes,
|
|
934
1258
|
begin: NonNegativeInt,
|
|
935
1259
|
end: NonNegativeInt,
|
|
936
1260
|
upperBound: RangeUpperBound,
|
|
@@ -942,7 +1266,7 @@ const findLowerBound =
|
|
|
942
1266
|
}
|
|
943
1267
|
|
|
944
1268
|
const result = deps.sqlite.exec<{
|
|
945
|
-
t:
|
|
1269
|
+
t: TimestampBytes;
|
|
946
1270
|
}>(sql.prepared`
|
|
947
1271
|
select t
|
|
948
1272
|
from evolu_timestamp
|
|
@@ -960,14 +1284,14 @@ const findLowerBound =
|
|
|
960
1284
|
if (!count.ok) return count;
|
|
961
1285
|
|
|
962
1286
|
// `decrement` converts a count to an index.
|
|
963
|
-
return ok(decrement(count.value)
|
|
1287
|
+
return ok(NonNegativeInt.orThrow(decrement(count.value)));
|
|
964
1288
|
};
|
|
965
1289
|
|
|
966
1290
|
const getTimestampCount =
|
|
967
1291
|
(deps: SqliteDep) =>
|
|
968
1292
|
(
|
|
969
|
-
ownerId:
|
|
970
|
-
timestamp:
|
|
1293
|
+
ownerId: OwnerIdBytes,
|
|
1294
|
+
timestamp: TimestampBytes,
|
|
971
1295
|
): Result<PositiveInt, SqliteError> => {
|
|
972
1296
|
const result = deps.sqlite.exec<{
|
|
973
1297
|
count: PositiveInt;
|
|
@@ -1024,7 +1348,7 @@ const getTimestampCount =
|
|
|
1024
1348
|
const fingerprint =
|
|
1025
1349
|
(deps: SqliteDep) =>
|
|
1026
1350
|
(
|
|
1027
|
-
ownerId:
|
|
1351
|
+
ownerId: OwnerIdBytes,
|
|
1028
1352
|
begin: NonNegativeInt,
|
|
1029
1353
|
end: NonNegativeInt,
|
|
1030
1354
|
): Result<Fingerprint, SqliteError> => {
|
|
@@ -1056,14 +1380,14 @@ const fingerprint =
|
|
|
1056
1380
|
const fingerprintRanges =
|
|
1057
1381
|
(deps: SqliteDep) =>
|
|
1058
1382
|
(
|
|
1059
|
-
ownerId:
|
|
1383
|
+
ownerId: OwnerIdBytes,
|
|
1060
1384
|
buckets: ReadonlyArray<NonNegativeInt>,
|
|
1061
1385
|
upperBound: RangeUpperBound = InfiniteUpperBound,
|
|
1062
1386
|
): Result<ReadonlyArray<FingerprintRange>, SqliteError> => {
|
|
1063
1387
|
const bucketsJson = JSON.stringify(buckets);
|
|
1064
1388
|
|
|
1065
1389
|
const result = deps.sqlite.exec<{
|
|
1066
|
-
b:
|
|
1390
|
+
b: TimestampBytes | null;
|
|
1067
1391
|
h1: Int64String;
|
|
1068
1392
|
h2: Int64String;
|
|
1069
1393
|
}>(sql.prepared`
|
|
@@ -1199,11 +1523,11 @@ const x = (a: string, b: string) => sql.raw(`(${a} | ${b}) - (${a} & ${b})`);
|
|
|
1199
1523
|
export const getTimestampByIndex =
|
|
1200
1524
|
(deps: SqliteDep) =>
|
|
1201
1525
|
(
|
|
1202
|
-
ownerId:
|
|
1526
|
+
ownerId: OwnerIdBytes,
|
|
1203
1527
|
index: NonNegativeInt,
|
|
1204
|
-
): Result<
|
|
1528
|
+
): Result<TimestampBytes, SqliteError> => {
|
|
1205
1529
|
const result = deps.sqlite.exec<{
|
|
1206
|
-
readonly pt:
|
|
1530
|
+
readonly pt: TimestampBytes;
|
|
1207
1531
|
}>(sql.prepared`
|
|
1208
1532
|
with
|
|
1209
1533
|
fi(b, cl, ic, pt, mt, nt, nc) as (
|
|
@@ -1279,3 +1603,76 @@ export const getTimestampByIndex =
|
|
|
1279
1603
|
if (!result.ok) return result;
|
|
1280
1604
|
return ok(result.value.rows[0].pt);
|
|
1281
1605
|
};
|
|
1606
|
+
|
|
1607
|
+
/** Retrieves usage information for an owner from the evolu_usage table. */
|
|
1608
|
+
export const getOwnerUsage =
|
|
1609
|
+
(deps: SqliteDep) =>
|
|
1610
|
+
(
|
|
1611
|
+
ownerIdBytes: OwnerIdBytes,
|
|
1612
|
+
initialTimestamp: TimestampBytes,
|
|
1613
|
+
): Result<
|
|
1614
|
+
{
|
|
1615
|
+
storedBytes: NonNegativeInt | null;
|
|
1616
|
+
firstTimestamp: TimestampBytes;
|
|
1617
|
+
lastTimestamp: TimestampBytes;
|
|
1618
|
+
},
|
|
1619
|
+
SqliteError
|
|
1620
|
+
> => {
|
|
1621
|
+
const result = deps.sqlite.exec<{
|
|
1622
|
+
storedBytes: NonNegativeInt;
|
|
1623
|
+
firstTimestamp: TimestampBytes | null;
|
|
1624
|
+
lastTimestamp: TimestampBytes | null;
|
|
1625
|
+
}>(sql`
|
|
1626
|
+
select storedBytes, firstTimestamp, lastTimestamp
|
|
1627
|
+
from evolu_usage
|
|
1628
|
+
where ownerId = ${ownerIdBytes};
|
|
1629
|
+
`);
|
|
1630
|
+
if (!result.ok) return result;
|
|
1631
|
+
|
|
1632
|
+
if (!isNonEmptyReadonlyArray(result.value.rows)) {
|
|
1633
|
+
return ok({
|
|
1634
|
+
storedBytes: null,
|
|
1635
|
+
firstTimestamp: initialTimestamp,
|
|
1636
|
+
lastTimestamp: initialTimestamp,
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
const row = firstInArray(result.value.rows);
|
|
1641
|
+
assert(row.firstTimestamp, "not null");
|
|
1642
|
+
assert(row.lastTimestamp, "not null");
|
|
1643
|
+
|
|
1644
|
+
return ok({
|
|
1645
|
+
storedBytes: row.storedBytes,
|
|
1646
|
+
firstTimestamp: row.firstTimestamp,
|
|
1647
|
+
lastTimestamp: row.lastTimestamp,
|
|
1648
|
+
});
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* Updates timestamp bounds in evolu_usage table.
|
|
1653
|
+
*
|
|
1654
|
+
* Used by both relay and client to maintain firstTimestamp/lastTimestamp after
|
|
1655
|
+
* processing messages.
|
|
1656
|
+
*/
|
|
1657
|
+
export const updateOwnerUsage =
|
|
1658
|
+
(deps: SqliteDep) =>
|
|
1659
|
+
(
|
|
1660
|
+
ownerIdBytes: OwnerIdBytes,
|
|
1661
|
+
storedBytes: PositiveInt,
|
|
1662
|
+
firstTimestamp: TimestampBytes,
|
|
1663
|
+
lastTimestamp: TimestampBytes,
|
|
1664
|
+
): Result<void, SqliteError> => {
|
|
1665
|
+
const result = deps.sqlite.exec(sql`
|
|
1666
|
+
insert into evolu_usage
|
|
1667
|
+
("ownerId", "storedBytes", "firstTimestamp", "lastTimestamp")
|
|
1668
|
+
values
|
|
1669
|
+
(${ownerIdBytes}, ${storedBytes}, ${firstTimestamp}, ${lastTimestamp})
|
|
1670
|
+
on conflict (ownerId) do update
|
|
1671
|
+
set
|
|
1672
|
+
storedBytes = ${storedBytes},
|
|
1673
|
+
firstTimestamp = ${firstTimestamp},
|
|
1674
|
+
lastTimestamp = ${lastTimestamp};
|
|
1675
|
+
`);
|
|
1676
|
+
if (!result.ok) return result;
|
|
1677
|
+
return ok();
|
|
1678
|
+
};
|