Skip to content

Commit

Permalink
allow to create new documents with metadata
Browse files Browse the repository at this point in the history
  • Loading branch information
paulsonnentag committed Jan 12, 2024
1 parent c5d41a0 commit f918cba
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 61 deletions.
24 changes: 15 additions & 9 deletions packages/automerge-repo/src/DocHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,24 +56,29 @@ export class DocHandle<T> //
constructor(
public documentId: DocumentId,
{
isNew = false,
timeoutDelay = 60_000,
changeMetadata = () => undefined,
}: DocHandleOptions = {}
changeMetadata: changeMetadataFunction = () => undefined,
init = false,
}: DocHandleOptions<T> = {}
) {
super()
this.#timeoutDelay = timeoutDelay
this.#changeMetadata = changeMetadata
this.#changeMetadata = changeMetadataFunction
this.#log = debug(`automerge-repo:dochandle:${this.documentId.slice(0, 5)}`)

// initial doc
let doc = A.init<T>()

// Make an empty change so that we have something to save to disk
if (isNew) {
if (init) {
const options = init === true ? {} : init

doc = A.emptyChange(
doc,
optionsWithGlobalMetadata({}, globalMetadataRef?.current ?? {})
optionsWithGlobalMetadata(
options,
this.#changeMetadata(this.documentId) ?? {}
)
)
}

Expand Down Expand Up @@ -226,7 +231,7 @@ export class DocHandle<T> //
})
.start()

this.#machine.send(isNew ? CREATE : FIND)
this.#machine.send(init ? CREATE : FIND)
}

// PRIVATE
Expand Down Expand Up @@ -497,10 +502,11 @@ function optionsWithGlobalMetadata<T>(
// WRAPPER CLASS TYPES

/** @hidden */
export interface DocHandleOptions {
isNew?: boolean
export interface DocHandleOptions<T> {
timeoutDelay?: number
changeMetadata?: ChangeMetadataFunction
// set init to true or pass in initialization options to create a new empty document
init?: boolean | DocHandleChangeOptions<T>
}

// todo: remove this type once we have real metadata on changes in automerge
Expand Down
21 changes: 11 additions & 10 deletions packages/automerge-repo/src/Repo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import {
parseAutomergeUrl,
} from "./AutomergeUrl.js"
import {
ChangeMetadataFunction,
DocHandle,
DocHandleEncodedChangePayload,
ChangeMetadata,
ChangeMetadataFunction,
DocHandleEncodedChangePayload,
DocHandleChangeOptions,
} from "./DocHandle.js"
import { RemoteHeadsSubscriptions } from "./RemoteHeadsSubscriptions.js"
import { headsAreSame } from "./helpers/headsAreSame.js"
Expand Down Expand Up @@ -332,17 +333,17 @@ export class Repo extends EventEmitter<RepoEvents> {
/** The documentId of the handle to look up or create */
documentId: DocumentId,

/** If we know we're creating a new document, specify this so we can have access to it immediately */
isNew: boolean
/** When creating a handle for a new doc set init to true or pass in a init options object */
init?: DocHandleChangeOptions<T>
) {
// If we have the handle cached, return it
if (this.#handleCache[documentId]) return this.#handleCache[documentId]

// If not, create a new handle, cache it, and return it
if (!documentId) throw new Error(`Invalid documentId ${documentId}`)
const handle = new DocHandle<T>(documentId, {
isNew,
changeMetadata: this.#changeMetadata,
init,
})
this.#handleCache[documentId] = handle
return handle
Expand All @@ -367,7 +368,7 @@ export class Repo extends EventEmitter<RepoEvents> {
* an empty object `{}`. Its documentId is generated by the system. we emit a `document` event
* to advertise interest in the document.
*/
create<T>(): DocHandle<T> {
create<T>(options: CreateDocHandleOptions = {}): DocHandle<T> {
// TODO:
// either
// - pass an initial value and do something like this to ensure that you get a valid initial value
Expand All @@ -388,7 +389,7 @@ export class Repo extends EventEmitter<RepoEvents> {

// Generate a new UUID and store it in the buffer
const { documentId } = parseAutomergeUrl(generateAutomergeUrl())
const handle = this.#getHandle<T>(documentId, true) as DocHandle<T>
const handle = this.#getHandle<T>(documentId, options) as DocHandle<T>
this.emit("document", { handle, isNew: true })
return handle
}
Expand Down Expand Up @@ -454,7 +455,7 @@ export class Repo extends EventEmitter<RepoEvents> {
return this.#handleCache[documentId]
}

const handle = this.#getHandle<T>(documentId, false) as DocHandle<T>
const handle = this.#getHandle<T>(documentId) as DocHandle<T>
this.emit("document", { handle, isNew: false })
return handle
}
Expand All @@ -465,7 +466,7 @@ export class Repo extends EventEmitter<RepoEvents> {
) {
const documentId = interpretAsDocumentId(id)

const handle = this.#getHandle(documentId, false)
const handle = this.#getHandle(documentId)
handle.delete()

delete this.#handleCache[documentId]
Expand All @@ -482,7 +483,7 @@ export class Repo extends EventEmitter<RepoEvents> {
async export(id: AnyDocumentId): Promise<Uint8Array | undefined> {
const documentId = interpretAsDocumentId(id)

const handle = this.#getHandle(documentId, false)
const handle = this.#getHandle(documentId)
const doc = await handle.doc()
if (!doc) return undefined
return Automerge.save(doc)
Expand Down
96 changes: 61 additions & 35 deletions packages/automerge-repo/test/DocHandle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ describe("DocHandle", () => {
})

it("should emit a change message when changes happen", async () => {
const handle = new DocHandle<TestDoc>(TEST_ID, { isNew: true })
const handle = new DocHandle<TestDoc>(TEST_ID, { init: true })

const p = new Promise<DocHandleChangePayload<TestDoc>>(resolve =>
handle.once("change", d => resolve(d))
Expand All @@ -129,7 +129,7 @@ describe("DocHandle", () => {

it("should not emit a change message if no change happens via update", () =>
new Promise<void>((done, reject) => {
const handle = new DocHandle<TestDoc>(TEST_ID, { isNew: true })
const handle = new DocHandle<TestDoc>(TEST_ID, { init: true })
handle.once("change", () => {
reject(new Error("shouldn't have changed"))
})
Expand All @@ -140,7 +140,7 @@ describe("DocHandle", () => {
}))

it("should update the internal doc prior to emitting the change message", async () => {
const handle = new DocHandle<TestDoc>(TEST_ID, { isNew: true })
const handle = new DocHandle<TestDoc>(TEST_ID, { init: true })

const p = new Promise<void>(resolve =>
handle.once("change", ({ handle, doc }) => {
Expand All @@ -158,7 +158,7 @@ describe("DocHandle", () => {
})

it("should emit distinct change messages when consecutive changes happen", async () => {
const handle = new DocHandle<TestDoc>(TEST_ID, { isNew: true })
const handle = new DocHandle<TestDoc>(TEST_ID, { init: true })

let calls = 0
const p = new Promise(resolve =>
Expand Down Expand Up @@ -188,7 +188,7 @@ describe("DocHandle", () => {
})

it("should emit a change message when changes happen", async () => {
const handle = new DocHandle<TestDoc>(TEST_ID, { isNew: true })
const handle = new DocHandle<TestDoc>(TEST_ID, { init: true })
const p = new Promise(resolve => handle.once("change", d => resolve(d)))

handle.change(doc => {
Expand All @@ -202,7 +202,7 @@ describe("DocHandle", () => {

it("should not emit a patch message if no change happens", () =>
new Promise<void>((done, reject) => {
const handle = new DocHandle<TestDoc>(TEST_ID, { isNew: true })
const handle = new DocHandle<TestDoc>(TEST_ID, { init: true })
handle.on("change", () => {
reject(new Error("shouldn't have changed"))
})
Expand Down Expand Up @@ -269,7 +269,7 @@ describe("DocHandle", () => {
})

it("should emit a delete event when deleted", async () => {
const handle = new DocHandle<TestDoc>(TEST_ID, { isNew: true })
const handle = new DocHandle<TestDoc>(TEST_ID, { init: true })

const p = new Promise<void>(resolve =>
handle.once("delete", () => resolve())
Expand All @@ -281,7 +281,7 @@ describe("DocHandle", () => {
})

it("should allow changing at old heads", async () => {
const handle = new DocHandle<TestDoc>(TEST_ID, { isNew: true })
const handle = new DocHandle<TestDoc>(TEST_ID, { init: true })

handle.change(doc => {
doc.foo = "bar"
Expand All @@ -304,11 +304,13 @@ describe("DocHandle", () => {
})

describe("metadata on changes", () => {
it("should allow to pass in a reference to global metadata", () => {
it("should allow to pass in metadata function", () => {
const documentIds = []

const handle = new DocHandle<TestDoc>(TEST_ID, {
isNew: true,
init: true,
changeMetadata: documentId => {
assert.equal(documentId, handle.documentId)
documentIds.push(documentId)

return {
author: "bob",
Expand All @@ -328,19 +330,24 @@ describe("DocHandle", () => {
doc.foo = "baz"
})

const doc2 = handle.docSync()
// changeMetadata was called with the right documentId
assert.equal(documentIds.length, 3)
assert(documentIds.every(documentId => documentId === handle.documentId))

const changes = A.getChanges(doc1, doc2).map(A.decodeChange)
assert.equal(changes.length, 2)
// changes all have bob as author metadata
const changes = A.getAllChanges(handle.docSync()).map(A.decodeChange)
assert.equal(changes.length, 3)
assert.equal(changes[0].message, JSON.stringify({ author: "bob" }))
assert.equal(changes[1].message, JSON.stringify({ author: "bob" }))
})

it("should allow to add additional local metadata with", () => {
it("should allow to provide additional metadata when applying change", () => {
const documentIds = []

const handle = new DocHandle<TestDoc>(TEST_ID, {
isNew: true,
init: { time: 0 },
changeMetadata: documentId => {
assert.equal(documentId, handle.documentId)
documentIds.push(documentId)

return {
author: "bob",
Expand All @@ -355,7 +362,7 @@ describe("DocHandle", () => {
doc => {
doc.foo = "bar"
},
{ metadata: { message: "with change" } }
{ metadata: { message: "with change" }, time: 1 }
)

// ... with change at
Expand All @@ -364,28 +371,40 @@ describe("DocHandle", () => {
doc => {
doc.foo = "baz"
},
{ metadata: { message: "with changeAt" } }
{ metadata: { message: "with changeAt" }, time: 2 }
)

const doc2 = handle.docSync()
// changeMetadata was called with the right documentId
assert.equal(documentIds.length, 3)
assert(documentIds.every(documentId => documentId === handle.documentId))

const changes = A.getChanges(doc1, doc2).map(A.decodeChange)
assert.equal(changes.length, 2)
// changes have bob as author and the locally set message and time
const changes = A.getAllChanges(handle.docSync()).map(A.decodeChange)
assert.equal(changes.length, 3)
assert.equal(changes[0].message, JSON.stringify({ author: "bob" }))
assert.equal(changes[0].time, 0)
assert.equal(
changes[0].message,
changes[1].message,
JSON.stringify({ author: "bob", message: "with change" })
)
assert.equal(changes[1].time, 1)
assert.equal(
changes[1].message,
changes[2].message,
JSON.stringify({ author: "bob", message: "with changeAt" })
)
assert.equal(changes[2].time, 2)
})

it("should allow to override global data with change", () => {
it("should allow to override global meta data when applying change", () => {
const documentIds = []

const handle = new DocHandle<TestDoc>(TEST_ID, {
isNew: true,
init: {
metadata: { author: "alex" },
time: 1,
},
changeMetadata: documentId => {
assert.equal(documentId, handle.documentId)
documentIds.push(documentId)

return {
author: "bob",
Expand All @@ -400,7 +419,7 @@ describe("DocHandle", () => {
doc => {
doc.foo = "bar"
},
{ metadata: { author: "sandra" } }
{ metadata: { author: "sandra" }, time: 2 }
)

// ... with change at
Expand All @@ -409,15 +428,22 @@ describe("DocHandle", () => {
doc => {
doc.foo = "baz"
},
{ metadata: { author: "frank" } }
{ metadata: { author: "frank" }, time: 3 }
)

const doc2 = handle.docSync()

const changes = A.getChanges(doc1, doc2).map(A.decodeChange)
assert.equal(changes.length, 2)
assert.equal(changes[0].message, JSON.stringify({ author: "sandra" }))
assert.equal(changes[1].message, JSON.stringify({ author: "frank" }))
// changeMetadata was called with the right documentId
assert.equal(documentIds.length, 3)
assert(documentIds.every(documentId => documentId === handle.documentId))

// changes have the locally overriden author and the timestamp
const changes = A.getAllChanges(handle.docSync()).map(A.decodeChange)
assert.equal(changes.length, 3)
assert.equal(changes[0].message, JSON.stringify({ author: "alex" }))
assert.equal(changes[0].time, 1)
assert.equal(changes[1].message, JSON.stringify({ author: "sandra" }))
assert.equal(changes[1].time, 2)
assert.equal(changes[2].message, JSON.stringify({ author: "frank" }))
assert.equal(changes[2].time, 3)
})
})

Expand Down
Loading

0 comments on commit f918cba

Please sign in to comment.