From 824d08aa56197e578b74fa48455cdd4421bd1da5 Mon Sep 17 00:00:00 2001 From: BrianHung Date: Tue, 24 Sep 2024 12:34:01 -0700 Subject: [PATCH 1/6] allow for initial id in repo create --- packages/automerge-repo/src/Repo.ts | 12 +++++++----- packages/automerge-repo/test/Repo.test.ts | 22 +++++++++++++++++++++- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/packages/automerge-repo/src/Repo.ts b/packages/automerge-repo/src/Repo.ts index 09342f09b..bf12445b8 100644 --- a/packages/automerge-repo/src/Repo.ts +++ b/packages/automerge-repo/src/Repo.ts @@ -340,12 +340,14 @@ export class Repo extends EventEmitter { /** * Creates a new document and returns a handle to it. The initial value of the document is an - * empty object `{}` unless an initial value is provided. Its documentId is generated by the - * system. we emit a `document` event to advertise interest in the document. + * empty object `{}` unless an initial value is provided. If an id is not provided, the system + * will generate a unique id. We emit a `document` event to advertise interest in the document. + * + * The `id` parameter should be sufficiently random or unique to avoid conflicts with existing documents. + * Alternatively, it can match an existing id, but the initial value must share ancestry with that document. */ - create(initialValue?: T): DocHandle { - // Generate a new UUID and store it in the buffer - const { documentId } = parseAutomergeUrl(generateAutomergeUrl()) + create(initialValue?: T, id: AnyDocumentId = generateAutomergeUrl()): DocHandle { + const documentId = interpretAsDocumentId(id) const handle = this.#getHandle({ documentId, }) as DocHandle diff --git a/packages/automerge-repo/test/Repo.test.ts b/packages/automerge-repo/test/Repo.test.ts index f2d12f3ed..7d6e4f7b1 100644 --- a/packages/automerge-repo/test/Repo.test.ts +++ b/packages/automerge-repo/test/Repo.test.ts @@ -3,7 +3,7 @@ import { MessageChannelNetworkAdapter } from "../../automerge-repo-network-messa import assert from "assert" import * as Uuid from "uuid" import { describe, expect, it } from "vitest" -import { parseAutomergeUrl } from "../src/AutomergeUrl.js" +import { interpretAsDocumentId, parseAutomergeUrl } from "../src/AutomergeUrl.js" import { generateAutomergeUrl, stringifyAutomergeUrl, @@ -76,6 +76,26 @@ describe("Repo", () => { assert.equal(handle.docSync().foo, "bar") }) + it("can create a document with an initial id", async () => { + const { repo } = setup() + const docId = interpretAsDocumentId(Uuid.v4() as LegacyDocumentId) + const docUrl = stringifyAutomergeUrl(docId) + const handle = repo.create({ foo: "bar" }, docId); + await handle.doc() + assert.equal(handle.documentId, docId) + assert.equal(handle.url, docUrl) + }) + + it("throws an error if we try to create a handle with an invalid id", async () => { + const { repo } = setup() + const docId = "invalid-url" as unknown as AutomergeUrl + try { + repo.create({ foo: "bar" }, docId); + } catch (e: any) { + assert.equal(e.message, "Invalid AutomergeUrl: 'invalid-url'") + } + }) + it("can find a document by url", () => { const { repo } = setup() const handle = repo.create() From 3476246535515c2013e38bad7ed883521c335001 Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Tue, 24 Sep 2024 15:31:06 -0700 Subject: [PATCH 2/6] support id for import --- packages/automerge-repo/src/Repo.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/automerge-repo/src/Repo.ts b/packages/automerge-repo/src/Repo.ts index bf12445b8..9b47c4ffd 100644 --- a/packages/automerge-repo/src/Repo.ts +++ b/packages/automerge-repo/src/Repo.ts @@ -493,10 +493,10 @@ export class Repo extends EventEmitter { * Imports document binary into the repo. * @param binary - The binary to import */ - import(binary: Uint8Array) { + import(binary: Uint8Array, id: AnyDocumentId = generateAutomergeUrl()) { const doc = Automerge.load(binary) - const handle = this.create() + const handle = this.create(undefined, id) handle.update(() => { return Automerge.clone(doc) From d7cf48b9c721dac3ba73d89b6a6658adfe9b584b Mon Sep 17 00:00:00 2001 From: Brian Hung Date: Tue, 24 Sep 2024 15:45:22 -0700 Subject: [PATCH 3/6] throw error if handle with id already exists --- packages/automerge-repo/src/Repo.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/automerge-repo/src/Repo.ts b/packages/automerge-repo/src/Repo.ts index 9b47c4ffd..e9f4f923b 100644 --- a/packages/automerge-repo/src/Repo.ts +++ b/packages/automerge-repo/src/Repo.ts @@ -348,6 +348,10 @@ export class Repo extends EventEmitter { */ create(initialValue?: T, id: AnyDocumentId = generateAutomergeUrl()): DocHandle { const documentId = interpretAsDocumentId(id) + if (this.#handleCache[documentId]) { + throw new Error(`A handle with id ${id} already exists.`) + } + const handle = this.#getHandle({ documentId, }) as DocHandle From c1121b5cbf5992a868d8c7b088da35dba6da610f Mon Sep 17 00:00:00 2001 From: BrianHung Date: Tue, 24 Sep 2024 16:53:43 -0700 Subject: [PATCH 4/6] add test case for existing id --- packages/automerge-repo/test/Repo.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/automerge-repo/test/Repo.test.ts b/packages/automerge-repo/test/Repo.test.ts index 7d6e4f7b1..ef1a68be0 100644 --- a/packages/automerge-repo/test/Repo.test.ts +++ b/packages/automerge-repo/test/Repo.test.ts @@ -96,6 +96,17 @@ describe("Repo", () => { } }) + it("throws an error if we try to create a handle with an existing id", async () => { + const { repo } = setup() + const handle = repo.create({ foo: "bar" }) + const docId = handle.url + try { + repo.create({ foo: "bar" }, docId); + } catch (e: any) { + assert.equal(e.message, `A handle with that id already exists: ${docId}`) + } + }) + it("can find a document by url", () => { const { repo } = setup() const handle = repo.create() From 2227886a914b468ca46bc695a54bdaf85b7f4ccd Mon Sep 17 00:00:00 2001 From: BrianHung Date: Tue, 24 Sep 2024 16:54:13 -0700 Subject: [PATCH 5/6] support id for clone --- packages/automerge-repo/src/Repo.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/automerge-repo/src/Repo.ts b/packages/automerge-repo/src/Repo.ts index e9f4f923b..3a0c31ee0 100644 --- a/packages/automerge-repo/src/Repo.ts +++ b/packages/automerge-repo/src/Repo.ts @@ -344,12 +344,12 @@ export class Repo extends EventEmitter { * will generate a unique id. We emit a `document` event to advertise interest in the document. * * The `id` parameter should be sufficiently random or unique to avoid conflicts with existing documents. - * Alternatively, it can match an existing id, but the initial value must share ancestry with that document. + * Use `clone` or `import` to create a handle from an existing document with shared ancestry. */ create(initialValue?: T, id: AnyDocumentId = generateAutomergeUrl()): DocHandle { const documentId = interpretAsDocumentId(id) if (this.#handleCache[documentId]) { - throw new Error(`A handle with id ${id} already exists.`) + throw new Error(`A handle with that id already exists: ${id}`) } const handle = this.#getHandle({ @@ -387,7 +387,7 @@ export class Repo extends EventEmitter { * @throws if the cloned handle is not yet ready or if * `clonedHandle.docSync()` returns `undefined` (i.e. the handle is unavailable). */ - clone(clonedHandle: DocHandle) { + clone(clonedHandle: DocHandle, id?: AnyDocumentId) { if (!clonedHandle.isReady()) { throw new Error( `Cloned handle is not yet in ready state. @@ -400,7 +400,7 @@ export class Repo extends EventEmitter { throw new Error("Cloned handle doesn't have a document.") } - const handle = this.create() + const handle = this.create(undefined, id) handle.update(() => { // we replace the document with the new cloned one @@ -497,7 +497,7 @@ export class Repo extends EventEmitter { * Imports document binary into the repo. * @param binary - The binary to import */ - import(binary: Uint8Array, id: AnyDocumentId = generateAutomergeUrl()) { + import(binary: Uint8Array, id?: AnyDocumentId) { const doc = Automerge.load(binary) const handle = this.create(undefined, id) From bc7840ad33ba09617b6e66bfeb6781ec9c46961e Mon Sep 17 00:00:00 2001 From: Peter van Hardenberg Date: Tue, 24 Sep 2024 17:27:12 -0700 Subject: [PATCH 6/6] iterate the docstrings a little and add a failing test --- packages/automerge-repo/src/Repo.ts | 77 +++++++++++++++-------- packages/automerge-repo/test/Repo.test.ts | 35 ++++++++--- 2 files changed, 78 insertions(+), 34 deletions(-) diff --git a/packages/automerge-repo/src/Repo.ts b/packages/automerge-repo/src/Repo.ts index 3a0c31ee0..92a6b946b 100644 --- a/packages/automerge-repo/src/Repo.ts +++ b/packages/automerge-repo/src/Repo.ts @@ -1,11 +1,7 @@ import { next as Automerge } from "@automerge/automerge/slim" import debug from "debug" import { EventEmitter } from "eventemitter3" -import { - generateAutomergeUrl, - interpretAsDocumentId, - parseAutomergeUrl, -} from "./AutomergeUrl.js" +import { generateAutomergeUrl, interpretAsDocumentId } from "./AutomergeUrl.js" import { DELETED, DocHandle, @@ -34,13 +30,22 @@ function randomPeerId() { return ("peer-" + Math.random().toString(36).slice(4)) as PeerId } -/** A Repo is a collection of documents with networking, syncing, and storage capabilities. */ -/** The `Repo` is the main entry point of this library +/** + * A Repo (short for repository) manages a collection of documents. + * + * You can use this object to find, create, and delete documents, and to + * as well as to import and export documents to and from binary format. + * + * A Repo has a {@link StorageSubsystem} and a {@link NetworkSubsystem}. + * During initialization you may provide a {@link StorageAdapter} and zero or + * more {@link NetworkAdapter}s. + * + * @param {RepoConfig} config - Configuration options for the Repo + * + * @emits Repo#document - When a new document is created or discovered + * @emits Repo#delete-document - When a document is deleted + * @emits Repo#unavailable-document - When a document is marked as unavailable * - * @remarks - * To construct a `Repo` you will need an {@link StorageAdapter} and one or - * more {@link NetworkAdapter}s. Once you have a `Repo` you can use it to - * obtain {@link DocHandle}s. */ export class Repo extends EventEmitter { #log: debug.Debugger @@ -340,13 +345,20 @@ export class Repo extends EventEmitter { /** * Creates a new document and returns a handle to it. The initial value of the document is an - * empty object `{}` unless an initial value is provided. If an id is not provided, the system - * will generate a unique id. We emit a `document` event to advertise interest in the document. - * - * The `id` parameter should be sufficiently random or unique to avoid conflicts with existing documents. - * Use `clone` or `import` to create a handle from an existing document with shared ancestry. + * empty object `{}` unless an initial value is provided. + * + * @see Repo#clone to create an independent copy of a handle. + * @see Repo#import to load data from a Uint8Array. + * + * @param [initialValue] - A value to initialize the document with + * @param [id] - A universally unique documentId **Caution!** ID reuse will lead to data corruption. + * @emits Repo#document + * @throws If a handle with the same id already exists */ - create(initialValue?: T, id: AnyDocumentId = generateAutomergeUrl()): DocHandle { + create( + initialValue?: T, + id: AnyDocumentId = generateAutomergeUrl() + ): DocHandle { const documentId = interpretAsDocumentId(id) if (this.#handleCache[documentId]) { throw new Error(`A handle with that id already exists: ${id}`) @@ -372,9 +384,8 @@ export class Repo extends EventEmitter { return handle } - /** Create a new DocHandle by cloning the history of an existing DocHandle. - * - * @param clonedHandle - The handle to clone + /** + * Create a new DocHandle by cloning the history of an existing DocHandle. * * @remarks This is a wrapper around the `clone` function in the Automerge library. * The new `DocHandle` will have a new URL but will share history with the original, @@ -384,8 +395,11 @@ export class Repo extends EventEmitter { * Any peers this `Repo` is connected to for whom `sharePolicy` returns `true` will * be notified of the newly created DocHandle. * - * @throws if the cloned handle is not yet ready or if - * `clonedHandle.docSync()` returns `undefined` (i.e. the handle is unavailable). + * @param clonedHandle - The handle to clone + * @param [id] - A universally unique documentId **Caution!** ID reuse will lead to data corruption. + * @emits Repo#document + * @throws if the source handle is not yet ready + * */ clone(clonedHandle: DocHandle, id?: AnyDocumentId) { if (!clonedHandle.isReady()) { @@ -413,12 +427,15 @@ export class Repo extends EventEmitter { /** * Retrieves a document by id. It gets data from the local system, but also emits a `document` * event to advertise interest in the document. + * + * @param documentUrl - The url or documentId of the handle to retrieve + * @emits Repo#document */ find( /** The url or documentId of the handle to retrieve */ - id: AnyDocumentId + documentUrl: AnyDocumentId ): DocHandle { - const documentId = interpretAsDocumentId(id) + const documentId = interpretAsDocumentId(documentUrl) // If we have the handle cached, return it if (this.#handleCache[documentId]) { @@ -464,6 +481,14 @@ export class Repo extends EventEmitter { return handle } + /** + * Removes a document from the local repo. + * + * @remarks This does not delete the document from the network or from other peers' local storage. + * + * @param documentUrl - The url or documentId of the handle to retrieve + * @emits Repo#delete-document + */ delete( /** The url or documentId of the handle to delete */ id: AnyDocumentId @@ -479,7 +504,8 @@ export class Repo extends EventEmitter { /** * Exports a document to a binary format. - * @param id - The url or documentId of the handle to export + * + * @param documentUrl - The url or documentId of the handle to export * * @returns Promise - A Promise containing the binary document, * or undefined if the document is unavailable. @@ -499,7 +525,6 @@ export class Repo extends EventEmitter { */ import(binary: Uint8Array, id?: AnyDocumentId) { const doc = Automerge.load(binary) - const handle = this.create(undefined, id) handle.update(() => { diff --git a/packages/automerge-repo/test/Repo.test.ts b/packages/automerge-repo/test/Repo.test.ts index ef1a68be0..3e5e98958 100644 --- a/packages/automerge-repo/test/Repo.test.ts +++ b/packages/automerge-repo/test/Repo.test.ts @@ -3,7 +3,10 @@ import { MessageChannelNetworkAdapter } from "../../automerge-repo-network-messa import assert from "assert" import * as Uuid from "uuid" import { describe, expect, it } from "vitest" -import { interpretAsDocumentId, parseAutomergeUrl } from "../src/AutomergeUrl.js" +import { + interpretAsDocumentId, + parseAutomergeUrl, +} from "../src/AutomergeUrl.js" import { generateAutomergeUrl, stringifyAutomergeUrl, @@ -80,7 +83,7 @@ describe("Repo", () => { const { repo } = setup() const docId = interpretAsDocumentId(Uuid.v4() as LegacyDocumentId) const docUrl = stringifyAutomergeUrl(docId) - const handle = repo.create({ foo: "bar" }, docId); + const handle = repo.create({ foo: "bar" }, docId) await handle.doc() assert.equal(handle.documentId, docId) assert.equal(handle.url, docUrl) @@ -90,7 +93,7 @@ describe("Repo", () => { const { repo } = setup() const docId = "invalid-url" as unknown as AutomergeUrl try { - repo.create({ foo: "bar" }, docId); + repo.create({ foo: "bar" }, docId) } catch (e: any) { assert.equal(e.message, "Invalid AutomergeUrl: 'invalid-url'") } @@ -100,11 +103,27 @@ describe("Repo", () => { const { repo } = setup() const handle = repo.create({ foo: "bar" }) const docId = handle.url - try { - repo.create({ foo: "bar" }, docId); - } catch (e: any) { - assert.equal(e.message, `A handle with that id already exists: ${docId}`) - } + expect(() => { + repo.create({ foo: "bar" }, docId) + }).toThrow() + }) + + it("throws an error if we try to create a handle with an existing id that isn't loaded yet", async () => { + const { repo, storageAdapter, networkAdapter } = setup() + + // we simulate a document that exists in storage but hasn't been loaded yet + // by writing it to the storage adapter with a different repo + const writerRepo = new Repo({ + storage: storageAdapter, + network: [networkAdapter], + }) + const handle = writerRepo.create({ foo: "bar" }) + + const docId = handle.url + + expect(() => { + repo.create({ foo: "bar" }, docId) + }).toThrow() }) it("can find a document by url", () => {