@evolu/common 6.0.1-preview.2 → 6.0.1-preview.21
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/Assert.d.ts +6 -3
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +7 -4
- 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 +1 -1
- package/dist/src/CallbackRegistry.d.ts +53 -0
- package/dist/src/CallbackRegistry.d.ts.map +1 -0
- package/dist/src/CallbackRegistry.js +25 -0
- 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 +29 -42
- package/dist/src/Evolu/Db.d.ts +159 -66
- package/dist/src/Evolu/Db.d.ts.map +1 -1
- package/dist/src/Evolu/Db.js +300 -705
- 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 +200 -115
- package/dist/src/Evolu/Evolu.d.ts.map +1 -1
- package/dist/src/Evolu/Evolu.js +287 -157
- 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 +144 -0
- package/dist/src/Evolu/LocalAuth.d.ts.map +1 -0
- package/dist/src/Evolu/LocalAuth.js +171 -0
- package/dist/src/Evolu/Owner.d.ts +209 -124
- package/dist/src/Evolu/Owner.d.ts.map +1 -1
- package/dist/src/Evolu/Owner.js +118 -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 +256 -211
- package/dist/src/Evolu/Protocol.d.ts.map +1 -1
- package/dist/src/Evolu/Protocol.js +558 -359
- 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 +72 -4
- package/dist/src/Evolu/Relay.d.ts.map +1 -1
- package/dist/src/Evolu/Relay.js +124 -13
- 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 +159 -14
- package/dist/src/Evolu/Storage.d.ts.map +1 -1
- package/dist/src/Evolu/Storage.js +42 -32
- 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 +414 -20
- package/dist/src/Evolu/Timestamp.d.ts +35 -27
- package/dist/src/Evolu/Timestamp.d.ts.map +1 -1
- package/dist/src/Evolu/Timestamp.js +27 -18
- package/dist/src/ManyToManyMap.d.ts +74 -10
- package/dist/src/ManyToManyMap.d.ts.map +1 -1
- package/dist/src/ManyToManyMap.js +41 -6
- package/dist/src/Number.d.ts +2 -1
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Random.d.ts +3 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/RefCountedResourceManager.d.ts +119 -0
- package/dist/src/RefCountedResourceManager.d.ts.map +1 -0
- package/dist/src/RefCountedResourceManager.js +197 -0
- package/dist/src/Result.d.ts +176 -51
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +30 -241
- package/dist/src/Sqlite.d.ts +21 -4
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +50 -8
- package/dist/src/Task.d.ts +511 -0
- package/dist/src/Task.d.ts.map +1 -0
- package/dist/src/Task.js +410 -0
- package/dist/src/Time.d.ts +59 -0
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +87 -4
- package/dist/src/Type.d.ts +474 -327
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +502 -436
- package/dist/src/Types.d.ts +0 -74
- 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 -13
- 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 +3 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -3
- package/package.json +14 -13
- package/src/Assert.ts +8 -7
- package/src/Brand.ts +75 -0
- package/src/Buffer.ts +1 -1
- package/src/CallbackRegistry.ts +84 -0
- package/src/Console.ts +91 -11
- package/src/Crypto.ts +94 -79
- package/src/Evolu/Db.ts +528 -1027
- package/src/Evolu/Diff.ts +7 -5
- package/src/Evolu/Evolu.ts +682 -329
- package/src/Evolu/Internal.ts +0 -2
- package/src/Evolu/LocalAuth.ts +422 -0
- package/src/Evolu/Owner.ts +282 -227
- package/src/Evolu/Platform.ts +9 -9
- package/src/Evolu/Protocol.ts +788 -640
- 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 +238 -18
- package/src/Evolu/Schema.ts +391 -191
- package/src/Evolu/Storage.ts +275 -55
- package/src/Evolu/Sync.ts +708 -37
- package/src/Evolu/Timestamp.ts +38 -35
- package/src/ManyToManyMap.ts +127 -24
- package/src/Number.ts +2 -6
- package/src/Random.ts +3 -2
- package/src/RefCountedResourceManager.ts +368 -0
- package/src/Result.ts +183 -53
- package/src/Sqlite.ts +59 -23
- package/src/Task.ts +779 -0
- package/src/Time.ts +168 -4
- package/src/Type.ts +708 -660
- package/src/Types.ts +0 -76
- package/src/WebSocket.ts +23 -17
- package/src/Worker.ts +72 -23
- package/src/index.ts +3 -3
- package/dist/src/Callbacks.d.ts +0 -20
- package/dist/src/Callbacks.d.ts.map +0 -1
- package/dist/src/Callbacks.js +0 -18
- 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/Callbacks.ts +0 -43
- 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,230 @@
|
|
|
1
|
+
import { sha256 } from "@noble/hashes/sha2.js";
|
|
2
|
+
import { NonEmptyReadonlyArray } from "../Array.js";
|
|
3
|
+
import { assert } from "../Assert.js";
|
|
4
|
+
import { Brand } from "../Brand.js";
|
|
5
|
+
import { decrement } from "../Number.js";
|
|
6
|
+
import { RandomDep } from "../Random.js";
|
|
7
|
+
import { ok, Result } from "../Result.js";
|
|
8
|
+
import { sql, SqliteDep, SqliteError, SqliteValue } from "../Sqlite.js";
|
|
9
|
+
import {
|
|
10
|
+
Id,
|
|
11
|
+
Int64String,
|
|
12
|
+
NonNegativeInt,
|
|
13
|
+
object,
|
|
14
|
+
PositiveInt,
|
|
15
|
+
record,
|
|
16
|
+
String,
|
|
17
|
+
} from "../Type.js";
|
|
18
|
+
import {
|
|
19
|
+
Owner,
|
|
20
|
+
OwnerId,
|
|
21
|
+
OwnerIdBytes,
|
|
22
|
+
ownerIdBytesToOwnerId,
|
|
23
|
+
OwnerWriteKey,
|
|
24
|
+
} from "./Owner.js";
|
|
25
|
+
import { orderTimestampBytes, Timestamp, TimestampBytes } from "./Timestamp.js";
|
|
26
|
+
|
|
1
27
|
/**
|
|
2
|
-
* Evolu Storage
|
|
28
|
+
* Evolu Storage
|
|
29
|
+
*
|
|
30
|
+
* The protocol using Storage is agnostic to storage implementation details—any
|
|
31
|
+
* storage can be plugged in, as long as it implements this interface.
|
|
32
|
+
* Implementations must handle their own errors; return values only indicate
|
|
33
|
+
* overall success or failure.
|
|
34
|
+
*
|
|
35
|
+
* The Storage API is synchronous because SQLite's synchronous API is the
|
|
36
|
+
* fastest way to use SQLite. Synchronous bindings (like better-sqlite3) call
|
|
37
|
+
* SQLite's C API directly with no context switching between the event loop and
|
|
38
|
+
* native code, and no promise microtasks or await overhead.
|
|
39
|
+
*
|
|
40
|
+
* The only exception is {@link Storage#writeMessages}, which is async to allow
|
|
41
|
+
* for async validation logic before writing to storage. The write operation
|
|
42
|
+
* itself remains synchronous.
|
|
43
|
+
*/
|
|
44
|
+
export interface Storage {
|
|
45
|
+
readonly getSize: (ownerId: OwnerIdBytes) => NonNegativeInt | null;
|
|
46
|
+
|
|
47
|
+
readonly fingerprint: (
|
|
48
|
+
ownerId: OwnerIdBytes,
|
|
49
|
+
begin: NonNegativeInt,
|
|
50
|
+
end: NonNegativeInt,
|
|
51
|
+
) => Fingerprint | null;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Computes fingerprints with their upper bounds in one call.
|
|
55
|
+
*
|
|
56
|
+
* This function can be replaced with many fingerprint/findLowerBound calls,
|
|
57
|
+
* but implementations can leverage it for batching and more efficient
|
|
58
|
+
* fingerprint computation.
|
|
59
|
+
*/
|
|
60
|
+
readonly fingerprintRanges: (
|
|
61
|
+
ownerId: OwnerIdBytes,
|
|
62
|
+
buckets: ReadonlyArray<NonNegativeInt>,
|
|
63
|
+
upperBound?: RangeUpperBound,
|
|
64
|
+
) => ReadonlyArray<FingerprintRange> | null;
|
|
65
|
+
|
|
66
|
+
readonly findLowerBound: (
|
|
67
|
+
ownerId: OwnerIdBytes,
|
|
68
|
+
begin: NonNegativeInt,
|
|
69
|
+
end: NonNegativeInt,
|
|
70
|
+
upperBound: RangeUpperBound,
|
|
71
|
+
) => NonNegativeInt | null;
|
|
72
|
+
|
|
73
|
+
readonly iterate: (
|
|
74
|
+
ownerId: OwnerIdBytes,
|
|
75
|
+
begin: NonNegativeInt,
|
|
76
|
+
end: NonNegativeInt,
|
|
77
|
+
callback: (timestamp: TimestampBytes, index: NonNegativeInt) => boolean,
|
|
78
|
+
) => void;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validates the {@link OwnerWriteKey} for the given {@link Owner}.
|
|
82
|
+
*
|
|
83
|
+
* Returns `true` if the write key is valid, `false` otherwise.
|
|
84
|
+
*/
|
|
85
|
+
readonly validateWriteKey: (
|
|
86
|
+
ownerId: OwnerIdBytes,
|
|
87
|
+
writeKey: OwnerWriteKey,
|
|
88
|
+
) => boolean;
|
|
89
|
+
|
|
90
|
+
/** Sets the {@link OwnerWriteKey} for the given {@link Owner}. */
|
|
91
|
+
readonly setWriteKey: (
|
|
92
|
+
ownerId: OwnerIdBytes,
|
|
93
|
+
writeKey: OwnerWriteKey,
|
|
94
|
+
) => boolean;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Write encrypted {@link CrdtMessage}s to storage.
|
|
98
|
+
*
|
|
99
|
+
* Must use a mutex (per ownerId on Relay) to ensure sequential processing and
|
|
100
|
+
* proper protocol logic handling during sync operations.
|
|
101
|
+
*
|
|
102
|
+
* Returns `true` on success, `false` on failure.
|
|
103
|
+
*/
|
|
104
|
+
readonly writeMessages: (
|
|
105
|
+
ownerId: OwnerIdBytes,
|
|
106
|
+
messages: NonEmptyReadonlyArray<EncryptedCrdtMessage>,
|
|
107
|
+
) => Promise<boolean>;
|
|
108
|
+
|
|
109
|
+
/** Read encrypted {@link DbChange}s from storage. */
|
|
110
|
+
readonly readDbChange: (
|
|
111
|
+
ownerId: OwnerIdBytes,
|
|
112
|
+
timestamp: TimestampBytes,
|
|
113
|
+
) => EncryptedDbChange | null;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Delete all data for the given {@link Owner}.
|
|
117
|
+
*
|
|
118
|
+
* Returns `true` on success, `false` on failure.
|
|
119
|
+
*/
|
|
120
|
+
readonly deleteOwner: (ownerId: OwnerIdBytes) => boolean;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export interface StorageDep {
|
|
124
|
+
readonly storage: Storage;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A cryptographic hash used for efficiently comparing collections of
|
|
129
|
+
* {@link TimestampBytes}s.
|
|
130
|
+
*
|
|
131
|
+
* It consists of the first {@link fingerprintSize} bytes of the SHA-256 hash of
|
|
132
|
+
* one or more timestamps.
|
|
133
|
+
*/
|
|
134
|
+
export type Fingerprint = Uint8Array & Brand<"Fingerprint">;
|
|
135
|
+
|
|
136
|
+
export const fingerprintSize = 12 as NonNegativeInt;
|
|
137
|
+
|
|
138
|
+
/** A fingerprint of an empty range. */
|
|
139
|
+
export const zeroFingerprint = new Uint8Array(fingerprintSize) as Fingerprint;
|
|
140
|
+
|
|
141
|
+
export interface BaseRange {
|
|
142
|
+
readonly upperBound: RangeUpperBound;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Union type for Range's upperBound: either a {@link TimestampBytes} or
|
|
147
|
+
* {@link InfiniteUpperBound}.
|
|
148
|
+
*/
|
|
149
|
+
export type RangeUpperBound = TimestampBytes | InfiniteUpperBound;
|
|
150
|
+
|
|
151
|
+
export const InfiniteUpperBound = Symbol("InfiniteUpperBound");
|
|
152
|
+
export type InfiniteUpperBound = typeof InfiniteUpperBound;
|
|
153
|
+
|
|
154
|
+
export const RangeType = {
|
|
155
|
+
Fingerprint: 1,
|
|
156
|
+
Skip: 0,
|
|
157
|
+
Timestamps: 2,
|
|
158
|
+
} as const;
|
|
159
|
+
|
|
160
|
+
export type RangeType = (typeof RangeType)[keyof typeof RangeType];
|
|
161
|
+
|
|
162
|
+
export interface SkipRange extends BaseRange {
|
|
163
|
+
readonly type: typeof RangeType.Skip;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface FingerprintRange extends BaseRange {
|
|
167
|
+
readonly type: typeof RangeType.Fingerprint;
|
|
168
|
+
readonly fingerprint: Fingerprint;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export interface TimestampsRange extends BaseRange {
|
|
172
|
+
readonly type: typeof RangeType.Timestamps;
|
|
173
|
+
readonly timestamps: ReadonlyArray<TimestampBytes>;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type Range = SkipRange | FingerprintRange | TimestampsRange;
|
|
177
|
+
|
|
178
|
+
/** An encrypted {@link CrdtMessage}. */
|
|
179
|
+
export interface EncryptedCrdtMessage {
|
|
180
|
+
readonly timestamp: Timestamp;
|
|
181
|
+
readonly change: EncryptedDbChange;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Encrypted DbChange */
|
|
185
|
+
export type EncryptedDbChange = Uint8Array & Brand<"EncryptedDbChange">;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A CRDT message combining a unique {@link Timestamp} with a {@link DbChange}.
|
|
189
|
+
*
|
|
190
|
+
* Used in Evolu's sync protocol to replicate data changes across devices. Evolu
|
|
191
|
+
* operates as a durable queue, providing exactly-once delivery guarantees for
|
|
192
|
+
* reliable synchronization across application restarts and network failures.
|
|
193
|
+
*/
|
|
194
|
+
export interface CrdtMessage {
|
|
195
|
+
readonly timestamp: Timestamp;
|
|
196
|
+
readonly change: DbChange;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A DbChange is a change to a table row. Together with a unique
|
|
201
|
+
* {@link Timestamp}, it forms a {@link CrdtMessage}.
|
|
202
|
+
*/
|
|
203
|
+
export const DbChange = object({
|
|
204
|
+
table: String,
|
|
205
|
+
id: Id,
|
|
206
|
+
values: record(String, SqliteValue),
|
|
207
|
+
});
|
|
208
|
+
export type DbChange = typeof DbChange.Type;
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Common interface for both client and relay SQLite storages.
|
|
3
212
|
*
|
|
4
213
|
* Evolu uses a Skiplist, which leverages SQLite indexes. The core logic is
|
|
5
214
|
* implemented in SQL, so it doesn't have to make roundtrips to the DB.
|
|
6
215
|
*
|
|
7
|
-
*
|
|
216
|
+
* While the SQL implementation may look sophisticated, it's conceptually simple
|
|
217
|
+
* and LLMs can explain how it works. The Skiplist data structure is well
|
|
218
|
+
* explained in [this Stack Overflow
|
|
219
|
+
* answer](https://stackoverflow.com/questions/61944198/what-is-a-zip-tree-and-how-does-it-work).
|
|
220
|
+
* The logic resembles [Negentropy's C++
|
|
221
|
+
* storage](https://github.com/hoytech/negentropy), except we use a Skiplist to
|
|
222
|
+
* leverage SQLite indexes, which makes the code simpler.
|
|
223
|
+
*
|
|
224
|
+
* Note: A paid review by the SQLite team is planned, as they use the same
|
|
225
|
+
* algorithm for their rsync tool.
|
|
226
|
+
*
|
|
227
|
+
* The ideal storage for a Relay should use an architecture like
|
|
8
228
|
* [strfry](https://github.com/hoytech/strfry) (a KV storage), but with Skiplist
|
|
9
229
|
* to ensure that insertion order doesn't matter (local-first apps can often
|
|
10
230
|
* write in the past.)
|
|
@@ -19,37 +239,18 @@
|
|
|
19
239
|
* each other, if necessary. One relay should handle hundreds of thousands of
|
|
20
240
|
* users, and when it goes down, nothing happens, because it will be
|
|
21
241
|
* synchronized later.
|
|
22
|
-
*
|
|
23
|
-
* @module
|
|
24
242
|
*/
|
|
25
|
-
|
|
26
|
-
import { assert } from "../Assert.js";
|
|
27
|
-
import { decrement } from "../Number.js";
|
|
28
|
-
import { RandomDep } from "../Random.js";
|
|
29
|
-
import { ok, Result } from "../Result.js";
|
|
30
|
-
import { sql, SqliteDep, SqliteError } from "../Sqlite.js";
|
|
31
|
-
import { Int64String, NonNegativeInt, PositiveInt } from "../Type.js";
|
|
32
|
-
import { Brand } from "../Types.js";
|
|
33
|
-
import { OwnerId } from "./Owner.js";
|
|
34
|
-
import {
|
|
35
|
-
BinaryOwnerId,
|
|
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
243
|
export interface SqliteStorageBase {
|
|
244
|
+
/**
|
|
245
|
+
* Inserts a timestamp for an owner into the skiplist-based storage.
|
|
246
|
+
*
|
|
247
|
+
* Must be idempotent - inserting the same timestamp multiple times has no
|
|
248
|
+
* effect after the first insertion. This is crucial for sync reliability as
|
|
249
|
+
* messages may be received and processed multiple times.
|
|
250
|
+
*/
|
|
50
251
|
readonly insertTimestamp: (
|
|
51
|
-
ownerId:
|
|
52
|
-
timestamp:
|
|
252
|
+
ownerId: OwnerIdBytes,
|
|
253
|
+
timestamp: TimestampBytes,
|
|
53
254
|
) => Result<void, SqliteError>;
|
|
54
255
|
|
|
55
256
|
readonly getSize: Storage["getSize"];
|
|
@@ -57,13 +258,14 @@ export interface SqliteStorageBase {
|
|
|
57
258
|
readonly fingerprintRanges: Storage["fingerprintRanges"];
|
|
58
259
|
readonly findLowerBound: Storage["findLowerBound"];
|
|
59
260
|
readonly iterate: Storage["iterate"];
|
|
261
|
+
readonly deleteOwner: Storage["deleteOwner"];
|
|
60
262
|
}
|
|
61
263
|
|
|
62
264
|
export interface SqliteStorageBaseDep {
|
|
63
265
|
readonly storage: SqliteStorageBase;
|
|
64
266
|
}
|
|
65
267
|
|
|
66
|
-
export type SqliteStorageDeps =
|
|
268
|
+
export type SqliteStorageDeps = RandomDep & SqliteDep;
|
|
67
269
|
|
|
68
270
|
export interface CreateSqliteStorageBaseOptions {
|
|
69
271
|
onStorageError: (error: SqliteError) => void;
|
|
@@ -80,22 +282,22 @@ export const createSqliteStorageBase =
|
|
|
80
282
|
const ownerStats = new Map<
|
|
81
283
|
OwnerId,
|
|
82
284
|
{
|
|
83
|
-
minT:
|
|
84
|
-
maxT:
|
|
285
|
+
minT: TimestampBytes;
|
|
286
|
+
maxT: TimestampBytes;
|
|
85
287
|
}
|
|
86
288
|
>();
|
|
87
289
|
|
|
88
290
|
return ok({
|
|
89
|
-
insertTimestamp: (ownerId:
|
|
90
|
-
const ownerIdString =
|
|
291
|
+
insertTimestamp: (ownerId: OwnerIdBytes, timestamp: TimestampBytes) => {
|
|
292
|
+
const ownerIdString = ownerIdBytesToOwnerId(ownerId);
|
|
91
293
|
const level = randomSkiplistLevel(deps);
|
|
92
294
|
|
|
93
295
|
let stats = ownerStats.get(ownerIdString);
|
|
94
296
|
|
|
95
297
|
if (!stats) {
|
|
96
298
|
const result = deps.sqlite.exec<{
|
|
97
|
-
maxT:
|
|
98
|
-
minT:
|
|
299
|
+
maxT: TimestampBytes | null;
|
|
300
|
+
minT: TimestampBytes | null;
|
|
99
301
|
}>(sql.prepared`
|
|
100
302
|
select min(t) as minT, max(t) as maxT
|
|
101
303
|
from evolu_timestamp
|
|
@@ -112,10 +314,10 @@ export const createSqliteStorageBase =
|
|
|
112
314
|
|
|
113
315
|
let strategy: InsertTimestampStrategy;
|
|
114
316
|
|
|
115
|
-
if (
|
|
317
|
+
if (orderTimestampBytes(timestamp, stats.maxT) === 1) {
|
|
116
318
|
strategy = "append";
|
|
117
319
|
stats.maxT = timestamp;
|
|
118
|
-
} else if (
|
|
320
|
+
} else if (orderTimestampBytes(timestamp, stats.minT) === -1) {
|
|
119
321
|
strategy = "prepend";
|
|
120
322
|
stats.minT = timestamp;
|
|
121
323
|
} else {
|
|
@@ -192,7 +394,7 @@ export const createSqliteStorageBase =
|
|
|
192
394
|
* implementing chunking, be sure to run performance tests (including
|
|
193
395
|
* fetching one by one).
|
|
194
396
|
*/
|
|
195
|
-
const result = deps.sqlite.exec<{ t:
|
|
397
|
+
const result = deps.sqlite.exec<{ t: TimestampBytes }>(sql`
|
|
196
398
|
select t
|
|
197
399
|
from evolu_timestamp
|
|
198
400
|
where ownerId = ${ownerId} and t > ${first.value}
|
|
@@ -209,6 +411,17 @@ export const createSqliteStorageBase =
|
|
|
209
411
|
if (!callback(result.value.rows[i].t, index)) return;
|
|
210
412
|
}
|
|
211
413
|
},
|
|
414
|
+
|
|
415
|
+
deleteOwner: (ownerId) => {
|
|
416
|
+
const result = deps.sqlite.exec(sql`
|
|
417
|
+
delete from evolu_timestamp where ownerId = ${ownerId};
|
|
418
|
+
`);
|
|
419
|
+
if (!result.ok) {
|
|
420
|
+
options.onStorageError(result.error);
|
|
421
|
+
return false;
|
|
422
|
+
}
|
|
423
|
+
return true;
|
|
424
|
+
},
|
|
212
425
|
});
|
|
213
426
|
};
|
|
214
427
|
|
|
@@ -229,7 +442,7 @@ const createTables = (deps: SqliteDep): Result<void, SqliteError> => {
|
|
|
229
442
|
*
|
|
230
443
|
* Columns:
|
|
231
444
|
*
|
|
232
|
-
* - `t` –
|
|
445
|
+
* - `t` – TimestampBytes
|
|
233
446
|
* - `h1`/`h2` – 12-byte fingerprint split into two integers for fast XOR
|
|
234
447
|
* - `c` – incremental count
|
|
235
448
|
* - `l` – Skiplist level (1 to 32)
|
|
@@ -283,13 +496,13 @@ type InsertTimestampStrategy = "append" | "prepend" | "insert";
|
|
|
283
496
|
const insertTimestamp =
|
|
284
497
|
(deps: SqliteDep) =>
|
|
285
498
|
(
|
|
286
|
-
ownerId:
|
|
287
|
-
timestamp:
|
|
499
|
+
ownerId: OwnerIdBytes,
|
|
500
|
+
timestamp: TimestampBytes,
|
|
288
501
|
level: PositiveInt,
|
|
289
502
|
strategy: InsertTimestampStrategy,
|
|
290
503
|
): Result<void, SqliteError> => {
|
|
291
504
|
const [h1, h2] = fingerprintToSqliteFingerprint(
|
|
292
|
-
|
|
505
|
+
timestampBytesToFingerprint(timestamp),
|
|
293
506
|
);
|
|
294
507
|
|
|
295
508
|
let queries: Array<ReturnType<typeof sql.prepared>> = [];
|
|
@@ -807,6 +1020,13 @@ const insertTimestamp =
|
|
|
807
1020
|
return ok();
|
|
808
1021
|
};
|
|
809
1022
|
|
|
1023
|
+
export const timestampBytesToFingerprint = (
|
|
1024
|
+
timestamp: TimestampBytes,
|
|
1025
|
+
): Fingerprint => {
|
|
1026
|
+
const hash = sha256(timestamp).slice(0, fingerprintSize);
|
|
1027
|
+
return hash as Fingerprint;
|
|
1028
|
+
};
|
|
1029
|
+
|
|
810
1030
|
/**
|
|
811
1031
|
* Generates a random skiplist level in the range [1, skiplistMaxLevel].
|
|
812
1032
|
* Probabilistic approach avoids the need for explicit tree balancing.
|
|
@@ -887,7 +1107,7 @@ const sqliteFingerprintToFingerprint = ([
|
|
|
887
1107
|
|
|
888
1108
|
const getSize =
|
|
889
1109
|
(deps: SqliteDep) =>
|
|
890
|
-
(ownerId:
|
|
1110
|
+
(ownerId: OwnerIdBytes): Result<NonNegativeInt, SqliteError> => {
|
|
891
1111
|
const result = deps.sqlite.exec<{ size: NonNegativeInt }>(sql.prepared`
|
|
892
1112
|
with
|
|
893
1113
|
ml(ml) as (
|
|
@@ -930,7 +1150,7 @@ const getSize =
|
|
|
930
1150
|
const findLowerBound =
|
|
931
1151
|
(deps: SqliteDep) =>
|
|
932
1152
|
(
|
|
933
|
-
ownerId:
|
|
1153
|
+
ownerId: OwnerIdBytes,
|
|
934
1154
|
begin: NonNegativeInt,
|
|
935
1155
|
end: NonNegativeInt,
|
|
936
1156
|
upperBound: RangeUpperBound,
|
|
@@ -942,7 +1162,7 @@ const findLowerBound =
|
|
|
942
1162
|
}
|
|
943
1163
|
|
|
944
1164
|
const result = deps.sqlite.exec<{
|
|
945
|
-
t:
|
|
1165
|
+
t: TimestampBytes;
|
|
946
1166
|
}>(sql.prepared`
|
|
947
1167
|
select t
|
|
948
1168
|
from evolu_timestamp
|
|
@@ -966,8 +1186,8 @@ const findLowerBound =
|
|
|
966
1186
|
const getTimestampCount =
|
|
967
1187
|
(deps: SqliteDep) =>
|
|
968
1188
|
(
|
|
969
|
-
ownerId:
|
|
970
|
-
timestamp:
|
|
1189
|
+
ownerId: OwnerIdBytes,
|
|
1190
|
+
timestamp: TimestampBytes,
|
|
971
1191
|
): Result<PositiveInt, SqliteError> => {
|
|
972
1192
|
const result = deps.sqlite.exec<{
|
|
973
1193
|
count: PositiveInt;
|
|
@@ -1024,7 +1244,7 @@ const getTimestampCount =
|
|
|
1024
1244
|
const fingerprint =
|
|
1025
1245
|
(deps: SqliteDep) =>
|
|
1026
1246
|
(
|
|
1027
|
-
ownerId:
|
|
1247
|
+
ownerId: OwnerIdBytes,
|
|
1028
1248
|
begin: NonNegativeInt,
|
|
1029
1249
|
end: NonNegativeInt,
|
|
1030
1250
|
): Result<Fingerprint, SqliteError> => {
|
|
@@ -1056,14 +1276,14 @@ const fingerprint =
|
|
|
1056
1276
|
const fingerprintRanges =
|
|
1057
1277
|
(deps: SqliteDep) =>
|
|
1058
1278
|
(
|
|
1059
|
-
ownerId:
|
|
1279
|
+
ownerId: OwnerIdBytes,
|
|
1060
1280
|
buckets: ReadonlyArray<NonNegativeInt>,
|
|
1061
1281
|
upperBound: RangeUpperBound = InfiniteUpperBound,
|
|
1062
1282
|
): Result<ReadonlyArray<FingerprintRange>, SqliteError> => {
|
|
1063
1283
|
const bucketsJson = JSON.stringify(buckets);
|
|
1064
1284
|
|
|
1065
1285
|
const result = deps.sqlite.exec<{
|
|
1066
|
-
b:
|
|
1286
|
+
b: TimestampBytes | null;
|
|
1067
1287
|
h1: Int64String;
|
|
1068
1288
|
h2: Int64String;
|
|
1069
1289
|
}>(sql.prepared`
|
|
@@ -1199,11 +1419,11 @@ const x = (a: string, b: string) => sql.raw(`(${a} | ${b}) - (${a} & ${b})`);
|
|
|
1199
1419
|
export const getTimestampByIndex =
|
|
1200
1420
|
(deps: SqliteDep) =>
|
|
1201
1421
|
(
|
|
1202
|
-
ownerId:
|
|
1422
|
+
ownerId: OwnerIdBytes,
|
|
1203
1423
|
index: NonNegativeInt,
|
|
1204
|
-
): Result<
|
|
1424
|
+
): Result<TimestampBytes, SqliteError> => {
|
|
1205
1425
|
const result = deps.sqlite.exec<{
|
|
1206
|
-
readonly pt:
|
|
1426
|
+
readonly pt: TimestampBytes;
|
|
1207
1427
|
}>(sql.prepared`
|
|
1208
1428
|
with
|
|
1209
1429
|
fi(b, cl, ic, pt, mt, nt, nc) as (
|