Skip to content

Commit 37e653c

Browse files
committed
Improve Owner docs and code, close evoluhq#615
1 parent 7e97930 commit 37e653c

12 files changed

Lines changed: 135 additions & 140 deletions

File tree

.changeset/bumpy-cameras-ask.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
"@evolu/common": patch
3+
---
4+
5+
Improve Owner API documentation and consistency
6+
7+
- Add `ReadonlyOwner` interface for owners without write keys
8+
- Export `UnuseOwner` type for better API clarity
9+
- Improve JSDoc comments across Owner types and related interfaces
10+
- Rename `BaseOwnerError` to `OwnerError` for consistency
11+
- Remove `createOwner` from public exports (use specific owner creation functions)
12+
- Remove transport properties from owner types (now passed via `useOwner`)
13+
- Add documentation for `OwnerWriteKey` rotation
14+
- Improve `useOwner` documentation in React and Vue hooks

packages/common/src/local-first/Evolu.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -423,32 +423,36 @@ export interface Evolu<S extends EvoluSchema = EvoluSchema> extends Disposable {
423423
readonly exportDatabase: () => Promise<Uint8Array<ArrayBuffer>>;
424424

425425
/**
426-
* Use an owner. Using an owner means syncing it and subscribing to
427-
* broadcasted changes. Returns a function to stop using the owner.
426+
* Use a {@link SyncOwner}. Returns a {@link UnuseOwner}.
428427
*
429-
* Transport connections are automatically deduplicated and reference-counted,
430-
* so multiple owners using the same transport will share a single
431-
* connection.
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.
432433
*
433434
* ### Example
434435
*
435436
* ```ts
436-
* // Use an owner (starts syncing and subscribing to changes).
437-
* const unuse = evolu.useOwner(shardOwner);
437+
* // Use an owner (starts syncing).
438+
* const unuseOwner = evolu.useOwner(shardOwner);
438439
*
439440
* // Later, stop using the owner.
440-
* unuse();
441+
* unuseOwner();
441442
*
442443
* // Bulk operations.
443-
* const unuses = owners.map((owner) => evolu.useOwner(owner));
444-
* // Later: unuses.forEach(unuse => unuse());
444+
* const unuseOwners = owners.map((owner) => evolu.useOwner(owner));
445+
* // Later: for (const unuse of unuseOwners) unuse();
445446
* ```
446447
*
447448
* @experimental
448449
*/
449-
readonly useOwner: (owner: SyncOwner) => () => void;
450+
readonly useOwner: (owner: SyncOwner) => UnuseOwner;
450451
}
451452

453+
/** Function returned by {@link Evolu#useOwner} to stop using an {@link SyncOwner}. */
454+
export type UnuseOwner = () => void;
455+
452456
/** Represents errors that can occur in Evolu. */
453457
export type EvoluError =
454458
| ProtocolError

packages/common/src/local-first/Owner.ts

Lines changed: 47 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,23 @@ import type { EncryptedDbChange, Storage } from "./Storage.js";
2222
import { TimestampBytes } from "./Timestamp.js";
2323

2424
/**
25-
* The Owner represents ownership of data in Evolu. Every database change is
26-
* assigned to an owner, enabling sync functionality and access control.
25+
* {@link Owner} without a {@link OwnerWriteKey}.
2726
*
28-
* Owners enable **partial sync** - applications can choose which owners to
29-
* sync, allowing selective data synchronization based on specific needs.
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.
3038
*
31-
* Owners also provide **real data deletion** - while individual changes in
32-
* local-first/distributed systems can only be marked as deleted, entire owners
33-
* can be completely deleted from both relays and devices (except for
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
3442
* {@link AppOwner}, which must be preserved for sync coordination).
3543
*
3644
* Evolu provides different owner types depending on their use case:
@@ -46,36 +54,37 @@ import { TimestampBytes } from "./Timestamp.js";
4654
* SLIP-21, ensuring secure and deterministic key generation:
4755
*
4856
* - {@link OwnerId}: Globally unique public identifier
49-
* - {@link EncryptionKey}: Symmetric encryption key for data protection
57+
* - {@link OwnerEncryptionKey}: Symmetric encryption key for data protection
5058
* - {@link OwnerWriteKey}: Authentication token for write operations (rotatable)
5159
*
52-
* @see {@link createOwner}
60+
* @see {@link createAppOwner}
61+
* @see {@link createShardOwner}
62+
* @see {@link createSharedOwner}
63+
* @see {@link createSharedReadonlyOwner}
5364
*/
54-
export interface Owner {
55-
readonly id: OwnerId;
56-
readonly encryptionKey: OwnerEncryptionKey;
65+
export interface Owner extends ReadonlyOwner {
5766
readonly writeKey: OwnerWriteKey;
5867
}
5968

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}. */
6470
export const OwnerId = brand("OwnerId", Id);
6571
export type OwnerId = typeof OwnerId.Type;
6672

6773
/** Bytes representation of {@link OwnerId}. */
6874
export const OwnerIdBytes = brand("OwnerIdBytes", IdBytes);
6975
export type OwnerIdBytes = typeof OwnerIdBytes.Type;
7076

77+
/** Converts {@link OwnerId} to {@link OwnerIdBytes}. */
7178
export const ownerIdToOwnerIdBytes = (ownerId: OwnerId): OwnerIdBytes =>
7279
idToIdBytes(ownerId) as OwnerIdBytes;
7380

81+
/** Converts {@link OwnerIdBytes} to {@link OwnerId}. */
7482
export const ownerIdBytesToOwnerId = (ownerIdBytes: OwnerIdBytes): OwnerId =>
7583
idBytesToId(ownerIdBytes as IdBytes) as OwnerId;
7684

7785
export const ownerWriteKeyLength = NonNegativeInt.orThrow(16);
7886

87+
/** Symmetric encryption key for {@link Owner} data protection. */
7988
export const OwnerEncryptionKey = brand("OwnerEncryptionKey", EncryptionKey);
8089
export type OwnerEncryptionKey = typeof OwnerEncryptionKey.Type;
8190

@@ -86,6 +95,16 @@ export type OwnerEncryptionKey = typeof OwnerEncryptionKey.Type;
8695
export const OwnerWriteKey = brand("OwnerWriteKey", Entropy16);
8796
export type OwnerWriteKey = typeof OwnerWriteKey.Type;
8897

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+
89108
/**
90109
* 32 bytes of cryptographic entropy used to derive {@link Owner} keys.
91110
*
@@ -107,22 +126,11 @@ export const ownerSecretToMnemonic = (secret: OwnerSecret): Mnemonic =>
107126
export const mnemonicToOwnerSecret = (mnemonic: Mnemonic): OwnerSecret =>
108127
bip39.mnemonicToEntropy(mnemonic, wordlist) as OwnerSecret;
109128

110-
/** Creates a randomly generated {@link OwnerWriteKey}. */
111-
export const createOwnerWriteKey = (deps: RandomBytesDep): OwnerWriteKey =>
112-
deps.randomBytes.create(16) as OwnerWriteKey;
113-
114129
/**
115130
* Creates an {@link Owner} from a {@link OwnerSecret} using SLIP-21 key
116131
* derivation.
117-
*
118-
* This is an internal helper function, use:
119-
*
120-
* - {@link createAppOwner}
121-
* - {@link createShardOwner}
122-
* - {@link createSharedOwner}
123-
* - {@link createSharedReadonlyOwner}
124132
*/
125-
export const createOwner = (secret: OwnerSecret): Owner => ({
133+
const createOwner = (secret: OwnerSecret): Owner => ({
126134
id: ownerIdBytesToOwnerId(
127135
OwnerIdBytes.orThrow(
128136
createSlip21(secret, ["Evolu", "OwnerIdBytes"]).slice(0, 16),
@@ -182,9 +190,9 @@ export interface AppOwner extends Owner {
182190

183191
/** Creates an {@link AppOwner} from an {@link OwnerSecret}. */
184192
export const createAppOwner = (secret: OwnerSecret): AppOwner => ({
193+
...createOwner(secret),
185194
type: "AppOwner",
186195
mnemonic: ownerSecretToMnemonic(secret),
187-
...createOwner(secret),
188196
});
189197

190198
/**
@@ -200,18 +208,13 @@ export const createAppOwner = (secret: OwnerSecret): AppOwner => ({
200208
*/
201209
export interface ShardOwner extends Owner {
202210
readonly type: "ShardOwner";
203-
readonly transports?: ReadonlyArray<OwnerTransport>;
204211
}
205212

206213
/** 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 => {
211215
return {
212-
type: "ShardOwner",
213216
...createOwner(secret),
214-
...(transports && { transports }),
217+
type: "ShardOwner",
215218
};
216219
};
217220

@@ -236,21 +239,18 @@ export const createShardOwner = (
236239
export const deriveShardOwner = (
237240
owner: AppOwner,
238241
path: NonEmptyReadonlyArray<string | number>,
239-
transports?: ReadonlyArray<OwnerTransport>,
240242
): ShardOwner => {
241243
const secret = createSlip21(owner.encryptionKey, path) as OwnerSecret;
242244

243245
return {
244-
type: "ShardOwner",
245246
...createOwner(secret),
246-
...(transports && { transports }),
247+
type: "ShardOwner",
247248
};
248249
};
249250

250251
/** An {@link Owner} for collaborative data with write access. */
251252
export interface SharedOwner extends Owner {
252253
readonly type: "SharedOwner";
253-
readonly transports?: ReadonlyArray<OwnerTransport>;
254254
}
255255

256256
/**
@@ -260,27 +260,18 @@ export interface SharedOwner extends Owner {
260260
* Use {@link createSharedReadonlyOwner} to create a read-only version for
261261
* sharing.
262262
*/
263-
export const createSharedOwner = (
264-
secret: OwnerSecret,
265-
transports?: ReadonlyArray<OwnerTransport>,
266-
): SharedOwner => {
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+
});
273267

274268
/**
275269
* Read-only version of a {@link SharedOwner} for data sharing. Contains only the
276270
* {@link OwnerId} and {@link EncryptionKey} needed for others to read the shared
277271
* data without write access.
278272
*/
279-
export interface SharedReadonlyOwner {
273+
export interface SharedReadonlyOwner extends ReadonlyOwner {
280274
readonly type: "SharedReadonlyOwner";
281-
readonly id: OwnerId;
282-
readonly encryptionKey: EncryptionKey;
283-
readonly transports?: ReadonlyArray<OwnerTransport>;
284275
}
285276

286277
/** Creates a {@link SharedReadonlyOwner} from a {@link SharedOwner}. */
@@ -290,7 +281,6 @@ export const createSharedReadonlyOwner = (
290281
type: "SharedReadonlyOwner",
291282
id: sharedOwner.id,
292283
encryptionKey: sharedOwner.encryptionKey,
293-
...(sharedOwner.transports && { transports: sharedOwner.transports }),
294284
});
295285

296286
/**
@@ -383,8 +373,8 @@ export const parseOwnerIdFromOwnerWebSocketTransportUrl = (
383373
url: string,
384374
): OwnerId | null => getOrNull(OwnerId.fromUnknown(url.split("=")[1]));
385375

386-
/** Base interface for all owner errors. */
387-
export interface BaseOwnerError {
376+
/** Common interface implemented by all owner domain errors. */
377+
export interface OwnerError {
388378
readonly ownerId: OwnerId;
389379
}
390380

packages/common/src/local-first/Protocol.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ import {
221221
} from "../Type.js";
222222
import { Predicate } from "../Types.js";
223223
import {
224-
BaseOwnerError,
224+
OwnerError,
225225
Owner,
226226
OwnerId,
227227
OwnerIdBytes,
@@ -382,7 +382,7 @@ export type ProtocolError =
382382
* Represents a version mismatch in the Evolu Protocol. Occurs when the
383383
* initiator and non-initiator are using incompatible protocol versions.
384384
*/
385-
export interface ProtocolVersionError extends BaseOwnerError {
385+
export interface ProtocolVersionError extends OwnerError {
386386
readonly type: "ProtocolVersionError";
387387
readonly version: NonNegativeInt;
388388
/** Indicates which side is obsolete and should update. */
@@ -397,15 +397,15 @@ export interface ProtocolInvalidDataError {
397397
}
398398

399399
/** Error when a {@link OwnerWriteKey} is invalid, missing, or fails validation. */
400-
export interface ProtocolWriteKeyError extends BaseOwnerError {
400+
export interface ProtocolWriteKeyError extends OwnerError {
401401
readonly type: "ProtocolWriteKeyError";
402402
}
403403

404404
/**
405405
* Error indicating a serious relay-side write failure. Clients should log this
406406
* error and show a generic sync error to the user.
407407
*/
408-
export interface ProtocolWriteError extends BaseOwnerError {
408+
export interface ProtocolWriteError extends OwnerError {
409409
readonly type: "ProtocolWriteError";
410410
}
411411

@@ -422,15 +422,15 @@ export interface ProtocolWriteError extends BaseOwnerError {
422422
* plan. Quota monitoring and management is the relay provider's
423423
* responsibility.
424424
*/
425-
export interface ProtocolQuotaError extends BaseOwnerError {
425+
export interface ProtocolQuotaError extends OwnerError {
426426
readonly type: "ProtocolQuotaError";
427427
}
428428

429429
/**
430430
* Error indicating a serious relay-side synchronization failure. Clients should
431431
* log this error and show a generic sync error to the user.
432432
*/
433-
export interface ProtocolSyncError extends BaseOwnerError {
433+
export interface ProtocolSyncError extends OwnerError {
434434
readonly type: "ProtocolSyncError";
435435
}
436436

packages/common/src/local-first/Public.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
export { createEvolu } from "./Evolu.js";
88
export type { Evolu, EvoluConfig, EvoluDeps, EvoluError } from "./Evolu.js";
9+
export type { UnuseOwner } from "./Evolu.js";
910
export * from "./LocalAuth.js";
1011
export * from "./Owner.js";
1112
export * as kysely from "./PublicKysely.js";

packages/common/src/local-first/Storage.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import {
2626
TypeError,
2727
} from "../Type.js";
2828
import {
29-
BaseOwnerError,
29+
OwnerError,
3030
Owner,
3131
OwnerId,
3232
OwnerIdBytes,
@@ -172,12 +172,12 @@ export interface StorageDep {
172172
}
173173

174174
/** Error indicating a serious write failure. */
175-
export interface StorageWriteError extends BaseOwnerError {
175+
export interface StorageWriteError extends OwnerError {
176176
readonly type: "StorageWriteError";
177177
}
178178

179179
/** Error when storage or billing quota is exceeded. */
180-
export interface StorageQuotaError extends BaseOwnerError {
180+
export interface StorageQuotaError extends OwnerError {
181181
readonly type: "StorageQuotaError";
182182
}
183183

0 commit comments

Comments
 (0)