change MAP,LIST,TEXT to be {},[],'' - allow recursion

This commit is contained in:
Orion Henry 2022-02-23 19:43:13 -05:00
parent b96aa168b4
commit 2fc0705907
7 changed files with 159 additions and 96 deletions

View file

@ -4,7 +4,6 @@ const { Int, Uint, Float64 } = require("./numbers");
const { Counter, getWriteableCounter } = require("./counter");
const { Text } = require("./text");
const { STATE, HEADS, FROZEN, OBJECT_ID, READ_ONLY } = require("./constants")
const { MAP, LIST, TABLE, TEXT } = require("automerge-wasm")
function parseListIndex(key) {
if (typeof key === 'string' && /^[0-9]+$/.test(key)) key = parseInt(key, 10)
@ -135,21 +134,21 @@ const MapHandler = {
}
switch (datatype) {
case "list":
const list = context.set(objectId, key, LIST)
const list = context.set(objectId, key, [])
const proxyList = listProxy(context, list, [ ... path, key ], readonly );
for (let i = 0; i < value.length; i++) {
proxyList[i] = value[i]
}
break;
case "text":
const text = context.set(objectId, key, TEXT)
const text = context.set(objectId, key, "", "text")
const proxyText = textProxy(context, text, [ ... path, key ], readonly );
for (let i = 0; i < value.length; i++) {
proxyText[i] = value.get(i)
}
break;
case "map":
const map = context.set(objectId, key, MAP)
const map = context.set(objectId, key, {})
const proxyMap = mapProxy(context, map, [ ... path, key ], readonly );
for (const key in value) {
proxyMap[key] = value[key]
@ -252,9 +251,9 @@ const ListHandler = {
case "list":
let list
if (index >= context.length(objectId)) {
list = context.insert(objectId, index, LIST)
list = context.insert(objectId, index, [])
} else {
list = context.set(objectId, index, LIST)
list = context.set(objectId, index, [])
}
const proxyList = listProxy(context, list, [ ... path, index ], readonly);
proxyList.splice(0,0,...value)
@ -262,9 +261,9 @@ const ListHandler = {
case "text":
let text
if (index >= context.length(objectId)) {
text = context.insert(objectId, index, TEXT)
text = context.insert(objectId, index, "", "text")
} else {
text = context.set(objectId, index, TEXT)
text = context.set(objectId, index, "", "text")
}
const proxyText = textProxy(context, text, [ ... path, index ], readonly);
proxyText.splice(0,0,...value)
@ -272,9 +271,9 @@ const ListHandler = {
case "map":
let map
if (index >= context.length(objectId)) {
map = context.insert(objectId, index, MAP)
map = context.insert(objectId, index, {})
} else {
map = context.set(objectId, index, MAP)
map = context.set(objectId, index, {})
}
const proxyMap = mapProxy(context, map, [ ... path, index ], readonly);
for (const key in value) {
@ -479,17 +478,17 @@ function listMethods(target) {
for (let [value,datatype] of values) {
switch (datatype) {
case "list":
const list = context.insert(objectId, index, LIST)
const list = context.insert(objectId, index, [])
const proxyList = listProxy(context, list, [ ... path, index ], readonly);
proxyList.splice(0,0,...value)
break;
case "text":
const text = context.insert(objectId, index, TEXT)
const text = context.insert(objectId, index, "", "text")
const proxyText = textProxy(context, text, [ ... path, index ], readonly);
proxyText.splice(0,0,...value)
break;
case "map":
const map = context.insert(objectId, index, MAP)
const map = context.insert(objectId, index, {})
const proxyMap = mapProxy(context, map, [ ... path, index ], readonly);
for (const key in value) {
proxyMap[key] = value[key]

View file

@ -6,8 +6,7 @@ export type SyncMessage = Uint8Array;
export type Prop = string | number;
export type Hash = string;
export type Heads = Hash[];
export type ObjectType = string; // opaque ??
export type Value = string | number | boolean | null | Date | Uint8Array | ObjectType;
export type Value = string | number | boolean | null | Date | Uint8Array | Array | Object;
export type FullValue =
["str", string] |
["int", number] |
@ -23,11 +22,6 @@ export type FullValue =
["text", ObjID] |
["table", ObjID]
export const LIST : ObjectType;
export const MAP : ObjectType;
export const TABLE : ObjectType;
export const TEXT : ObjectType;
export enum ObjTypeName {
list = "list",
map = "map",
@ -44,7 +38,10 @@ export type Datatype =
"null" |
"timestamp" |
"counter" |
"bytes";
"bytes" |
"map" |
"text" |
"list";
export type DecodedSyncMessage = {
heads: Heads,
@ -86,10 +83,10 @@ export function decodeSyncState(data: Uint8Array): SyncState;
export class Automerge {
// change state
set(obj: ObjID, prop: Prop, value: Value, datatype?: Datatype): ObjID | undefined;
make(obj: ObjID, prop: Prop, value: ObjectType): ObjID;
make(obj: ObjID, prop: Prop, value: Value, datatype?: Datatype): ObjID;
insert(obj: ObjID, index: number, value: Value, datatype?: Datatype): ObjID | undefined;
push(obj: ObjID, value: Value, datatype?: Datatype): ObjID | undefined;
splice(obj: ObjID, start: number, delete_count: number, text?: string | Array<Value | FullValue>): ObjID[] | undefined;
splice(obj: ObjID, start: number, delete_count: number, text?: string | Array<Value>): ObjID[] | undefined;
inc(obj: ObjID, prop: Prop, value: number): void;
del(obj: ObjID, prop: Prop): void;

View file

@ -19,10 +19,9 @@
"main": "./dev/index.js",
"scripts": {
"build": "rimraf ./dev && wasm-pack build --target nodejs --dev --out-name index -d dev && cp index.d.ts dev",
"release": "rimraf ./dev && wasm-pack build --target nodejs --release --out-name index -d dev && yarn opt && cp index.d.ts dev",
"release": "rimraf ./dev && wasm-pack build --target nodejs --release --out-name index -d dev && cp index.d.ts dev",
"pkg": "rimraf ./pkg && wasm-pack build --target web --release --out-name index -d pkg && cp index.d.ts pkg && cd pkg && yarn pack && mv automerge-wasm*tgz ..",
"prof": "rimraf ./dev && wasm-pack build --target nodejs --profiling --out-name index -d dev",
"opt": "wasm-opt -Oz dev/index_bg.wasm -o tmp.wasm && mv tmp.wasm dev/index_bg.wasm",
"test": "yarn build && ts-mocha -p tsconfig.json --type-check --bail --full-trace test/*.ts"
},
"dependencies": {},

View file

@ -3,6 +3,7 @@ use automerge::{Change, ChangeHash, Prop};
use js_sys::{Array, Object, Reflect, Uint8Array};
use std::collections::HashSet;
use std::fmt::Display;
use unicode_segmentation::UnicodeSegmentation;
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
@ -257,23 +258,60 @@ pub(crate) fn to_prop(p: JsValue) -> Result<Prop, JsValue> {
}
}
pub(crate) fn to_objtype(a: &JsValue) -> Option<am::ObjType> {
if !a.is_function() {
return None;
}
let f: js_sys::Function = a.clone().try_into().unwrap();
let f = f.to_string();
if f.starts_with("class MAP", 0) {
Some(am::ObjType::Map)
} else if f.starts_with("class LIST", 0) {
Some(am::ObjType::List)
} else if f.starts_with("class TEXT", 0) {
Some(am::ObjType::Text)
} else if f.starts_with("class TABLE", 0) {
Some(am::ObjType::Table)
} else {
am::log!("to_objtype(function) -> {}", f);
None
pub(crate) fn to_objtype(
value: &JsValue,
datatype: &Option<String>,
) -> Option<(am::ObjType, Vec<(Prop, JsValue)>)> {
match datatype.as_deref() {
Some("map") => {
let map = value.clone().dyn_into::<js_sys::Object>().ok()?;
// FIXME unwrap
let map = js_sys::Object::keys(&map)
.iter()
.zip(js_sys::Object::values(&map).iter())
.map(|(key, val)| (key.as_string().unwrap().into(), val))
.collect();
Some((am::ObjType::Map, map))
}
Some("list") => {
let list = value.clone().dyn_into::<js_sys::Array>().ok()?;
let list = list
.iter()
.enumerate()
.map(|(i, e)| (i.into(), e))
.collect();
Some((am::ObjType::List, list))
}
Some("text") => {
let text = value.as_string()?;
let text = text
.graphemes(true)
.enumerate()
.map(|(i, ch)| (i.into(), ch.into()))
.collect();
Some((am::ObjType::Text, text))
}
Some(_) => None,
None => {
if let Ok(list) = value.clone().dyn_into::<js_sys::Array>() {
let list = list
.iter()
.enumerate()
.map(|(i, e)| (i.into(), e))
.collect();
Some((am::ObjType::List, list))
} else if let Ok(map) = value.clone().dyn_into::<js_sys::Object>() {
// FIXME unwrap
let map = js_sys::Object::keys(&map)
.iter()
.zip(js_sys::Object::values(&map).iter())
.map(|(key, val)| (key.as_string().unwrap().into(), val))
.collect();
Some((am::ObjType::Map, map))
} else {
None
}
}
}
}

View file

@ -131,15 +131,11 @@ impl Automerge {
} else {
if let Ok(array) = text.dyn_into::<Array>() {
for i in array.iter() {
if let Ok(array) = i.clone().dyn_into::<Array>() {
let value = array.get(1);
let datatype = array.get(2);
let value = self.import_value(value, datatype)?;
vals.push(value);
} else {
let value = self.import_value(i, JsValue::null())?;
vals.push(value);
let (value, subvals) = self.import_value(&i, None)?;
if !subvals.is_empty() {
return Err(to_js_err("splice must be shallow"));
}
vals.push(value);
}
}
let result = self.0.splice(&obj, start, delete_count, vals)?;
@ -162,9 +158,10 @@ impl Automerge {
datatype: JsValue,
) -> Result<Option<String>, JsValue> {
let obj = self.import(obj)?;
let value = self.import_value(value, datatype)?;
let (value, subvals) = self.import_value(&value, datatype.as_string())?;
let index = self.0.length(&obj);
let opid = self.0.insert(&obj, index, value)?;
self.subset(&opid, subvals)?;
Ok(opid.map(|id| id.to_string()))
}
@ -177,8 +174,9 @@ impl Automerge {
) -> Result<Option<String>, JsValue> {
let obj = self.import(obj)?;
let index = index as f64;
let value = self.import_value(value, datatype)?;
let (value, subvals) = self.import_value(&value, datatype.as_string())?;
let opid = self.0.insert(&obj, index as usize, value)?;
self.subset(&opid, subvals)?;
Ok(opid.map(|id| id.to_string()))
}
@ -191,17 +189,44 @@ impl Automerge {
) -> Result<JsValue, JsValue> {
let obj = self.import(obj)?;
let prop = self.import_prop(prop)?;
let value = self.import_value(value, datatype)?;
let (value, subvals) = self.import_value(&value, datatype.as_string())?;
let opid = self.0.set(&obj, prop, value)?;
self.subset(&opid, subvals)?;
Ok(opid.map(|id| id.to_string()).into())
}
pub fn make(&mut self, obj: JsValue, prop: JsValue, value: JsValue) -> Result<String, JsValue> {
fn subset(
&mut self,
obj: &Option<am::ObjId>,
vals: Vec<(am::Prop, JsValue)>,
) -> Result<(), JsValue> {
if let Some(id) = obj {
for (p, v) in vals {
let (value, subvals) = self.import_value(&v, None)?;
//let opid = self.0.set(id, p, value)?;
let opid = match p {
Prop::Map(s) => self.0.set(id, s, value)?,
Prop::Seq(i) => self.0.insert(id, i, value)?,
};
self.subset(&opid, subvals)?;
}
}
Ok(())
}
pub fn make(
&mut self,
obj: JsValue,
prop: JsValue,
value: JsValue,
datatype: JsValue,
) -> Result<String, JsValue> {
let obj = self.import(obj)?;
let prop = self.import_prop(prop)?;
let value = self.import_value(value, JsValue::null())?;
let (value, subvals) = self.import_value(&value, datatype.as_string())?;
if value.is_object() {
let opid = self.0.set(&obj, prop, value)?;
self.subset(&opid, subvals)?;
Ok(opid.unwrap().to_string())
} else {
Err(to_js_err("invalid object type"))
@ -480,15 +505,18 @@ impl Automerge {
}
}
fn import_value(&mut self, value: JsValue, datatype: JsValue) -> Result<Value, JsValue> {
let d = datatype.as_string();
match self.import_scalar(&value, &d) {
Some(val) => Ok(val.into()),
fn import_value(
&mut self,
value: &JsValue,
datatype: Option<String>,
) -> Result<(Value, Vec<(Prop, JsValue)>), JsValue> {
match self.import_scalar(value, &datatype) {
Some(val) => Ok((val.into(), vec![])),
None => {
if let Some(o) = to_objtype(&value) {
Ok(o.into())
if let Some((o, subvals)) = to_objtype(value, &datatype) {
Ok((o.into(), subvals))
} else {
web_sys::console::log_3(&"Invalid value".into(), &value, &datatype);
web_sys::console::log_2(&"Invalid value".into(), value);
Err(to_js_err("invalid value"))
}
}
@ -591,15 +619,3 @@ pub fn encode_sync_state(state: SyncState) -> Result<Uint8Array, JsValue> {
pub fn decode_sync_state(data: Uint8Array) -> Result<SyncState, JsValue> {
SyncState::decode(data)
}
#[wasm_bindgen(js_name = MAP)]
pub struct Map {}
#[wasm_bindgen(js_name = LIST)]
pub struct List {}
#[wasm_bindgen(js_name = TEXT)]
pub struct Text {}
#[wasm_bindgen(js_name = TABLE)]
pub struct Table {}

View file

@ -3,7 +3,7 @@ import { describe, it } from 'mocha';
import assert from 'assert'
//@ts-ignore
import { BloomFilter } from './helpers/sync'
import { create, loadDoc, SyncState, Automerge, MAP, LIST, TEXT, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '../dev/index'
import { create, loadDoc, SyncState, Automerge, encodeChange, decodeChange, initSyncState, decodeSyncMessage, decodeSyncState, encodeSyncState, encodeSyncMessage } from '../dev/index'
import { DecodedSyncMessage } from '../index';
import { Hash } from '../dev/index';
@ -64,7 +64,7 @@ describe('Automerge', () => {
doc.set(root, "bool", true)
doc.set(root, "time1", 1000, "timestamp")
doc.set(root, "time2", new Date(1001))
doc.set(root, "list", LIST);
doc.set(root, "list", []);
doc.set(root, "null", null)
result = doc.value(root,"hello")
@ -124,7 +124,7 @@ describe('Automerge', () => {
let root = "_root"
let result
let submap = doc.set(root, "submap", MAP)
let submap = doc.set(root, "submap", {})
if (!submap) throw new Error('should be not null')
doc.set(submap, "number", 6, "uint")
assert.strictEqual(doc.pendingOps(),2)
@ -141,7 +141,7 @@ describe('Automerge', () => {
let doc = create()
let root = "_root"
let submap = doc.set(root, "numbers", LIST)
let submap = doc.set(root, "numbers", [])
if (!submap) throw new Error('should be not null')
doc.insert(submap, 0, "a");
doc.insert(submap, 1, "b");
@ -165,7 +165,7 @@ describe('Automerge', () => {
let doc = create()
let root = "_root"
let submap = doc.set(root, "letters", LIST)
let submap = doc.set(root, "letters", [])
if (!submap) throw new Error('should be not null')
doc.insert(submap, 0, "a");
doc.insert(submap, 0, "b");
@ -230,11 +230,11 @@ describe('Automerge', () => {
let doc = create()
let root = "_root";
let text = doc.set(root, "text", TEXT);
let text = doc.set(root, "text", "", "text");
if (!text) throw new Error('should not be undefined')
doc.splice(text, 0, 0, "hello ")
doc.splice(text, 6, 0, ["w","o","r","l","d"])
doc.splice(text, 11, 0, [["str","!"],["str","?"]])
doc.splice(text, 11, 0, ["!","?"])
assert.deepEqual(doc.value(text, 0),["str","h"])
assert.deepEqual(doc.value(text, 1),["str","e"])
assert.deepEqual(doc.value(text, 9),["str","l"])
@ -282,7 +282,7 @@ describe('Automerge', () => {
it('should be able to splice text', () => {
let doc = create()
let text = doc.set("_root", "text", TEXT);
let text = doc.set("_root", "text", "", "text");
if (!text) throw new Error('should not be undefined')
doc.splice(text, 0, 0, "hello world");
let heads1 = doc.commit();
@ -331,7 +331,7 @@ describe('Automerge', () => {
it('local inc increments all visible counters in a sequence', () => {
let doc1 = create("aaaa")
let seq = doc1.set("_root", "seq", LIST)
let seq = doc1.set("_root", "seq", [])
if (!seq) throw new Error('Should not be undefined')
doc1.insert(seq, 0, "hello")
let doc2 = loadDoc(doc1.save(), "bbbb");
@ -363,18 +363,32 @@ describe('Automerge', () => {
doc4.free()
})
it('recursive sets are possible', () => {
let doc = create("aaaa")
let l1 = doc.make("_root","list",[{ foo: "bar"}, [1,2,3]])
let l2 = doc.insert(l1, 0, { zip: ["a", "b"] })
let l3 = doc.set("_root","info1","hello world","text")
let l4 = doc.set("_root","info2","hello world")
assert.deepEqual(doc.toJS(), {
"list": [ { zip: ["a", "b"] }, { foo: "bar"}, [ 1,2,3]],
"info1": "hello world".split(""),
"info2": "hello world"
})
doc.free()
})
it('only returns an object id when objects are created', () => {
let doc = create("aaaa")
let r1 = doc.set("_root","foo","bar")
let r2 = doc.set("_root","list",LIST)
let r2 = doc.set("_root","list",[])
let r3 = doc.set("_root","counter",10, "counter")
let r4 = doc.inc("_root","counter",1)
let r5 = doc.del("_root","counter")
if (!r2) throw new Error('should not be undefined')
let r6 = doc.insert(r2,0,10);
let r7 = doc.insert(r2,0,MAP);
let r7 = doc.insert(r2,0,{});
let r8 = doc.splice(r2,1,0,["a","b","c"]);
let r9 = doc.splice(r2,1,0,["a",LIST,MAP,"d"]);
let r9 = doc.splice(r2,1,0,["a",[],{},"d"]);
assert.deepEqual(r1,null);
assert.deepEqual(r2,"2@aaaa");
assert.deepEqual(r3,null);
@ -389,11 +403,11 @@ describe('Automerge', () => {
it('objects without properties are preserved', () => {
let doc1 = create("aaaa")
let a = doc1.set("_root","a",MAP);
let a = doc1.set("_root","a",{});
if (!a) throw new Error('should not be undefined')
let b = doc1.set("_root","b",MAP);
let b = doc1.set("_root","b",{});
if (!b) throw new Error('should not be undefined')
let c = doc1.set("_root","c",MAP);
let c = doc1.set("_root","c",{});
if (!c) throw new Error('should not be undefined')
let d = doc1.set(c,"d","dd");
let saved = doc1.save();
@ -411,7 +425,7 @@ describe('Automerge', () => {
it('should handle merging text conflicts then saving & loading', () => {
let A = create("aabbcc")
let At = A.make('_root', 'text', TEXT)
let At = A.make('_root', 'text', "", "text")
A.splice(At, 0, 0, 'hello')
let B = A.fork()
@ -462,7 +476,7 @@ describe('Automerge', () => {
let s1 = initSyncState(), s2 = initSyncState()
// make two nodes with the same changes
let list = n1.set("_root","n", LIST)
let list = n1.set("_root","n", [])
if (!list) throw new Error('undefined')
n1.commit("",0)
for (let i = 0; i < 10; i++) {
@ -486,7 +500,7 @@ describe('Automerge', () => {
let n1 = create(), n2 = create()
// make changes for n1 that n2 should request
let list = n1.set("_root","n",LIST)
let list = n1.set("_root","n",[])
if (!list) throw new Error('undefined')
n1.commit("",0)
for (let i = 0; i < 10; i++) {
@ -503,7 +517,7 @@ describe('Automerge', () => {
let n1 = create(), n2 = create()
// make changes for n1 that n2 should request
let list = n1.set("_root","n",LIST)
let list = n1.set("_root","n",[])
if (!list) throw new Error('undefined')
n1.commit("",0)
for (let i = 0; i < 10; i++) {
@ -660,7 +674,7 @@ describe('Automerge', () => {
let n1 = create('01234567'), n2 = create('89abcdef')
let s1 = initSyncState(), s2 = initSyncState(), message = null
let items = n1.set("_root", "items", LIST)
let items = n1.set("_root", "items", [])
if (!items) throw new Error('undefined')
n1.commit("",0)

View file

@ -10,8 +10,8 @@ const Automerge = require('../automerge-wasm')
const start = new Date()
let doc = Automerge.init();
let text = doc.set("_root", "text", Automerge.TEXT)
let doc = Automerge.create();
let text = doc.set("_root", "text", "", "text")
for (let i = 0; i < edits.length; i++) {
let edit = edits[i]