Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

allow for initial id in repo create #388

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 60 additions & 29 deletions packages/automerge-repo/src/Repo.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<RepoEvents> {
#log: debug.Debugger
Expand Down Expand Up @@ -340,12 +345,25 @@ export class Repo extends EventEmitter<RepoEvents> {

/**
* 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<T>(initialValue?: T): DocHandle<T> {
// Generate a new UUID and store it in the buffer
const { documentId } = parseAutomergeUrl(generateAutomergeUrl())
create<T>(
initialValue?: T,
id: AnyDocumentId = generateAutomergeUrl()
): DocHandle<T> {
const documentId = interpretAsDocumentId(id)
if (this.#handleCache[documentId]) {
throw new Error(`A handle with that id already exists: ${id}`)
}

const handle = this.#getHandle<T>({
documentId,
}) as DocHandle<T>
Expand All @@ -366,9 +384,8 @@ export class Repo extends EventEmitter<RepoEvents> {
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,
Expand All @@ -378,10 +395,13 @@ export class Repo extends EventEmitter<RepoEvents> {
* 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<T>(clonedHandle: DocHandle<T>) {
clone<T>(clonedHandle: DocHandle<T>, id?: AnyDocumentId) {
if (!clonedHandle.isReady()) {
throw new Error(
`Cloned handle is not yet in ready state.
Expand All @@ -394,7 +414,7 @@ export class Repo extends EventEmitter<RepoEvents> {
throw new Error("Cloned handle doesn't have a document.")
}

const handle = this.create<T>()
const handle = this.create<T>(undefined, id)

handle.update(() => {
// we replace the document with the new cloned one
Expand All @@ -407,12 +427,15 @@ export class Repo extends EventEmitter<RepoEvents> {
/**
* 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<T>(
/** The url or documentId of the handle to retrieve */
id: AnyDocumentId
documentUrl: AnyDocumentId
): DocHandle<T> {
const documentId = interpretAsDocumentId(id)
const documentId = interpretAsDocumentId(documentUrl)

// If we have the handle cached, return it
if (this.#handleCache[documentId]) {
Expand Down Expand Up @@ -458,6 +481,14 @@ export class Repo extends EventEmitter<RepoEvents> {
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
Expand All @@ -473,7 +504,8 @@ export class Repo extends EventEmitter<RepoEvents> {

/**
* 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<Uint8Array | undefined> - A Promise containing the binary document,
* or undefined if the document is unavailable.
Expand All @@ -491,10 +523,9 @@ export class Repo extends EventEmitter<RepoEvents> {
* Imports document binary into the repo.
* @param binary - The binary to import
*/
import<T>(binary: Uint8Array) {
import<T>(binary: Uint8Array, id?: AnyDocumentId) {
const doc = Automerge.load<T>(binary)

const handle = this.create<T>()
const handle = this.create<T>(undefined, id)

handle.update(() => {
return Automerge.clone(doc)
Expand Down
52 changes: 51 additions & 1 deletion packages/automerge-repo/test/Repo.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
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,
Expand Down Expand Up @@ -76,6 +79,53 @@
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()

Check failure on line 126 in packages/automerge-repo/test/Repo.test.ts

View workflow job for this annotation

GitHub Actions / Run Tests

packages/automerge-repo/test/Repo.test.ts > Repo > local only > throws an error if we try to create a handle with an existing id that isn't loaded yet

AssertionError: expected [Function] to throw an error ❯ packages/automerge-repo/test/Repo.test.ts:126:10
})

it("can find a document by url", () => {
const { repo } = setup()
const handle = repo.create<TestDoc>()
Expand Down
Loading