@evolu/common 7.2.1 → 7.2.2
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/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +4 -1
- package/dist/src/local-first/Evolu.d.ts +14 -11
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Owner.d.ts +39 -42
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +22 -29
- package/dist/src/local-first/Protocol.d.ts +6 -6
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Public.d.ts +1 -0
- package/dist/src/local-first/Public.d.ts.map +1 -1
- package/dist/src/local-first/Storage.d.ts +3 -3
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Sync.d.ts +9 -14
- package/dist/src/local-first/Sync.d.ts.map +1 -1
- package/dist/src/local-first/Sync.js +21 -21
- package/package.json +1 -1
- package/src/WebSocket.ts +6 -1
- package/src/local-first/Evolu.ts +15 -11
- package/src/local-first/Owner.ts +47 -57
- package/src/local-first/Protocol.ts +6 -6
- package/src/local-first/Public.ts +1 -0
- package/src/local-first/Storage.ts +3 -3
- package/src/local-first/Sync.ts +31 -39
|
@@ -19,7 +19,7 @@ export const createSync = (deps) => (config) => {
|
|
|
19
19
|
const getSyncOwner = (ownerId) => {
|
|
20
20
|
if (isDisposed)
|
|
21
21
|
return null;
|
|
22
|
-
return
|
|
22
|
+
return resources.getConsumer(ownerId);
|
|
23
23
|
};
|
|
24
24
|
const storageResult = createClientStorage({
|
|
25
25
|
...deps,
|
|
@@ -28,21 +28,21 @@ export const createSync = (deps) => (config) => {
|
|
|
28
28
|
if (!storageResult.ok)
|
|
29
29
|
return storageResult;
|
|
30
30
|
const storage = storageResult.value;
|
|
31
|
-
const createResource = (
|
|
32
|
-
const transportKey = createTransportKey(
|
|
31
|
+
const createResource = (transport) => {
|
|
32
|
+
const transportKey = createTransportKey(transport);
|
|
33
33
|
deps.console.log("[sync]", "createWebSocket", {
|
|
34
34
|
transportKey,
|
|
35
|
-
url:
|
|
35
|
+
url: transport.url,
|
|
36
36
|
});
|
|
37
|
-
return deps.createWebSocket(
|
|
37
|
+
return deps.createWebSocket(transport.url, {
|
|
38
38
|
binaryType: "arraybuffer",
|
|
39
39
|
onOpen: () => {
|
|
40
40
|
if (isDisposed)
|
|
41
41
|
return;
|
|
42
|
-
const webSocket =
|
|
42
|
+
const webSocket = resources.getResource(transportKey);
|
|
43
43
|
if (!webSocket)
|
|
44
44
|
return;
|
|
45
|
-
const ownerIds =
|
|
45
|
+
const ownerIds = resources.getConsumersForResource(transportKey);
|
|
46
46
|
deps.console.log("[sync]", "onOpen", { transportKey, ownerIds });
|
|
47
47
|
for (const ownerId of ownerIds) {
|
|
48
48
|
const message = createProtocolMessageForSync({ storage })(ownerId, SubscriptionFlags.Subscribe);
|
|
@@ -67,7 +67,7 @@ export const createSync = (deps) => (config) => {
|
|
|
67
67
|
// Only handle ArrayBuffer data for sync messages
|
|
68
68
|
if (isDisposed || !(data instanceof ArrayBuffer))
|
|
69
69
|
return;
|
|
70
|
-
const webSocket =
|
|
70
|
+
const webSocket = resources.getResource(transportKey);
|
|
71
71
|
if (!webSocket)
|
|
72
72
|
return;
|
|
73
73
|
const input = new Uint8Array(data);
|
|
@@ -102,7 +102,7 @@ export const createSync = (deps) => (config) => {
|
|
|
102
102
|
},
|
|
103
103
|
});
|
|
104
104
|
};
|
|
105
|
-
const
|
|
105
|
+
const resources = createResources({
|
|
106
106
|
createResource,
|
|
107
107
|
getResourceKey: createTransportKey,
|
|
108
108
|
getConsumerId: (owner) => owner.id,
|
|
@@ -135,15 +135,15 @@ export const createSync = (deps) => (config) => {
|
|
|
135
135
|
return;
|
|
136
136
|
}
|
|
137
137
|
deps.console.log("[sync]", "useOwner", { use, owner });
|
|
138
|
-
const
|
|
138
|
+
const transports = owner.transports ?? config.transports;
|
|
139
139
|
if (use) {
|
|
140
|
-
|
|
140
|
+
resources.addConsumer(owner, transports);
|
|
141
141
|
}
|
|
142
142
|
else {
|
|
143
|
-
const result =
|
|
143
|
+
const result = resources.removeConsumer(owner, transports);
|
|
144
144
|
if (!result.ok) {
|
|
145
145
|
deps.console.warn("[sync]", "Failed to remove consumer", {
|
|
146
|
-
|
|
146
|
+
transports,
|
|
147
147
|
ownerId: owner.id,
|
|
148
148
|
error: result.error,
|
|
149
149
|
});
|
|
@@ -182,11 +182,11 @@ export const createSync = (deps) => (config) => {
|
|
|
182
182
|
encryptionKey: owner.encryptionKey,
|
|
183
183
|
writeKey: owner.writeKey,
|
|
184
184
|
}, messages);
|
|
185
|
-
const
|
|
185
|
+
const transports = owner.transports ?? config.transports;
|
|
186
186
|
// Send message to all transports for this owner
|
|
187
|
-
for (const
|
|
188
|
-
const transportKey = createTransportKey(
|
|
189
|
-
const webSocket =
|
|
187
|
+
for (const transport of transports) {
|
|
188
|
+
const transportKey = createTransportKey(transport);
|
|
189
|
+
const webSocket = resources.getResource(transportKey);
|
|
190
190
|
if (!webSocket)
|
|
191
191
|
continue;
|
|
192
192
|
if (webSocket.isOpen()) {
|
|
@@ -201,7 +201,7 @@ export const createSync = (deps) => (config) => {
|
|
|
201
201
|
if (isDisposed)
|
|
202
202
|
return;
|
|
203
203
|
isDisposed = true;
|
|
204
|
-
|
|
204
|
+
resources[Symbol.dispose]();
|
|
205
205
|
},
|
|
206
206
|
};
|
|
207
207
|
return ok(sync);
|
|
@@ -341,9 +341,9 @@ const createClientStorage = (deps) => (config) => {
|
|
|
341
341
|
};
|
|
342
342
|
return ok(storage);
|
|
343
343
|
};
|
|
344
|
-
/** Creates a unique identifier for a
|
|
345
|
-
const createTransportKey = (
|
|
346
|
-
return `${
|
|
344
|
+
/** Creates a unique identifier for a {@link OwnerTransport}. */
|
|
345
|
+
const createTransportKey = (transport) => {
|
|
346
|
+
return `${transport.type}:${transport.url}`;
|
|
347
347
|
};
|
|
348
348
|
export const applyLocalOnlyChange = (deps) => (change) => {
|
|
349
349
|
if (change.isDelete) {
|
package/package.json
CHANGED
package/src/WebSocket.ts
CHANGED
|
@@ -171,7 +171,12 @@ export const createWebSocket: CreateWebSocket = (
|
|
|
171
171
|
socket.onmessage = null;
|
|
172
172
|
socket.onerror = null;
|
|
173
173
|
|
|
174
|
-
|
|
174
|
+
if (
|
|
175
|
+
socket.readyState !== socket.CLOSING &&
|
|
176
|
+
socket.readyState !== socket.CLOSED
|
|
177
|
+
) {
|
|
178
|
+
socket.close();
|
|
179
|
+
}
|
|
175
180
|
socket = null;
|
|
176
181
|
};
|
|
177
182
|
|
package/src/local-first/Evolu.ts
CHANGED
|
@@ -423,32 +423,36 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> extends Disposable {
|
|
|
423
423
|
readonly exportDatabase: () => Promise<Uint8Array<ArrayBuffer>>;
|
|
424
424
|
|
|
425
425
|
/**
|
|
426
|
-
* Use
|
|
427
|
-
* broadcasted changes. Returns a function to stop using the owner.
|
|
426
|
+
* Use a {@link SyncOwner}. Returns a {@link UnuseOwner}.
|
|
428
427
|
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
428
|
+
* Using an owner means syncing it with its transports, or the transports
|
|
429
|
+
* defined in Evolu config if the owner has no transports defined.
|
|
430
|
+
*
|
|
431
|
+
* Transport are automatically deduplicated and reference-counted, so multiple
|
|
432
|
+
* owners using the same transport will share a single connection.
|
|
432
433
|
*
|
|
433
434
|
* ### Example
|
|
434
435
|
*
|
|
435
436
|
* ```ts
|
|
436
|
-
* // Use an owner (starts syncing
|
|
437
|
-
* const
|
|
437
|
+
* // Use an owner (starts syncing).
|
|
438
|
+
* const unuseOwner = evolu.useOwner(shardOwner);
|
|
438
439
|
*
|
|
439
440
|
* // Later, stop using the owner.
|
|
440
|
-
*
|
|
441
|
+
* unuseOwner();
|
|
441
442
|
*
|
|
442
443
|
* // Bulk operations.
|
|
443
|
-
* const
|
|
444
|
-
* // Later:
|
|
444
|
+
* const unuseOwners = owners.map((owner) => evolu.useOwner(owner));
|
|
445
|
+
* // Later: for (const unuse of unuseOwners) unuse();
|
|
445
446
|
* ```
|
|
446
447
|
*
|
|
447
448
|
* @experimental
|
|
448
449
|
*/
|
|
449
|
-
readonly useOwner: (owner: SyncOwner) =>
|
|
450
|
+
readonly useOwner: (owner: SyncOwner) => UnuseOwner;
|
|
450
451
|
}
|
|
451
452
|
|
|
453
|
+
/** Function returned by {@link Evolu#useOwner} to stop using an {@link SyncOwner}. */
|
|
454
|
+
export type UnuseOwner = () => void;
|
|
455
|
+
|
|
452
456
|
/** Represents errors that can occur in Evolu. */
|
|
453
457
|
export type EvoluError =
|
|
454
458
|
| ProtocolError
|
package/src/local-first/Owner.ts
CHANGED
|
@@ -22,15 +22,23 @@ import type { EncryptedDbChange, Storage } from "./Storage.js";
|
|
|
22
22
|
import { TimestampBytes } from "./Timestamp.js";
|
|
23
23
|
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
26
|
-
* assigned to an owner, enabling sync functionality and access control.
|
|
25
|
+
* {@link Owner} without a {@link OwnerWriteKey}.
|
|
27
26
|
*
|
|
28
|
-
*
|
|
29
|
-
|
|
27
|
+
* @see {@link createSharedReadonlyOwner}
|
|
28
|
+
*/
|
|
29
|
+
export interface ReadonlyOwner {
|
|
30
|
+
readonly id: OwnerId;
|
|
31
|
+
readonly encryptionKey: OwnerEncryptionKey;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* The Owner represents ownership of data in Evolu. Every database change is
|
|
36
|
+
* assigned to an owner and encrypted with its {@link OwnerEncryptionKey}. Owners
|
|
37
|
+
* allow partial sync, only the {@link AppOwner} is synced by default.
|
|
30
38
|
*
|
|
31
|
-
* Owners also provide
|
|
32
|
-
* local-first/distributed systems can only be
|
|
33
|
-
*
|
|
39
|
+
* Owners can also provide real data deletion, while individual changes in
|
|
40
|
+
* local-first/distributed systems can only be soft deleted, entire owners can
|
|
41
|
+
* be completely deleted from both relays and devices (except for
|
|
34
42
|
* {@link AppOwner}, which must be preserved for sync coordination).
|
|
35
43
|
*
|
|
36
44
|
* Evolu provides different owner types depending on their use case:
|
|
@@ -46,21 +54,19 @@ import { TimestampBytes } from "./Timestamp.js";
|
|
|
46
54
|
* SLIP-21, ensuring secure and deterministic key generation:
|
|
47
55
|
*
|
|
48
56
|
* - {@link OwnerId}: Globally unique public identifier
|
|
49
|
-
* - {@link
|
|
57
|
+
* - {@link OwnerEncryptionKey}: Symmetric encryption key for data protection
|
|
50
58
|
* - {@link OwnerWriteKey}: Authentication token for write operations (rotatable)
|
|
51
59
|
*
|
|
52
|
-
* @see {@link
|
|
60
|
+
* @see {@link createAppOwner}
|
|
61
|
+
* @see {@link createShardOwner}
|
|
62
|
+
* @see {@link createSharedOwner}
|
|
63
|
+
* @see {@link createSharedReadonlyOwner}
|
|
53
64
|
*/
|
|
54
|
-
export interface Owner {
|
|
55
|
-
readonly id: OwnerId;
|
|
56
|
-
readonly encryptionKey: OwnerEncryptionKey;
|
|
65
|
+
export interface Owner extends ReadonlyOwner {
|
|
57
66
|
readonly writeKey: OwnerWriteKey;
|
|
58
67
|
}
|
|
59
68
|
|
|
60
|
-
/**
|
|
61
|
-
* OwnerId is a branded {@link Id} that uniquely identifies an {@link Owner}.
|
|
62
|
-
* Branded from {@link Id} to leverage existing helpers like {@link idToIdBytes}.
|
|
63
|
-
*/
|
|
69
|
+
/** OwnerId is a branded {@link Id} that uniquely identifies an {@link Owner}. */
|
|
64
70
|
export const OwnerId = brand("OwnerId", Id);
|
|
65
71
|
export type OwnerId = typeof OwnerId.Type;
|
|
66
72
|
|
|
@@ -68,14 +74,17 @@ export type OwnerId = typeof OwnerId.Type;
|
|
|
68
74
|
export const OwnerIdBytes = brand("OwnerIdBytes", IdBytes);
|
|
69
75
|
export type OwnerIdBytes = typeof OwnerIdBytes.Type;
|
|
70
76
|
|
|
77
|
+
/** Converts {@link OwnerId} to {@link OwnerIdBytes}. */
|
|
71
78
|
export const ownerIdToOwnerIdBytes = (ownerId: OwnerId): OwnerIdBytes =>
|
|
72
79
|
idToIdBytes(ownerId) as OwnerIdBytes;
|
|
73
80
|
|
|
81
|
+
/** Converts {@link OwnerIdBytes} to {@link OwnerId}. */
|
|
74
82
|
export const ownerIdBytesToOwnerId = (ownerIdBytes: OwnerIdBytes): OwnerId =>
|
|
75
83
|
idBytesToId(ownerIdBytes as IdBytes) as OwnerId;
|
|
76
84
|
|
|
77
85
|
export const ownerWriteKeyLength = NonNegativeInt.orThrow(16);
|
|
78
86
|
|
|
87
|
+
/** Symmetric encryption key for {@link Owner} data protection. */
|
|
79
88
|
export const OwnerEncryptionKey = brand("OwnerEncryptionKey", EncryptionKey);
|
|
80
89
|
export type OwnerEncryptionKey = typeof OwnerEncryptionKey.Type;
|
|
81
90
|
|
|
@@ -86,6 +95,16 @@ export type OwnerEncryptionKey = typeof OwnerEncryptionKey.Type;
|
|
|
86
95
|
export const OwnerWriteKey = brand("OwnerWriteKey", Entropy16);
|
|
87
96
|
export type OwnerWriteKey = typeof OwnerWriteKey.Type;
|
|
88
97
|
|
|
98
|
+
/**
|
|
99
|
+
* Creates a new random {@link OwnerWriteKey} for rotation.
|
|
100
|
+
*
|
|
101
|
+
* The initial OwnerWriteKey is deterministically derived from
|
|
102
|
+
* {@link OwnerSecret}. Use `createOwnerWriteKey` to rotate (replace) the write
|
|
103
|
+
* key without changing the owner identity.
|
|
104
|
+
*/
|
|
105
|
+
export const createOwnerWriteKey = (deps: RandomBytesDep): OwnerWriteKey =>
|
|
106
|
+
deps.randomBytes.create(16) as OwnerWriteKey;
|
|
107
|
+
|
|
89
108
|
/**
|
|
90
109
|
* 32 bytes of cryptographic entropy used to derive {@link Owner} keys.
|
|
91
110
|
*
|
|
@@ -107,22 +126,11 @@ export const ownerSecretToMnemonic = (secret: OwnerSecret): Mnemonic =>
|
|
|
107
126
|
export const mnemonicToOwnerSecret = (mnemonic: Mnemonic): OwnerSecret =>
|
|
108
127
|
bip39.mnemonicToEntropy(mnemonic, wordlist) as OwnerSecret;
|
|
109
128
|
|
|
110
|
-
/** Creates a randomly generated {@link OwnerWriteKey}. */
|
|
111
|
-
export const createOwnerWriteKey = (deps: RandomBytesDep): OwnerWriteKey =>
|
|
112
|
-
deps.randomBytes.create(16) as OwnerWriteKey;
|
|
113
|
-
|
|
114
129
|
/**
|
|
115
130
|
* Creates an {@link Owner} from a {@link OwnerSecret} using SLIP-21 key
|
|
116
131
|
* derivation.
|
|
117
|
-
*
|
|
118
|
-
* This is an internal helper function, use:
|
|
119
|
-
*
|
|
120
|
-
* - {@link createAppOwner}
|
|
121
|
-
* - {@link createShardOwner}
|
|
122
|
-
* - {@link createSharedOwner}
|
|
123
|
-
* - {@link createSharedReadonlyOwner}
|
|
124
132
|
*/
|
|
125
|
-
|
|
133
|
+
const createOwner = (secret: OwnerSecret): Owner => ({
|
|
126
134
|
id: ownerIdBytesToOwnerId(
|
|
127
135
|
OwnerIdBytes.orThrow(
|
|
128
136
|
createSlip21(secret, ["Evolu", "OwnerIdBytes"]).slice(0, 16),
|
|
@@ -182,9 +190,9 @@ export interface AppOwner extends Owner {
|
|
|
182
190
|
|
|
183
191
|
/** Creates an {@link AppOwner} from an {@link OwnerSecret}. */
|
|
184
192
|
export const createAppOwner = (secret: OwnerSecret): AppOwner => ({
|
|
193
|
+
...createOwner(secret),
|
|
185
194
|
type: "AppOwner",
|
|
186
195
|
mnemonic: ownerSecretToMnemonic(secret),
|
|
187
|
-
...createOwner(secret),
|
|
188
196
|
});
|
|
189
197
|
|
|
190
198
|
/**
|
|
@@ -200,18 +208,13 @@ export const createAppOwner = (secret: OwnerSecret): AppOwner => ({
|
|
|
200
208
|
*/
|
|
201
209
|
export interface ShardOwner extends Owner {
|
|
202
210
|
readonly type: "ShardOwner";
|
|
203
|
-
readonly transports?: ReadonlyArray<OwnerTransport>;
|
|
204
211
|
}
|
|
205
212
|
|
|
206
213
|
/** Creates a {@link ShardOwner} from an {@link OwnerSecret}. */
|
|
207
|
-
export const createShardOwner = (
|
|
208
|
-
secret: OwnerSecret,
|
|
209
|
-
transports?: ReadonlyArray<OwnerTransport>,
|
|
210
|
-
): ShardOwner => {
|
|
214
|
+
export const createShardOwner = (secret: OwnerSecret): ShardOwner => {
|
|
211
215
|
return {
|
|
212
|
-
type: "ShardOwner",
|
|
213
216
|
...createOwner(secret),
|
|
214
|
-
|
|
217
|
+
type: "ShardOwner",
|
|
215
218
|
};
|
|
216
219
|
};
|
|
217
220
|
|
|
@@ -236,21 +239,18 @@ export const createShardOwner = (
|
|
|
236
239
|
export const deriveShardOwner = (
|
|
237
240
|
owner: AppOwner,
|
|
238
241
|
path: NonEmptyReadonlyArray<string | number>,
|
|
239
|
-
transports?: ReadonlyArray<OwnerTransport>,
|
|
240
242
|
): ShardOwner => {
|
|
241
243
|
const secret = createSlip21(owner.encryptionKey, path) as OwnerSecret;
|
|
242
244
|
|
|
243
245
|
return {
|
|
244
|
-
type: "ShardOwner",
|
|
245
246
|
...createOwner(secret),
|
|
246
|
-
|
|
247
|
+
type: "ShardOwner",
|
|
247
248
|
};
|
|
248
249
|
};
|
|
249
250
|
|
|
250
251
|
/** An {@link Owner} for collaborative data with write access. */
|
|
251
252
|
export interface SharedOwner extends Owner {
|
|
252
253
|
readonly type: "SharedOwner";
|
|
253
|
-
readonly transports?: ReadonlyArray<OwnerTransport>;
|
|
254
254
|
}
|
|
255
255
|
|
|
256
256
|
/**
|
|
@@ -260,27 +260,18 @@ export interface SharedOwner extends Owner {
|
|
|
260
260
|
* Use {@link createSharedReadonlyOwner} to create a read-only version for
|
|
261
261
|
* sharing.
|
|
262
262
|
*/
|
|
263
|
-
export const createSharedOwner = (
|
|
264
|
-
secret
|
|
265
|
-
|
|
266
|
-
)
|
|
267
|
-
return {
|
|
268
|
-
type: "SharedOwner",
|
|
269
|
-
...createOwner(secret),
|
|
270
|
-
...(transports && { transports }),
|
|
271
|
-
};
|
|
272
|
-
};
|
|
263
|
+
export const createSharedOwner = (secret: OwnerSecret): SharedOwner => ({
|
|
264
|
+
...createOwner(secret),
|
|
265
|
+
type: "SharedOwner",
|
|
266
|
+
});
|
|
273
267
|
|
|
274
268
|
/**
|
|
275
269
|
* Read-only version of a {@link SharedOwner} for data sharing. Contains only the
|
|
276
270
|
* {@link OwnerId} and {@link EncryptionKey} needed for others to read the shared
|
|
277
271
|
* data without write access.
|
|
278
272
|
*/
|
|
279
|
-
export interface SharedReadonlyOwner {
|
|
273
|
+
export interface SharedReadonlyOwner extends ReadonlyOwner {
|
|
280
274
|
readonly type: "SharedReadonlyOwner";
|
|
281
|
-
readonly id: OwnerId;
|
|
282
|
-
readonly encryptionKey: EncryptionKey;
|
|
283
|
-
readonly transports?: ReadonlyArray<OwnerTransport>;
|
|
284
275
|
}
|
|
285
276
|
|
|
286
277
|
/** Creates a {@link SharedReadonlyOwner} from a {@link SharedOwner}. */
|
|
@@ -290,7 +281,6 @@ export const createSharedReadonlyOwner = (
|
|
|
290
281
|
type: "SharedReadonlyOwner",
|
|
291
282
|
id: sharedOwner.id,
|
|
292
283
|
encryptionKey: sharedOwner.encryptionKey,
|
|
293
|
-
...(sharedOwner.transports && { transports: sharedOwner.transports }),
|
|
294
284
|
});
|
|
295
285
|
|
|
296
286
|
/**
|
|
@@ -383,8 +373,8 @@ export const parseOwnerIdFromOwnerWebSocketTransportUrl = (
|
|
|
383
373
|
url: string,
|
|
384
374
|
): OwnerId | null => getOrNull(OwnerId.fromUnknown(url.split("=")[1]));
|
|
385
375
|
|
|
386
|
-
/**
|
|
387
|
-
export interface
|
|
376
|
+
/** Common interface implemented by all owner domain errors. */
|
|
377
|
+
export interface OwnerError {
|
|
388
378
|
readonly ownerId: OwnerId;
|
|
389
379
|
}
|
|
390
380
|
|
|
@@ -221,7 +221,7 @@ import {
|
|
|
221
221
|
} from "../Type.js";
|
|
222
222
|
import { Predicate } from "../Types.js";
|
|
223
223
|
import {
|
|
224
|
-
|
|
224
|
+
OwnerError,
|
|
225
225
|
Owner,
|
|
226
226
|
OwnerId,
|
|
227
227
|
OwnerIdBytes,
|
|
@@ -382,7 +382,7 @@ export type ProtocolError =
|
|
|
382
382
|
* Represents a version mismatch in the Evolu Protocol. Occurs when the
|
|
383
383
|
* initiator and non-initiator are using incompatible protocol versions.
|
|
384
384
|
*/
|
|
385
|
-
export interface ProtocolVersionError extends
|
|
385
|
+
export interface ProtocolVersionError extends OwnerError {
|
|
386
386
|
readonly type: "ProtocolVersionError";
|
|
387
387
|
readonly version: NonNegativeInt;
|
|
388
388
|
/** Indicates which side is obsolete and should update. */
|
|
@@ -397,7 +397,7 @@ export interface ProtocolInvalidDataError {
|
|
|
397
397
|
}
|
|
398
398
|
|
|
399
399
|
/** Error when a {@link OwnerWriteKey} is invalid, missing, or fails validation. */
|
|
400
|
-
export interface ProtocolWriteKeyError extends
|
|
400
|
+
export interface ProtocolWriteKeyError extends OwnerError {
|
|
401
401
|
readonly type: "ProtocolWriteKeyError";
|
|
402
402
|
}
|
|
403
403
|
|
|
@@ -405,7 +405,7 @@ export interface ProtocolWriteKeyError extends BaseOwnerError {
|
|
|
405
405
|
* Error indicating a serious relay-side write failure. Clients should log this
|
|
406
406
|
* error and show a generic sync error to the user.
|
|
407
407
|
*/
|
|
408
|
-
export interface ProtocolWriteError extends
|
|
408
|
+
export interface ProtocolWriteError extends OwnerError {
|
|
409
409
|
readonly type: "ProtocolWriteError";
|
|
410
410
|
}
|
|
411
411
|
|
|
@@ -422,7 +422,7 @@ export interface ProtocolWriteError extends BaseOwnerError {
|
|
|
422
422
|
* plan. Quota monitoring and management is the relay provider's
|
|
423
423
|
* responsibility.
|
|
424
424
|
*/
|
|
425
|
-
export interface ProtocolQuotaError extends
|
|
425
|
+
export interface ProtocolQuotaError extends OwnerError {
|
|
426
426
|
readonly type: "ProtocolQuotaError";
|
|
427
427
|
}
|
|
428
428
|
|
|
@@ -430,7 +430,7 @@ export interface ProtocolQuotaError extends BaseOwnerError {
|
|
|
430
430
|
* Error indicating a serious relay-side synchronization failure. Clients should
|
|
431
431
|
* log this error and show a generic sync error to the user.
|
|
432
432
|
*/
|
|
433
|
-
export interface ProtocolSyncError extends
|
|
433
|
+
export interface ProtocolSyncError extends OwnerError {
|
|
434
434
|
readonly type: "ProtocolSyncError";
|
|
435
435
|
}
|
|
436
436
|
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
export { createEvolu } from "./Evolu.js";
|
|
8
8
|
export type { Evolu, EvoluConfig, EvoluDeps, EvoluError } from "./Evolu.js";
|
|
9
|
+
export type { UnuseOwner } from "./Evolu.js";
|
|
9
10
|
export * from "./LocalAuth.js";
|
|
10
11
|
export * from "./Owner.js";
|
|
11
12
|
export * as kysely from "./PublicKysely.js";
|
|
@@ -26,7 +26,7 @@ import {
|
|
|
26
26
|
TypeError,
|
|
27
27
|
} from "../Type.js";
|
|
28
28
|
import {
|
|
29
|
-
|
|
29
|
+
OwnerError,
|
|
30
30
|
Owner,
|
|
31
31
|
OwnerId,
|
|
32
32
|
OwnerIdBytes,
|
|
@@ -172,12 +172,12 @@ export interface StorageDep {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
/** Error indicating a serious write failure. */
|
|
175
|
-
export interface StorageWriteError extends
|
|
175
|
+
export interface StorageWriteError extends OwnerError {
|
|
176
176
|
readonly type: "StorageWriteError";
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
/** Error when storage or billing quota is exceeded. */
|
|
180
|
-
export interface StorageQuotaError extends
|
|
180
|
+
export interface StorageQuotaError extends OwnerError {
|
|
181
181
|
readonly type: "StorageQuotaError";
|
|
182
182
|
}
|
|
183
183
|
|
package/src/local-first/Sync.ts
CHANGED
|
@@ -42,16 +42,13 @@ import { CreateWebSocketDep, WebSocket } from "../WebSocket.js";
|
|
|
42
42
|
import type { AppOwnerDep, PostMessageDep } from "./Db.js";
|
|
43
43
|
import {
|
|
44
44
|
AppOwner,
|
|
45
|
-
|
|
45
|
+
Owner,
|
|
46
46
|
OwnerId,
|
|
47
47
|
OwnerIdBytes,
|
|
48
48
|
ownerIdBytesToOwnerId,
|
|
49
49
|
ownerIdToOwnerIdBytes,
|
|
50
50
|
OwnerTransport,
|
|
51
|
-
|
|
52
|
-
ShardOwner,
|
|
53
|
-
SharedOwner,
|
|
54
|
-
SharedReadonlyOwner,
|
|
51
|
+
ReadonlyOwner,
|
|
55
52
|
} from "./Owner.js";
|
|
56
53
|
import {
|
|
57
54
|
applyProtocolMessageAsClient,
|
|
@@ -97,9 +94,9 @@ export interface Sync extends Disposable {
|
|
|
97
94
|
/**
|
|
98
95
|
* Assigns or removes an owner to/from transports with reference counting.
|
|
99
96
|
*
|
|
100
|
-
* Owners are only
|
|
101
|
-
* `owner.transports` or falls back to
|
|
102
|
-
* increment/decrement reference counts (useful for React Hooks).
|
|
97
|
+
* Owners are only synced if assigned to at least one transport. Uses
|
|
98
|
+
* `owner.transports` or falls back to {@link SyncConfig} transports. Multiple
|
|
99
|
+
* calls increment/decrement reference counts (useful for React Hooks).
|
|
103
100
|
*/
|
|
104
101
|
readonly useOwner: (use: boolean, owner: SyncOwner) => void;
|
|
105
102
|
|
|
@@ -119,18 +116,13 @@ export interface SyncDep {
|
|
|
119
116
|
}
|
|
120
117
|
|
|
121
118
|
/**
|
|
122
|
-
* Represents an owner for sync operations.
|
|
123
|
-
* abstracts over the specific owner types ({@link ShardOwner},
|
|
124
|
-
* {@link SharedOwner}, {@link SharedReadonlyOwner}) for the sync layer.
|
|
119
|
+
* Represents an owner for sync operations.
|
|
125
120
|
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
121
|
+
* Includes readonly owner fields plus optional write key (for clients that
|
|
122
|
+
* write) and optional transports to override SyncConfig transports per owner.
|
|
128
123
|
*/
|
|
129
|
-
export interface SyncOwner {
|
|
130
|
-
readonly
|
|
131
|
-
readonly encryptionKey: OwnerEncryptionKey;
|
|
132
|
-
/** Optional for read-only owners like {@link SharedReadonlyOwner}. */
|
|
133
|
-
readonly writeKey?: OwnerWriteKey;
|
|
124
|
+
export interface SyncOwner extends ReadonlyOwner {
|
|
125
|
+
readonly writeKey?: Owner["writeKey"];
|
|
134
126
|
readonly transports?: ReadonlyArray<OwnerTransport>;
|
|
135
127
|
}
|
|
136
128
|
|
|
@@ -180,7 +172,7 @@ export const createSync =
|
|
|
180
172
|
/** Returns owner data only if actively assigned to at least one transport. */
|
|
181
173
|
const getSyncOwner = (ownerId: OwnerId): SyncOwner | null => {
|
|
182
174
|
if (isDisposed) return null;
|
|
183
|
-
return
|
|
175
|
+
return resources.getConsumer(ownerId);
|
|
184
176
|
};
|
|
185
177
|
|
|
186
178
|
const storageResult = createClientStorage({
|
|
@@ -191,24 +183,24 @@ export const createSync =
|
|
|
191
183
|
if (!storageResult.ok) return storageResult;
|
|
192
184
|
const storage = storageResult.value;
|
|
193
185
|
|
|
194
|
-
const createResource = (
|
|
195
|
-
const transportKey = createTransportKey(
|
|
186
|
+
const createResource = (transport: OwnerTransport): WebSocket => {
|
|
187
|
+
const transportKey = createTransportKey(transport);
|
|
196
188
|
|
|
197
189
|
deps.console.log("[sync]", "createWebSocket", {
|
|
198
190
|
transportKey,
|
|
199
|
-
url:
|
|
191
|
+
url: transport.url,
|
|
200
192
|
});
|
|
201
193
|
|
|
202
|
-
return deps.createWebSocket(
|
|
194
|
+
return deps.createWebSocket(transport.url, {
|
|
203
195
|
binaryType: "arraybuffer",
|
|
204
196
|
|
|
205
197
|
onOpen: () => {
|
|
206
198
|
if (isDisposed) return;
|
|
207
199
|
|
|
208
|
-
const webSocket =
|
|
200
|
+
const webSocket = resources.getResource(transportKey);
|
|
209
201
|
if (!webSocket) return;
|
|
210
202
|
|
|
211
|
-
const ownerIds =
|
|
203
|
+
const ownerIds = resources.getConsumersForResource(transportKey);
|
|
212
204
|
deps.console.log("[sync]", "onOpen", { transportKey, ownerIds });
|
|
213
205
|
|
|
214
206
|
for (const ownerId of ownerIds) {
|
|
@@ -239,7 +231,7 @@ export const createSync =
|
|
|
239
231
|
// Only handle ArrayBuffer data for sync messages
|
|
240
232
|
if (isDisposed || !(data instanceof ArrayBuffer)) return;
|
|
241
233
|
|
|
242
|
-
const webSocket =
|
|
234
|
+
const webSocket = resources.getResource(transportKey);
|
|
243
235
|
if (!webSocket) return;
|
|
244
236
|
|
|
245
237
|
const input = new Uint8Array(data);
|
|
@@ -277,7 +269,7 @@ export const createSync =
|
|
|
277
269
|
});
|
|
278
270
|
};
|
|
279
271
|
|
|
280
|
-
const
|
|
272
|
+
const resources = createResources<
|
|
281
273
|
WebSocket,
|
|
282
274
|
TransportKey,
|
|
283
275
|
OwnerTransport,
|
|
@@ -327,16 +319,16 @@ export const createSync =
|
|
|
327
319
|
}
|
|
328
320
|
|
|
329
321
|
deps.console.log("[sync]", "useOwner", { use, owner });
|
|
330
|
-
const
|
|
322
|
+
const transports = owner.transports ?? config.transports;
|
|
331
323
|
|
|
332
324
|
if (use) {
|
|
333
|
-
|
|
325
|
+
resources.addConsumer(owner, transports);
|
|
334
326
|
} else {
|
|
335
|
-
const result =
|
|
327
|
+
const result = resources.removeConsumer(owner, transports);
|
|
336
328
|
|
|
337
329
|
if (!result.ok) {
|
|
338
330
|
deps.console.warn("[sync]", "Failed to remove consumer", {
|
|
339
|
-
|
|
331
|
+
transports,
|
|
340
332
|
ownerId: owner.id,
|
|
341
333
|
error: result.error,
|
|
342
334
|
});
|
|
@@ -382,13 +374,13 @@ export const createSync =
|
|
|
382
374
|
messages,
|
|
383
375
|
);
|
|
384
376
|
|
|
385
|
-
const
|
|
377
|
+
const transports = owner.transports ?? config.transports;
|
|
386
378
|
|
|
387
379
|
// Send message to all transports for this owner
|
|
388
|
-
for (const
|
|
389
|
-
const transportKey = createTransportKey(
|
|
380
|
+
for (const transport of transports) {
|
|
381
|
+
const transportKey = createTransportKey(transport);
|
|
390
382
|
|
|
391
|
-
const webSocket =
|
|
383
|
+
const webSocket = resources.getResource(transportKey);
|
|
392
384
|
if (!webSocket) continue;
|
|
393
385
|
|
|
394
386
|
if (webSocket.isOpen()) {
|
|
@@ -404,7 +396,7 @@ export const createSync =
|
|
|
404
396
|
[Symbol.dispose]: () => {
|
|
405
397
|
if (isDisposed) return;
|
|
406
398
|
isDisposed = true;
|
|
407
|
-
|
|
399
|
+
resources[Symbol.dispose]();
|
|
408
400
|
},
|
|
409
401
|
};
|
|
410
402
|
|
|
@@ -646,9 +638,9 @@ const createClientStorage =
|
|
|
646
638
|
|
|
647
639
|
type TransportKey = string & Brand<"TransportKey">;
|
|
648
640
|
|
|
649
|
-
/** Creates a unique identifier for a
|
|
650
|
-
const createTransportKey = (
|
|
651
|
-
return `${
|
|
641
|
+
/** Creates a unique identifier for a {@link OwnerTransport}. */
|
|
642
|
+
const createTransportKey = (transport: OwnerTransport): TransportKey => {
|
|
643
|
+
return `${transport.type}:${transport.url}` as TransportKey;
|
|
652
644
|
};
|
|
653
645
|
|
|
654
646
|
export const applyLocalOnlyChange =
|