diff --git a/packages/automerge-repo/src/DocHandle.ts b/packages/automerge-repo/src/DocHandle.ts index b85508711..e6825ac54 100644 --- a/packages/automerge-repo/src/DocHandle.ts +++ b/packages/automerge-repo/src/DocHandle.ts @@ -72,6 +72,9 @@ export class DocHandle extends EventEmitter> { this.emit("delete", { handle: this }) return { doc: A.init() } }), + onClear: assign(() => { + return { doc: A.init() } + }), onUnavailable: () => { this.emit("unavailable", { handle: this }) }, @@ -87,6 +90,7 @@ export class DocHandle extends EventEmitter> { on: { UPDATE: { actions: "onUpdate" }, DELETE: ".deleted", + CLEAR: ".cleared", }, states: { idle: { @@ -114,6 +118,7 @@ export class DocHandle extends EventEmitter> { }, ready: {}, deleted: { entry: "onDelete", type: "final" }, + cleared: { entry: "onClear", type: "final" }, }, }) @@ -211,6 +216,13 @@ export class DocHandle extends EventEmitter> { */ 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. * @@ -426,6 +438,11 @@ export class DocHandle extends EventEmitter> { 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 @@ -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 @@ -567,8 +586,9 @@ type DocHandleEvent = type: typeof UPDATE payload: { callback: (doc: A.Doc) => A.Doc } } - | { type: typeof TIMEOUT } | { type: typeof DELETE } + | { type: typeof CLEAR } + | { type: typeof TIMEOUT } | { type: typeof DOC_UNAVAILABLE } const BEGIN = "BEGIN" @@ -576,5 +596,6 @@ 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" diff --git a/packages/automerge-repo/test/DocHandle.test.ts b/packages/automerge-repo/test/DocHandle.test.ts index a82bbaa98..c3297760e 100644 --- a/packages/automerge-repo/test/DocHandle.test.ts +++ b/packages/automerge-repo/test/DocHandle.test.ts @@ -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 @@ -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()