Skip to content

Commit

Permalink
Add support for clearing doc from DocHandle so that reference can be …
Browse files Browse the repository at this point in the history
…released and memory freed, without deleting document from storage (#1)

Co-authored-by: George Su <[email protected]>
  • Loading branch information
georgewsu and George Su authored Aug 1, 2024
1 parent 373ce97 commit 8dbc3aa
Show file tree
Hide file tree
Showing 2 changed files with 52 additions and 2 deletions.
25 changes: 23 additions & 2 deletions packages/automerge-repo/src/DocHandle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
this.emit("delete", { handle: this })
return { doc: A.init() }
}),
onClear: assign(() => {
return { doc: A.init() }
}),
onUnavailable: () => {
this.emit("unavailable", { handle: this })
},
Expand All @@ -87,6 +90,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
on: {
UPDATE: { actions: "onUpdate" },
DELETE: ".deleted",
CLEAR: ".cleared",
},
states: {
idle: {
Expand Down Expand Up @@ -114,6 +118,7 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
},
ready: {},
deleted: { entry: "onDelete", type: "final" },
cleared: { entry: "onClear", type: "final" },
},
})

Expand Down Expand Up @@ -211,6 +216,13 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
*/
isDeleted = () => this.inState(["deleted"])

/**
* @returns true if the document has been marked as cleared.
*
* Cleared documents are removed from the sync process but not storage.
*/
isCleared = () => this.inState(["cleared"])

/**
* @returns true if the document is currently unavailable.
*
Expand Down Expand Up @@ -426,6 +438,11 @@ export class DocHandle<T> extends EventEmitter<DocHandleEvents<T>> {
this.#machine.send({ type: DELETE })
}

/** Called by the repo when the document is cleared from handleCache. */
clear() {
this.#machine.send({ type: CLEAR })
}

/**
* Sends an arbitrary ephemeral message out to all reachable peers who would receive sync messages
* from you. It has no guarantee of delivery, and is not persisted to the underlying automerge doc
Expand Down Expand Up @@ -541,12 +558,14 @@ export const HandleState = {
READY: "ready",
/** The document has been deleted from the repo */
DELETED: "deleted",
/** The document has been removed from the repo and handleCache but not deleted from storage */
CLEARED: "cleared",
/** The document was not available in storage or from any connected peers */
UNAVAILABLE: "unavailable",
} as const
export type HandleState = (typeof HandleState)[keyof typeof HandleState]

export const { IDLE, LOADING, REQUESTING, READY, DELETED, UNAVAILABLE } =
export const { IDLE, LOADING, REQUESTING, READY, DELETED, CLEARED, UNAVAILABLE } =
HandleState

// context
Expand All @@ -567,14 +586,16 @@ type DocHandleEvent<T> =
type: typeof UPDATE
payload: { callback: (doc: A.Doc<T>) => A.Doc<T> }
}
| { type: typeof TIMEOUT }
| { type: typeof DELETE }
| { type: typeof CLEAR }
| { type: typeof TIMEOUT }
| { type: typeof DOC_UNAVAILABLE }

const BEGIN = "BEGIN"
const REQUEST = "REQUEST"
const DOC_READY = "DOC_READY"
const UPDATE = "UPDATE"
const DELETE = "DELETE"
const CLEAR = "CLEAR"
const TIMEOUT = "TIMEOUT"
const DOC_UNAVAILABLE = "DOC_UNAVAILABLE"
29 changes: 29 additions & 0 deletions packages/automerge-repo/test/DocHandle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { eventPromise } from "../src/helpers/eventPromise.js"
import { pause } from "../src/helpers/pause.js"
import { DocHandle, DocHandleChangePayload } from "../src/index.js"
import { TestDoc } from "./types.js"
import { CLEARED } from "../src/DocHandle.js"

describe("DocHandle", () => {
const TEST_ID = parseAutomergeUrl(generateAutomergeUrl()).documentId
Expand Down Expand Up @@ -325,6 +326,34 @@ describe("DocHandle", () => {
assert.equal(handle.isDeleted(), true)
})

it("should clear document reference when cleared", async () => {
const handle = setup()

handle.change(doc => {
doc.foo = "bar"
})
const doc = await handle.doc()
assert.equal(doc?.foo, "bar")

handle.clear()
const clearedDoc = await handle.doc([CLEARED])

assert.equal(handle.isCleared(), true)
assert.equal(handle.isReady(), false)
assert.equal(handle.docSync(), undefined)
assert.equal(clearedDoc?.foo, undefined)
})

it("should prevent transitioning out of cleared state because it is final", async () => {
const handle = setup()

handle.clear()
await handle.doc([CLEARED])

assert.throws(() => handle.change(d => (d.foo = "bar")))
assert.equal(handle.isCleared(), true)
})

it("should allow changing at old heads", async () => {
const handle = setup()

Expand Down

0 comments on commit 8dbc3aa

Please sign in to comment.