loro-repo 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,7 +9,7 @@ LoroRepo is the collection-sync layer that sits above Flock. It keeps document m
9
9
  ## What you get
10
10
 
11
11
  - **Metadata-first coordination** – `repo.listDoc()` and `repo.watch()` expose LWW metadata so UIs can render collections before bodies arrive.
12
- - **On-demand documents** – `openCollaborativeDoc()` gives you a repo-managed `LoroDoc` that persists and syncs automatically; `openDetachedDoc()` is a read-only snapshot.
12
+ - **On-demand documents** – `openPersistedDoc()` hands back a repo-managed `LoroDoc` that persists locally and can sync once or join live rooms; `openDetachedDoc()` is a read-only snapshot.
13
13
  - **Binary asset orchestration** – `linkAsset()`/`fetchAsset()` dedupe SHA-256 addressed blobs across docs, while `gcAssets()` sweeps unreferenced payloads.
14
14
  - **Pluggable adapters** – supply your own `TransportAdapter`, `StorageAdapter`, and `AssetTransportAdapter` (or use the built-ins below) to target servers, CF Durable Objects, or local-first meshes.
15
15
  - **Consistent events** – every event includes `by: "local" | "sync" | "live"` so you can react differently to local edits, explicit sync pulls, or realtime merges.
@@ -22,36 +22,37 @@ import {
22
22
  BroadcastChannelTransportAdapter,
23
23
  IndexedDBStorageAdaptor,
24
24
  } from "loro-repo";
25
- import { LoroDoc } from "loro-crdt";
26
25
 
27
26
  type DocMeta = { title?: string; tags?: string[] };
28
27
 
29
- const repo = new LoroRepo<DocMeta>({
28
+ const repo = await LoroRepo.create<DocMeta>({
30
29
  transportAdapter: new BroadcastChannelTransportAdapter({ namespace: "notes" }),
31
30
  storageAdapter: new IndexedDBStorageAdaptor({ dbName: "notes-db" }),
32
- docFactory: async () => new LoroDoc(),
33
31
  });
34
32
 
35
- await repo.ready();
36
33
  await repo.sync({ scope: "meta" }); // metadata-first
37
34
 
38
35
  await repo.upsertDocMeta("note:welcome", { title: "Welcome" });
39
36
 
40
- const handle = await repo.openCollaborativeDoc("note:welcome");
41
- await handle.whenSyncedWithRemote;
37
+ const handle = await repo.openPersistedDoc("note:welcome");
38
+ await handle.syncOnce(); // optional: fetch body once
39
+ const room = await handle.joinRoom(); // optional: live updates
42
40
  handle.doc.getText("content").insert(0, "Hello from LoroRepo");
43
41
  handle.doc.commit();
44
- await handle.close();
42
+ room.unsubscribe();
43
+ await repo.unloadDoc("note:welcome");
45
44
  ```
46
45
 
47
46
  ## Using the API
48
47
 
49
- - **Define your metadata contract** once via `new LoroRepo<Meta>()`. All metadata helpers (`upsertDocMeta`, `getDocMeta`, `listDoc`, `watch`) stay type-safe.
48
+ - **Create a repo** with `await LoroRepo.create<Meta>({ transportAdapter?, storageAdapter?, assetTransportAdapter?, docFrontierDebounceMs? })`; metadata is hydrated automatically.
49
+ - **Define your metadata contract** once via the generic `Meta`. All metadata helpers (`upsertDocMeta`, `getDocMeta`, `listDoc`, `watch`) stay type-safe.
50
50
  - **Choose sync lanes** with `repo.sync({ scope: "meta" | "doc" | "full", docIds?: string[] })` to pull remote changes on demand.
51
+ - **Work with documents** using `openPersistedDoc(docId)` for repo-managed docs (persisted snapshots + frontier tracking) and `openDetachedDoc(docId)` for isolated snapshots; call `joinDocRoom`/`handle.joinRoom` for live sync, or `unloadDoc`/`flush` to persist and drop cached docs.
51
52
  - **Join realtime rooms** by calling `joinMetaRoom()` / `joinDocRoom(docId)`; the behaviour depends entirely on the transport adapter you injected.
52
53
  - **Manage assets** through `linkAsset`, `uploadAsset`, `fetchAsset` (alias `ensureAsset`), `listAssets`, and `gcAssets({ minKeepMs })`.
53
54
  - **React to changes** by subscribing with `repo.watch(listener, { docIds, kinds, metadataFields, by })`.
54
- - **Shut down cleanly** via `await repo.close()` to flush snapshots and dispose adapters.
55
+ - **Shut down cleanly** via `await repo.destroy()` to flush snapshots and dispose adapters.
55
56
 
56
57
  ## Built-in adapters
57
58
 
@@ -83,22 +84,22 @@ await handle.close();
83
84
  ## Core API surface
84
85
 
85
86
  **Lifecycle**
86
- - `new LoroRepo<Meta>(options)` – wire adapters (`transportAdapter`, `storageAdapter`, `assetTransportAdapter`, `docFactory`, `docFrontierDebounceMs`).
87
- - `await repo.ready()` – hydrate metadata snapshot before touching docs.
87
+ - `await LoroRepo.create<Meta>({ transportAdapter?, storageAdapter?, assetTransportAdapter?, docFrontierDebounceMs? })` – hydrate metadata and initialise adapters.
88
88
  - `await repo.sync({ scope: "meta" | "doc" | "full", docIds?: string[] })` – pull remote updates on demand.
89
- - `await repo.close()` – persist pending work and dispose adapters.
89
+ - `await repo.destroy()` – persist pending work and dispose adapters.
90
90
 
91
91
  **Metadata**
92
92
  - `await repo.upsertDocMeta(docId, patch)` – LWW merge with your `Meta` type.
93
93
  - `await repo.getDocMeta(docId)` – clone the stored metadata (or `undefined`).
94
94
  - `await repo.listDoc(query?)` – list docs by prefix/range/limit (`RepoDocMeta<Meta>[]`).
95
- - `repo.getMetaReplica()` – access raw `Flock` if you need advanced scans.
95
+ - `repo.getMeta()` – access raw `Flock` if you need advanced scans.
96
96
 
97
97
  **Documents**
98
- - `await repo.openCollaborativeDoc(docId)` – returns `{ doc, whenSyncedWithRemote, close }`; mutations persist and sync automatically.
98
+ - `await repo.openPersistedDoc(docId)` – returns `{ doc, syncOnce, joinRoom }`; mutations persist locally and frontiers are written to metadata.
99
99
  - `await repo.openDetachedDoc(docId)` – isolated snapshot handle (no persistence, no live sync) ideal for read-only tasks.
100
- - `await repo.whenDocInSyncWithRemote(docId)` – ensure a document caught up without opening it.
101
- - `await repo.joinDocRoom(docId, params?)` – spawn a realtime session through your transport; use `subscription.unsubscribe()` when done.
100
+ - `await repo.joinDocRoom(docId, params?)` or `await handle.joinRoom(auth?)` spawn a realtime session through your transport; use `subscription.unsubscribe()` when done.
101
+ - `await repo.unloadDoc(docId)` – flush pending work for a doc and evict it from memory.
102
+ - `await repo.flush()` – persist all loaded docs and flush pending frontier updates.
102
103
 
103
104
  **Assets**
104
105
  - `await repo.linkAsset(docId, { content, mime?, tag?, policy?, assetId?, createdAt? })` – upload + link, returning the SHA-256 assetId.
package/dist/index.cjs CHANGED
@@ -6,16 +6,12 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
6
6
  var __getProtoOf = Object.getPrototypeOf;
7
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
8
8
  var __copyProps = (to, from, except, desc) => {
9
- if (from && typeof from === "object" || typeof from === "function") {
10
- for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
- key = keys[i];
12
- if (!__hasOwnProp.call(to, key) && key !== except) {
13
- __defProp(to, key, {
14
- get: ((k) => from[k]).bind(null, key),
15
- enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
- });
17
- }
18
- }
9
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
10
+ key = keys[i];
11
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
12
+ get: ((k) => from[k]).bind(null, key),
13
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
14
+ });
19
15
  }
20
16
  return to;
21
17
  };
@@ -26,18 +22,27 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
22
 
27
23
  //#endregion
28
24
  let __loro_dev_flock = require("@loro-dev/flock");
29
- let loro_adaptors = require("loro-adaptors");
25
+ __loro_dev_flock = __toESM(__loro_dev_flock);
26
+ let loro_adaptors_loro = require("loro-adaptors/loro");
27
+ loro_adaptors_loro = __toESM(loro_adaptors_loro);
30
28
  let loro_protocol = require("loro-protocol");
29
+ loro_protocol = __toESM(loro_protocol);
31
30
  let loro_websocket = require("loro-websocket");
31
+ loro_websocket = __toESM(loro_websocket);
32
32
  let loro_crdt = require("loro-crdt");
33
+ loro_crdt = __toESM(loro_crdt);
34
+ let loro_adaptors_flock = require("loro-adaptors/flock");
35
+ loro_adaptors_flock = __toESM(loro_adaptors_flock);
33
36
  let node_fs = require("node:fs");
37
+ node_fs = __toESM(node_fs);
34
38
  let node_path = require("node:path");
35
39
  node_path = __toESM(node_path);
36
40
  let node_crypto = require("node:crypto");
41
+ node_crypto = __toESM(node_crypto);
37
42
 
38
43
  //#region src/loro-adaptor.ts
39
44
  function createRepoFlockAdaptorFromDoc(flock, config = {}) {
40
- return new loro_adaptors.FlockAdaptor(flock, config);
45
+ return new loro_adaptors_flock.FlockAdaptor(flock, config);
41
46
  }
42
47
 
43
48
  //#endregion
@@ -391,7 +396,7 @@ var WebSocketTransportAdapter = class {
391
396
  });
392
397
  await this.leaveDocSession(docId).catch(() => {});
393
398
  }
394
- const adaptor = new loro_adaptors.LoroAdaptor(doc);
399
+ const adaptor = new loro_adaptors_loro.LoroAdaptor(doc);
395
400
  debug("joining doc room", {
396
401
  docId,
397
402
  roomId,
@@ -1895,13 +1900,11 @@ var AssetManager = class {
1895
1900
  if (params.assetId && params.assetId !== assetId) throw new Error("Provided assetId does not match content digest");
1896
1901
  const existing = this.assets.get(assetId);
1897
1902
  if (existing) {
1898
- if (!existing.data) {
1899
- const clone = bytes.slice();
1900
- existing.data = clone;
1901
- if (this.storage) await this.storage.save({
1903
+ if (this.storage) {
1904
+ if (!await this.storage.loadAsset(assetId)) await this.storage.save({
1902
1905
  type: "asset",
1903
1906
  assetId,
1904
- data: clone.slice()
1907
+ data: bytes.slice()
1905
1908
  });
1906
1909
  }
1907
1910
  let metadataMutated = false;
@@ -1928,7 +1931,7 @@ var AssetManager = class {
1928
1931
  await this.persistMeta();
1929
1932
  this.eventBus.emit({
1930
1933
  kind: "asset-metadata",
1931
- asset: this.createAssetDownload(assetId, metadata$1, existing.data),
1934
+ asset: this.createAssetDownload(assetId, metadata$1, bytes),
1932
1935
  by: "local"
1933
1936
  });
1934
1937
  }
@@ -1958,7 +1961,8 @@ var AssetManager = class {
1958
1961
  assetId,
1959
1962
  data: storedBytes.slice()
1960
1963
  });
1961
- this.rememberAsset(metadata, storedBytes);
1964
+ this.rememberAsset(metadata);
1965
+ this.markAssetAsOrphan(assetId, metadata);
1962
1966
  this.updateDocAssetMetadata(assetId, metadata);
1963
1967
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
1964
1968
  await this.persistMeta();
@@ -1979,13 +1983,11 @@ var AssetManager = class {
1979
1983
  const existing = this.assets.get(assetId);
1980
1984
  if (existing) {
1981
1985
  metadata = existing.metadata;
1982
- if (!existing.data) {
1983
- const clone = bytes.slice();
1984
- existing.data = clone;
1985
- if (this.storage) await this.storage.save({
1986
+ if (this.storage) {
1987
+ if (!await this.storage.loadAsset(assetId)) await this.storage.save({
1986
1988
  type: "asset",
1987
1989
  assetId,
1988
- data: clone.slice()
1990
+ data: bytes.slice()
1989
1991
  });
1990
1992
  }
1991
1993
  let nextMetadata = metadata;
@@ -2025,11 +2027,11 @@ var AssetManager = class {
2025
2027
  await this.persistMeta();
2026
2028
  this.eventBus.emit({
2027
2029
  kind: "asset-metadata",
2028
- asset: this.createAssetDownload(assetId, metadata, existing.data),
2030
+ asset: this.createAssetDownload(assetId, metadata, bytes),
2029
2031
  by: "local"
2030
2032
  });
2031
2033
  } else metadata = existing.metadata;
2032
- storedBytes = existing.data;
2034
+ storedBytes = bytes;
2033
2035
  this.rememberAsset(metadata);
2034
2036
  } else {
2035
2037
  metadata = {
@@ -2055,7 +2057,7 @@ var AssetManager = class {
2055
2057
  assetId,
2056
2058
  data: storedBytes.slice()
2057
2059
  });
2058
- this.rememberAsset(metadata, storedBytes);
2060
+ this.rememberAsset(metadata);
2059
2061
  this.updateDocAssetMetadata(assetId, metadata);
2060
2062
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
2061
2063
  created = true;
@@ -2063,9 +2065,7 @@ var AssetManager = class {
2063
2065
  const mapping = this.docAssets.get(docId) ?? /* @__PURE__ */ new Map();
2064
2066
  mapping.set(assetId, metadata);
2065
2067
  this.docAssets.set(docId, mapping);
2066
- const refs = this.assetToDocRefs.get(assetId) ?? /* @__PURE__ */ new Set();
2067
- refs.add(docId);
2068
- this.assetToDocRefs.set(assetId, refs);
2068
+ this.addDocReference(assetId, docId);
2069
2069
  this.metaFlock.put([
2070
2070
  "ld",
2071
2071
  docId,
@@ -2095,20 +2095,7 @@ var AssetManager = class {
2095
2095
  docId,
2096
2096
  assetId
2097
2097
  ]);
2098
- const refs = this.assetToDocRefs.get(assetId);
2099
- if (refs) {
2100
- refs.delete(docId);
2101
- if (refs.size === 0) {
2102
- this.assetToDocRefs.delete(assetId);
2103
- const record = this.assets.get(assetId);
2104
- if (record) this.orphanedAssets.set(assetId, {
2105
- metadata: record.metadata,
2106
- deletedAt: Date.now()
2107
- });
2108
- this.metaFlock.delete(["a", assetId]);
2109
- this.assets.delete(assetId);
2110
- }
2111
- }
2098
+ this.removeDocAssetReference(assetId, docId);
2112
2099
  await this.persistMeta();
2113
2100
  this.eventBus.emit({
2114
2101
  kind: "asset-unlink",
@@ -2195,12 +2182,11 @@ var AssetManager = class {
2195
2182
  this.handleAssetRemoval(assetId, by);
2196
2183
  return;
2197
2184
  }
2198
- const existingData = previous?.data;
2199
- this.rememberAsset(metadata, existingData);
2185
+ this.rememberAsset(metadata);
2200
2186
  this.updateDocAssetMetadata(assetId, cloneRepoAssetMetadata(metadata));
2201
2187
  if (!previous || !assetMetadataEqual(previous.metadata, metadata)) this.eventBus.emit({
2202
2188
  kind: "asset-metadata",
2203
- asset: this.createAssetDownload(assetId, metadata, existingData),
2189
+ asset: this.createAssetDownload(assetId, metadata),
2204
2190
  by
2205
2191
  });
2206
2192
  }
@@ -2215,11 +2201,7 @@ var AssetManager = class {
2215
2201
  if (typeof assetId !== "string") continue;
2216
2202
  const metadata = assetMetaFromJson(row.value);
2217
2203
  if (!metadata) continue;
2218
- const existing = this.assets.get(assetId);
2219
- nextAssets.set(assetId, {
2220
- metadata,
2221
- data: existing?.data
2222
- });
2204
+ nextAssets.set(assetId, { metadata });
2223
2205
  }
2224
2206
  const nextDocAssets = /* @__PURE__ */ new Map();
2225
2207
  const linkRows = this.metaFlock.scan({ prefix: ["ld"] });
@@ -2255,12 +2237,18 @@ var AssetManager = class {
2255
2237
  this.assetToDocRefs.set(assetId, refs);
2256
2238
  }
2257
2239
  this.assets.clear();
2258
- for (const record of nextAssets.values()) this.rememberAsset(record.metadata, record.data);
2240
+ for (const record of nextAssets.values()) this.rememberAsset(record.metadata);
2241
+ for (const assetId of nextAssets.keys()) {
2242
+ const refs = this.assetToDocRefs.get(assetId);
2243
+ if (!refs || refs.size === 0) {
2244
+ if (!this.orphanedAssets.has(assetId)) this.markAssetAsOrphan(assetId, nextAssets.get(assetId).metadata);
2245
+ } else this.orphanedAssets.delete(assetId);
2246
+ }
2259
2247
  for (const [assetId, record] of nextAssets) {
2260
2248
  const previous = prevAssets.get(assetId)?.metadata;
2261
2249
  if (!assetMetadataEqual(previous, record.metadata)) this.eventBus.emit({
2262
2250
  kind: "asset-metadata",
2263
- asset: this.createAssetDownload(assetId, record.metadata, record.data),
2251
+ asset: this.createAssetDownload(assetId, record.metadata),
2264
2252
  by
2265
2253
  });
2266
2254
  }
@@ -2356,36 +2344,24 @@ var AssetManager = class {
2356
2344
  };
2357
2345
  }
2358
2346
  async materializeAsset(assetId) {
2359
- let record = this.assets.get(assetId);
2360
- if (record?.data) return {
2361
- metadata: record.metadata,
2362
- bytes: record.data.slice()
2363
- };
2347
+ const record = this.assets.get(assetId);
2364
2348
  if (record && this.storage) {
2365
2349
  const stored = await this.storage.loadAsset(assetId);
2366
- if (stored) {
2367
- const clone = stored.slice();
2368
- record.data = clone.slice();
2369
- return {
2370
- metadata: record.metadata,
2371
- bytes: clone
2372
- };
2373
- }
2350
+ if (stored) return {
2351
+ metadata: record.metadata,
2352
+ bytes: stored
2353
+ };
2374
2354
  }
2375
2355
  if (!record && this.storage) {
2376
2356
  const stored = await this.storage.loadAsset(assetId);
2377
2357
  if (stored) {
2378
2358
  const metadata$1 = this.getAssetMetadata(assetId);
2379
2359
  if (!metadata$1) throw new Error(`Missing metadata for asset ${assetId}`);
2380
- const clone = stored.slice();
2381
- this.assets.set(assetId, {
2382
- metadata: metadata$1,
2383
- data: clone.slice()
2384
- });
2360
+ this.assets.set(assetId, { metadata: metadata$1 });
2385
2361
  this.updateDocAssetMetadata(assetId, metadata$1);
2386
2362
  return {
2387
2363
  metadata: metadata$1,
2388
- bytes: clone
2364
+ bytes: stored
2389
2365
  };
2390
2366
  }
2391
2367
  }
@@ -2401,10 +2377,7 @@ var AssetManager = class {
2401
2377
  ...remote.policy ? { policy: remote.policy } : {},
2402
2378
  ...remote.tag ? { tag: remote.tag } : {}
2403
2379
  };
2404
- this.assets.set(assetId, {
2405
- metadata,
2406
- data: remoteBytes.slice()
2407
- });
2380
+ this.assets.set(assetId, { metadata });
2408
2381
  this.updateDocAssetMetadata(assetId, metadata);
2409
2382
  this.metaFlock.put(["a", assetId], assetMetaToJson(metadata));
2410
2383
  await this.persistMeta();
@@ -2426,18 +2399,14 @@ var AssetManager = class {
2426
2399
  if (assets) assets.set(assetId, metadata);
2427
2400
  }
2428
2401
  }
2429
- rememberAsset(metadata, bytes) {
2430
- const data = bytes ? bytes.slice() : this.assets.get(metadata.assetId)?.data;
2431
- this.assets.set(metadata.assetId, {
2432
- metadata,
2433
- data
2434
- });
2435
- this.orphanedAssets.delete(metadata.assetId);
2402
+ rememberAsset(metadata) {
2403
+ this.assets.set(metadata.assetId, { metadata });
2436
2404
  }
2437
2405
  addDocReference(assetId, docId) {
2438
2406
  const refs = this.assetToDocRefs.get(assetId) ?? /* @__PURE__ */ new Set();
2439
2407
  refs.add(docId);
2440
2408
  this.assetToDocRefs.set(assetId, refs);
2409
+ this.orphanedAssets.delete(assetId);
2441
2410
  }
2442
2411
  removeDocAssetReference(assetId, docId) {
2443
2412
  const refs = this.assetToDocRefs.get(assetId);