diff --git a/packages/automerge-repo/src/Repo.ts b/packages/automerge-repo/src/Repo.ts index 09342f09b..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,12 +345,25 @@ 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. + * + * @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): 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) + if (this.#handleCache[documentId]) { + throw new Error(`A handle with that id already exists: ${id}`) + } + const handle = this.#getHandle({ documentId, }) as DocHandle @@ -366,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, @@ -378,10 +395,13 @@ 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) { + clone(clonedHandle: DocHandle, id?: AnyDocumentId) { if (!clonedHandle.isReady()) { throw new Error( `Cloned handle is not yet in ready state. @@ -394,7 +414,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 @@ -407,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]) { @@ -458,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 @@ -473,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. @@ -491,10 +523,9 @@ 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) { const doc = Automerge.load(binary) - - const handle = this.create() + const handle = this.create(undefined, id) handle.update(() => { return Automerge.clone(doc) diff --git a/packages/automerge-repo/test/Repo.test.ts b/packages/automerge-repo/test/Repo.test.ts index f2d12f3ed..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 { parseAutomergeUrl } from "../src/AutomergeUrl.js" +import { + interpretAsDocumentId, + parseAutomergeUrl, +} from "../src/AutomergeUrl.js" import { generateAutomergeUrl, stringifyAutomergeUrl, @@ -76,6 +79,53 @@ 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("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 + 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", () => { const { repo } = setup() const handle = repo.create()