diff --git a/javascript/src/index.ts b/javascript/src/index.ts index 3dcf2cc4..c799d14c 100644 --- a/javascript/src/index.ts +++ b/javascript/src/index.ts @@ -98,6 +98,9 @@ export function getBackend(doc: Doc): Automerge { } function _state(doc: Doc, checkroot = true): InternalState { + if (typeof doc !== 'object') { + throw new RangeError("must be the document root") + } const state = Reflect.get(doc, STATE) if (state === undefined || (checkroot && _obj(doc) !== "_root")) { throw new RangeError("must be the document root") @@ -164,14 +167,23 @@ export function init(_opts?: ActorId | InitOptions): Doc { } /** - * Make a copy of an automerge document. + * Make a copy of an automerge document. By default it allocates a new actorId so the copy can be later merged. */ -export function clone(doc: Doc): Doc { +export function view(doc: Doc, heads: Heads): Doc { const state = _state(doc) - const handle = state.heads ? state.handle.forkAt(state.heads) : state.handle.fork() - const clonedDoc: any = handle.materialize("/", undefined, {...state, handle}) + const handle = state.handle + return state.handle.materialize("/", heads, { ...state, handle, heads }) as any +} - return clonedDoc +/** + * Make a copy of an automerge document. By default it allocates a new actorId so the copy can be later merged. + */ +export function clone(doc: Doc, _opts?: ActorId | InitOptions): Doc { + const state = _state(doc) + const heads = state.heads + const opts = importOpts(_opts) + const handle = state.handle.fork(opts.actor, heads) + return handle.applyPatches(doc, { ... state, heads, handle }) } /** Explicity free the memory backing a document. Note that this is note @@ -264,10 +276,8 @@ export function change(doc: Doc, options: string | ChangeOptions | Chan function progressDocument(doc: Doc, heads: Heads, callback?: PatchCallback): Doc { let state = _state(doc) let nextState = {...state, heads: undefined}; - // @ts-ignore let nextDoc = state.handle.applyPatches(doc, nextState, callback) state.heads = heads - if (nextState.freeze) {Object.freeze(nextDoc)} return nextDoc } @@ -284,7 +294,7 @@ function _change(doc: Doc, options: ChangeOptions, callback: ChangeFn(doc: Doc, options: string | ChangeOptions) const state = _state(doc) if (state.heads) { - throw new RangeError("Attempting to use an outdated Automerge document") + throw new RangeError("Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy.") } if (_readonly(doc) === false) { throw new RangeError("Calls to Automerge.change cannot be nested") @@ -616,7 +626,7 @@ export function applyChanges(doc: Doc, changes: Change[], opts?: ApplyOpti const state = _state(doc) if (!opts) {opts = {}} if (state.heads) { - throw new RangeError("Attempting to use an outdated Automerge document") + throw new RangeError("Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy.") } if (_readonly(doc) === false) { throw new RangeError("Calls to Automerge.change cannot be nested") @@ -721,7 +731,7 @@ export function receiveSyncMessage(doc: Doc, inState: SyncState, message: if (!opts) {opts = {}} const state = _state(doc) if (state.heads) { - throw new RangeError("Attempting to change an out of date document - set at: " + _trace(doc)); + throw new RangeError("Attempting to change an outdated document. Use Automerge.clone() if you wish to make a writable copy.") } if (_readonly(doc) === false) { throw new RangeError("Calls to Automerge.change cannot be nested") diff --git a/javascript/test/basic_test.ts b/javascript/test/basic_test.ts index 130fc6ec..637d9029 100644 --- a/javascript/test/basic_test.ts +++ b/javascript/test/basic_test.ts @@ -7,6 +7,22 @@ describe('Automerge', () => { it('should init clone and free', () => { let doc1 = Automerge.init() let doc2 = Automerge.clone(doc1); + + // this is only needed if weakrefs are not supported + Automerge.free(doc1) + Automerge.free(doc2) + }) + + it('should be able to make a view with specifc heads', () => { + let doc1 = Automerge.init() + let doc2 = Automerge.change(doc1, (d) => d.value = 1) + let heads2 = Automerge.getHeads(doc2) + let doc3 = Automerge.change(doc2, (d) => d.value = 2) + let doc2_v2 = Automerge.view(doc3, heads2) + assert.deepEqual(doc2, doc2_v2) + let doc2_v2_clone = Automerge.clone(doc2, "aabbcc") + assert.deepEqual(doc2, doc2_v2_clone) + assert.equal(Automerge.getActorId(doc2_v2_clone), "aabbcc") }) it('handle basic set and read on root object', () => { diff --git a/javascript/test/legacy_tests.ts b/javascript/test/legacy_tests.ts index ea814016..0d152a2d 100644 --- a/javascript/test/legacy_tests.ts +++ b/javascript/test/legacy_tests.ts @@ -231,14 +231,14 @@ describe('Automerge', () => { s2 = Automerge.change(s1, doc2 => doc2.two = 2) doc1.one = 1 }) - }, /Attempting to use an outdated Automerge document/) + }, /Attempting to change an outdated document/) }) it('should not allow the same base document to be used for multiple changes', () => { assert.throws(() => { Automerge.change(s1, doc => doc.one = 1) Automerge.change(s1, doc => doc.two = 2) - }, /Attempting to use an outdated Automerge document/) + }, /Attempting to change an outdated document/) }) it('should allow a document to be cloned', () => { diff --git a/rust/automerge-wasm/index.d.ts b/rust/automerge-wasm/index.d.ts index e6dbd6c8..67d03b84 100644 --- a/rust/automerge-wasm/index.d.ts +++ b/rust/automerge-wasm/index.d.ts @@ -199,12 +199,11 @@ export class Automerge { getMissingDeps(heads?: Heads): Heads; // memory management - free(): void; - clone(actor?: string): Automerge; - fork(actor?: string): Automerge; - forkAt(heads: Heads, actor?: string): Automerge; + free(): void; // only needed if weak-refs are unsupported + clone(actor?: string): Automerge; // TODO - remove, this is dangerous + fork(actor?: string, heads?: Heads): Automerge; - // dump internal state to console.log + // dump internal state to console.log - for debugging dump(): void; // experimental api can go here diff --git a/rust/automerge-wasm/src/lib.rs b/rust/automerge-wasm/src/lib.rs index c08486a8..d8f0072f 100644 --- a/rust/automerge-wasm/src/lib.rs +++ b/rust/automerge-wasm/src/lib.rs @@ -98,24 +98,15 @@ impl Automerge { Ok(automerge) } - pub fn fork(&mut self, actor: Option) -> Result { - let mut automerge = Automerge { - doc: self.doc.fork(), - freeze: self.freeze, - external_types: self.external_types.clone(), + pub fn fork(&mut self, actor: Option, heads: JsValue) -> Result { + let heads: Result, _> = JS(heads).try_into(); + let doc = if let Ok(heads) = heads { + self.doc.fork_at(&heads)? + } else { + self.doc.fork() }; - if let Some(s) = actor { - let actor = automerge::ActorId::from(hex::decode(s).map_err(to_js_err)?.to_vec()); - automerge.doc.set_actor(actor); - } - Ok(automerge) - } - - #[wasm_bindgen(js_name = forkAt)] - pub fn fork_at(&mut self, heads: JsValue, actor: Option) -> Result { - let deps: Vec<_> = JS(heads).try_into()?; let mut automerge = Automerge { - doc: self.doc.fork_at(&deps)?, + doc, freeze: self.freeze, external_types: self.external_types.clone(), }; diff --git a/rust/automerge-wasm/test/test.ts b/rust/automerge-wasm/test/test.ts index 0f6ce354..8e8acd69 100644 --- a/rust/automerge-wasm/test/test.ts +++ b/rust/automerge-wasm/test/test.ts @@ -425,7 +425,7 @@ describe('Automerge', () => { assert.deepEqual(doc2.getWithType(c, "d"), ["str", "dd"]) }) - it('should allow you to forkAt a heads', () => { + it('should allow you to fork at a heads', () => { const A = create("aaaaaa") A.put("/", "key1", "val1"); A.put("/", "key2", "val2"); @@ -436,8 +436,8 @@ describe('Automerge', () => { A.merge(B) const heads2 = A.getHeads(); A.put("/", "key5", "val5"); - assert.deepEqual(A.forkAt(heads1).materialize("/"), A.materialize("/", heads1)) - assert.deepEqual(A.forkAt(heads2).materialize("/"), A.materialize("/", heads2)) + assert.deepEqual(A.fork(undefined, heads1).materialize("/"), A.materialize("/", heads1)) + assert.deepEqual(A.fork(undefined, heads2).materialize("/"), A.materialize("/", heads2)) }) it('should handle merging text conflicts then saving & loading', () => {