Compare commits

...

2 commits

Author SHA1 Message Date
Orion Henry
4ca9c51a47 made a view(), optimized clone(), changed error messages to suggest clone() 2022-10-25 17:31:14 -05:00
Orion Henry
0ad422beb5 Automerge.clone(heads) 2022-10-19 15:17:45 -05:00
6 changed files with 53 additions and 37 deletions

View file

@ -98,6 +98,9 @@ export function getBackend<T>(doc: Doc<T>): Automerge {
} }
function _state<T>(doc: Doc<T>, checkroot = true): InternalState<T> { function _state<T>(doc: Doc<T>, checkroot = true): InternalState<T> {
if (typeof doc !== 'object') {
throw new RangeError("must be the document root")
}
const state = Reflect.get(doc, STATE) const state = Reflect.get(doc, STATE)
if (state === undefined || (checkroot && _obj(doc) !== "_root")) { if (state === undefined || (checkroot && _obj(doc) !== "_root")) {
throw new RangeError("must be the document root") throw new RangeError("must be the document root")
@ -164,14 +167,23 @@ export function init<T>(_opts?: ActorId | InitOptions<T>): Doc<T> {
} }
/** /**
* 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<T>(doc: Doc<T>): Doc<T> { export function view<T>(doc: Doc<T>, heads: Heads): Doc<T> {
const state = _state(doc) const state = _state(doc)
const handle = state.heads ? state.handle.forkAt(state.heads) : state.handle.fork() const handle = state.handle
const clonedDoc: any = handle.materialize("/", undefined, {...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<T>(doc: Doc<T>, _opts?: ActorId | InitOptions<T>): Doc<T> {
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 /** Explicity free the memory backing a document. Note that this is note
@ -264,10 +276,8 @@ export function change<T>(doc: Doc<T>, options: string | ChangeOptions<T> | Chan
function progressDocument<T>(doc: Doc<T>, heads: Heads, callback?: PatchCallback<T>): Doc<T> { function progressDocument<T>(doc: Doc<T>, heads: Heads, callback?: PatchCallback<T>): Doc<T> {
let state = _state(doc) let state = _state(doc)
let nextState = {...state, heads: undefined}; let nextState = {...state, heads: undefined};
// @ts-ignore
let nextDoc = state.handle.applyPatches(doc, nextState, callback) let nextDoc = state.handle.applyPatches(doc, nextState, callback)
state.heads = heads state.heads = heads
if (nextState.freeze) {Object.freeze(nextDoc)}
return nextDoc return nextDoc
} }
@ -284,7 +294,7 @@ function _change<T>(doc: Doc<T>, options: ChangeOptions<T>, callback: ChangeFn<T
throw new RangeError("must be the document root"); throw new RangeError("must be the document root");
} }
if (state.heads) { 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) { if (_readonly(doc) === false) {
throw new RangeError("Calls to Automerge.change cannot be nested") throw new RangeError("Calls to Automerge.change cannot be nested")
@ -331,7 +341,7 @@ export function emptyChange<T>(doc: Doc<T>, options: string | ChangeOptions<T>)
const state = _state(doc) const state = _state(doc)
if (state.heads) { 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) { if (_readonly(doc) === false) {
throw new RangeError("Calls to Automerge.change cannot be nested") throw new RangeError("Calls to Automerge.change cannot be nested")
@ -616,7 +626,7 @@ export function applyChanges<T>(doc: Doc<T>, changes: Change[], opts?: ApplyOpti
const state = _state(doc) const state = _state(doc)
if (!opts) {opts = {}} if (!opts) {opts = {}}
if (state.heads) { 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) { if (_readonly(doc) === false) {
throw new RangeError("Calls to Automerge.change cannot be nested") throw new RangeError("Calls to Automerge.change cannot be nested")
@ -721,7 +731,7 @@ export function receiveSyncMessage<T>(doc: Doc<T>, inState: SyncState, message:
if (!opts) {opts = {}} if (!opts) {opts = {}}
const state = _state(doc) const state = _state(doc)
if (state.heads) { 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) { if (_readonly(doc) === false) {
throw new RangeError("Calls to Automerge.change cannot be nested") throw new RangeError("Calls to Automerge.change cannot be nested")

View file

@ -7,6 +7,22 @@ describe('Automerge', () => {
it('should init clone and free', () => { it('should init clone and free', () => {
let doc1 = Automerge.init() let doc1 = Automerge.init()
let doc2 = Automerge.clone(doc1); 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', () => { it('handle basic set and read on root object', () => {

View file

@ -231,14 +231,14 @@ describe('Automerge', () => {
s2 = Automerge.change(s1, doc2 => doc2.two = 2) s2 = Automerge.change(s1, doc2 => doc2.two = 2)
doc1.one = 1 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', () => { it('should not allow the same base document to be used for multiple changes', () => {
assert.throws(() => { assert.throws(() => {
Automerge.change(s1, doc => doc.one = 1) Automerge.change(s1, doc => doc.one = 1)
Automerge.change(s1, doc => doc.two = 2) 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', () => { it('should allow a document to be cloned', () => {

View file

@ -199,12 +199,11 @@ export class Automerge {
getMissingDeps(heads?: Heads): Heads; getMissingDeps(heads?: Heads): Heads;
// memory management // memory management
free(): void; free(): void; // only needed if weak-refs are unsupported
clone(actor?: string): Automerge; clone(actor?: string): Automerge; // TODO - remove, this is dangerous
fork(actor?: string): Automerge; fork(actor?: string, heads?: Heads): Automerge;
forkAt(heads: Heads, actor?: string): Automerge;
// dump internal state to console.log // dump internal state to console.log - for debugging
dump(): void; dump(): void;
// experimental api can go here // experimental api can go here

View file

@ -98,24 +98,15 @@ impl Automerge {
Ok(automerge) Ok(automerge)
} }
pub fn fork(&mut self, actor: Option<String>) -> Result<Automerge, JsValue> { pub fn fork(&mut self, actor: Option<String>, heads: JsValue) -> Result<Automerge, JsValue> {
let mut automerge = Automerge { let heads: Result<Vec<am::ChangeHash>, _> = JS(heads).try_into();
doc: self.doc.fork(), let doc = if let Ok(heads) = heads {
freeze: self.freeze, self.doc.fork_at(&heads)?
external_types: self.external_types.clone(), } 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<String>) -> Result<Automerge, JsValue> {
let deps: Vec<_> = JS(heads).try_into()?;
let mut automerge = Automerge { let mut automerge = Automerge {
doc: self.doc.fork_at(&deps)?, doc,
freeze: self.freeze, freeze: self.freeze,
external_types: self.external_types.clone(), external_types: self.external_types.clone(),
}; };

View file

@ -425,7 +425,7 @@ describe('Automerge', () => {
assert.deepEqual(doc2.getWithType(c, "d"), ["str", "dd"]) 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") const A = create("aaaaaa")
A.put("/", "key1", "val1"); A.put("/", "key1", "val1");
A.put("/", "key2", "val2"); A.put("/", "key2", "val2");
@ -436,8 +436,8 @@ describe('Automerge', () => {
A.merge(B) A.merge(B)
const heads2 = A.getHeads(); const heads2 = A.getHeads();
A.put("/", "key5", "val5"); A.put("/", "key5", "val5");
assert.deepEqual(A.forkAt(heads1).materialize("/"), A.materialize("/", heads1)) assert.deepEqual(A.fork(undefined, heads1).materialize("/"), A.materialize("/", heads1))
assert.deepEqual(A.forkAt(heads2).materialize("/"), A.materialize("/", heads2)) assert.deepEqual(A.fork(undefined, heads2).materialize("/"), A.materialize("/", heads2))
}) })
it('should handle merging text conflicts then saving & loading', () => { it('should handle merging text conflicts then saving & loading', () => {