Improve typescript types (#508)
This commit is contained in:
parent
931ee7e77b
commit
f428fe0169
13 changed files with 450 additions and 186 deletions
|
@ -3,4 +3,13 @@ module.exports = {
|
|||
parser: "@typescript-eslint/parser",
|
||||
plugins: ["@typescript-eslint"],
|
||||
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-unused-vars": [
|
||||
"error",
|
||||
{
|
||||
argsIgnorePattern: "^_",
|
||||
varsIgnorePattern: "^_",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
|
100
javascript/src/conflicts.ts
Normal file
100
javascript/src/conflicts.ts
Normal file
|
@ -0,0 +1,100 @@
|
|||
import { Counter, type AutomergeValue } from "./types"
|
||||
import { Text } from "./text"
|
||||
import { type AutomergeValue as UnstableAutomergeValue } from "./unstable_types"
|
||||
import { type Target, Text1Target, Text2Target } from "./proxies"
|
||||
import { mapProxy, listProxy, ValueType } from "./proxies"
|
||||
import type { Prop, ObjID } from "@automerge/automerge-wasm"
|
||||
import { Automerge } from "@automerge/automerge-wasm"
|
||||
|
||||
export type ConflictsF<T extends Target> = { [key: string]: ValueType<T> }
|
||||
export type Conflicts = ConflictsF<Text1Target>
|
||||
export type UnstableConflicts = ConflictsF<Text2Target>
|
||||
|
||||
export function stableConflictAt(
|
||||
context: Automerge,
|
||||
objectId: ObjID,
|
||||
prop: Prop
|
||||
): Conflicts | undefined {
|
||||
return conflictAt<Text1Target>(
|
||||
context,
|
||||
objectId,
|
||||
prop,
|
||||
true,
|
||||
(context: Automerge, conflictId: ObjID): AutomergeValue => {
|
||||
return new Text(context.text(conflictId))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
export function unstableConflictAt(
|
||||
context: Automerge,
|
||||
objectId: ObjID,
|
||||
prop: Prop
|
||||
): UnstableConflicts | undefined {
|
||||
return conflictAt<Text2Target>(
|
||||
context,
|
||||
objectId,
|
||||
prop,
|
||||
true,
|
||||
(context: Automerge, conflictId: ObjID): UnstableAutomergeValue => {
|
||||
return context.text(conflictId)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
function conflictAt<T extends Target>(
|
||||
context: Automerge,
|
||||
objectId: ObjID,
|
||||
prop: Prop,
|
||||
textV2: boolean,
|
||||
handleText: (a: Automerge, conflictId: ObjID) => ValueType<T>
|
||||
): ConflictsF<T> | undefined {
|
||||
const values = context.getAll(objectId, prop)
|
||||
if (values.length <= 1) {
|
||||
return
|
||||
}
|
||||
const result: ConflictsF<T> = {}
|
||||
for (const fullVal of values) {
|
||||
switch (fullVal[0]) {
|
||||
case "map":
|
||||
result[fullVal[1]] = mapProxy<T>(
|
||||
context,
|
||||
fullVal[1],
|
||||
textV2,
|
||||
[prop],
|
||||
true
|
||||
)
|
||||
break
|
||||
case "list":
|
||||
result[fullVal[1]] = listProxy<T>(
|
||||
context,
|
||||
fullVal[1],
|
||||
textV2,
|
||||
[prop],
|
||||
true
|
||||
)
|
||||
break
|
||||
case "text":
|
||||
result[fullVal[1]] = handleText(context, fullVal[1] as ObjID)
|
||||
break
|
||||
case "str":
|
||||
case "uint":
|
||||
case "int":
|
||||
case "f64":
|
||||
case "boolean":
|
||||
case "bytes":
|
||||
case "null":
|
||||
result[fullVal[2]] = fullVal[1] as ValueType<T>
|
||||
break
|
||||
case "counter":
|
||||
result[fullVal[2]] = new Counter(fullVal[1]) as ValueType<T>
|
||||
break
|
||||
case "timestamp":
|
||||
result[fullVal[2]] = new Date(fullVal[1]) as ValueType<T>
|
||||
break
|
||||
default:
|
||||
throw RangeError(`datatype ${fullVal[0]} unimplemented`)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
|
@ -100,7 +100,7 @@ export function getWriteableCounter(
|
|||
path: Prop[],
|
||||
objectId: ObjID,
|
||||
key: Prop
|
||||
) {
|
||||
): WriteableCounter {
|
||||
return new WriteableCounter(value, context, path, objectId, key)
|
||||
}
|
||||
|
||||
|
|
|
@ -14,6 +14,7 @@ export type { ChangeToEncode } from "@automerge/automerge-wasm"
|
|||
|
||||
export function UseApi(api: API) {
|
||||
for (const k in api) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-extra-semi,@typescript-eslint/no-explicit-any
|
||||
;(ApiHandler as any)[k] = (api as any)[k]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { Text } from "./text"
|
||||
import {
|
||||
Automerge,
|
||||
|
@ -6,13 +7,12 @@ import {
|
|||
type Prop,
|
||||
} from "@automerge/automerge-wasm"
|
||||
|
||||
import type {
|
||||
AutomergeValue,
|
||||
ScalarValue,
|
||||
MapValue,
|
||||
ListValue,
|
||||
TextValue,
|
||||
} from "./types"
|
||||
import type { AutomergeValue, ScalarValue, MapValue, ListValue } from "./types"
|
||||
import {
|
||||
type AutomergeValue as UnstableAutomergeValue,
|
||||
MapValue as UnstableMapValue,
|
||||
ListValue as UnstableListValue,
|
||||
} from "./unstable_types"
|
||||
import { Counter, getWriteableCounter } from "./counter"
|
||||
import {
|
||||
STATE,
|
||||
|
@ -26,19 +26,38 @@ import {
|
|||
} from "./constants"
|
||||
import { RawString } from "./raw_string"
|
||||
|
||||
type Target = {
|
||||
type TargetCommon = {
|
||||
context: Automerge
|
||||
objectId: ObjID
|
||||
path: Array<Prop>
|
||||
readonly: boolean
|
||||
heads?: Array<string>
|
||||
cache: {}
|
||||
cache: object
|
||||
trace?: any
|
||||
frozen: boolean
|
||||
textV2: boolean
|
||||
}
|
||||
|
||||
function parseListIndex(key) {
|
||||
export type Text2Target = TargetCommon & { textV2: true }
|
||||
export type Text1Target = TargetCommon & { textV2: false }
|
||||
export type Target = Text1Target | Text2Target
|
||||
|
||||
export type ValueType<T extends Target> = T extends Text2Target
|
||||
? UnstableAutomergeValue
|
||||
: T extends Text1Target
|
||||
? AutomergeValue
|
||||
: never
|
||||
type MapValueType<T extends Target> = T extends Text2Target
|
||||
? UnstableMapValue
|
||||
: T extends Text1Target
|
||||
? MapValue
|
||||
: never
|
||||
type ListValueType<T extends Target> = T extends Text2Target
|
||||
? UnstableListValue
|
||||
: T extends Text1Target
|
||||
? ListValue
|
||||
: never
|
||||
|
||||
function parseListIndex(key: any) {
|
||||
if (typeof key === "string" && /^[0-9]+$/.test(key)) key = parseInt(key, 10)
|
||||
if (typeof key !== "number") {
|
||||
return key
|
||||
|
@ -49,7 +68,10 @@ function parseListIndex(key) {
|
|||
return key
|
||||
}
|
||||
|
||||
function valueAt(target: Target, prop: Prop): AutomergeValue | undefined {
|
||||
function valueAt<T extends Target>(
|
||||
target: T,
|
||||
prop: Prop
|
||||
): ValueType<T> | undefined {
|
||||
const { context, objectId, path, readonly, heads, textV2 } = target
|
||||
const value = context.getWithType(objectId, prop, heads)
|
||||
if (value === null) {
|
||||
|
@ -61,7 +83,7 @@ function valueAt(target: Target, prop: Prop): AutomergeValue | undefined {
|
|||
case undefined:
|
||||
return
|
||||
case "map":
|
||||
return mapProxy(
|
||||
return mapProxy<T>(
|
||||
context,
|
||||
val as ObjID,
|
||||
textV2,
|
||||
|
@ -70,7 +92,7 @@ function valueAt(target: Target, prop: Prop): AutomergeValue | undefined {
|
|||
heads
|
||||
)
|
||||
case "list":
|
||||
return listProxy(
|
||||
return listProxy<T>(
|
||||
context,
|
||||
val as ObjID,
|
||||
textV2,
|
||||
|
@ -80,7 +102,7 @@ function valueAt(target: Target, prop: Prop): AutomergeValue | undefined {
|
|||
)
|
||||
case "text":
|
||||
if (textV2) {
|
||||
return context.text(val as ObjID, heads)
|
||||
return context.text(val as ObjID, heads) as ValueType<T>
|
||||
} else {
|
||||
return textProxy(
|
||||
context,
|
||||
|
@ -88,29 +110,36 @@ function valueAt(target: Target, prop: Prop): AutomergeValue | undefined {
|
|||
[...path, prop],
|
||||
readonly,
|
||||
heads
|
||||
)
|
||||
) as unknown as ValueType<T>
|
||||
}
|
||||
case "str":
|
||||
return val
|
||||
return val as ValueType<T>
|
||||
case "uint":
|
||||
return val
|
||||
return val as ValueType<T>
|
||||
case "int":
|
||||
return val
|
||||
return val as ValueType<T>
|
||||
case "f64":
|
||||
return val
|
||||
return val as ValueType<T>
|
||||
case "boolean":
|
||||
return val
|
||||
return val as ValueType<T>
|
||||
case "null":
|
||||
return null
|
||||
return null as ValueType<T>
|
||||
case "bytes":
|
||||
return val
|
||||
return val as ValueType<T>
|
||||
case "timestamp":
|
||||
return val
|
||||
return val as ValueType<T>
|
||||
case "counter": {
|
||||
if (readonly) {
|
||||
return new Counter(val as number)
|
||||
return new Counter(val as number) as ValueType<T>
|
||||
} else {
|
||||
return getWriteableCounter(val as number, context, path, objectId, prop)
|
||||
const counter: Counter = getWriteableCounter(
|
||||
val as number,
|
||||
context,
|
||||
path,
|
||||
objectId,
|
||||
prop
|
||||
)
|
||||
return counter as ValueType<T>
|
||||
}
|
||||
}
|
||||
default:
|
||||
|
@ -118,7 +147,21 @@ function valueAt(target: Target, prop: Prop): AutomergeValue | undefined {
|
|||
}
|
||||
}
|
||||
|
||||
function import_value(value: any, textV2: boolean) {
|
||||
type ImportedValue =
|
||||
| [null, "null"]
|
||||
| [number, "uint"]
|
||||
| [number, "int"]
|
||||
| [number, "f64"]
|
||||
| [number, "counter"]
|
||||
| [number, "timestamp"]
|
||||
| [string, "str"]
|
||||
| [Text | string, "text"]
|
||||
| [Uint8Array, "bytes"]
|
||||
| [Array<any>, "list"]
|
||||
| [Record<string, any>, "map"]
|
||||
| [boolean, "boolean"]
|
||||
|
||||
function import_value(value: any, textV2: boolean): ImportedValue {
|
||||
switch (typeof value) {
|
||||
case "object":
|
||||
if (value == null) {
|
||||
|
@ -170,7 +213,10 @@ function import_value(value: any, textV2: boolean) {
|
|||
}
|
||||
|
||||
const MapHandler = {
|
||||
get(target: Target, key): AutomergeValue | { handle: Automerge } {
|
||||
get<T extends Target>(
|
||||
target: T,
|
||||
key: any
|
||||
): ValueType<T> | ObjID | boolean | { handle: Automerge } {
|
||||
const { context, objectId, cache } = target
|
||||
if (key === Symbol.toStringTag) {
|
||||
return target[Symbol.toStringTag]
|
||||
|
@ -185,7 +231,7 @@ const MapHandler = {
|
|||
return cache[key]
|
||||
},
|
||||
|
||||
set(target: Target, key, val) {
|
||||
set(target: Target, key: any, val: any) {
|
||||
const { context, objectId, path, readonly, frozen, textV2 } = target
|
||||
target.cache = {} // reset cache on set
|
||||
if (val && val[OBJECT_ID]) {
|
||||
|
@ -221,8 +267,10 @@ const MapHandler = {
|
|||
}
|
||||
case "text": {
|
||||
if (textV2) {
|
||||
assertString(value)
|
||||
context.putObject(objectId, key, value)
|
||||
} else {
|
||||
assertText(value)
|
||||
const text = context.putObject(objectId, key, "")
|
||||
const proxyText = textProxy(context, text, [...path, key], readonly)
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
|
@ -251,7 +299,7 @@ const MapHandler = {
|
|||
return true
|
||||
},
|
||||
|
||||
deleteProperty(target: Target, key) {
|
||||
deleteProperty(target: Target, key: any) {
|
||||
const { context, objectId, readonly } = target
|
||||
target.cache = {} // reset cache on delete
|
||||
if (readonly) {
|
||||
|
@ -261,12 +309,12 @@ const MapHandler = {
|
|||
return true
|
||||
},
|
||||
|
||||
has(target: Target, key) {
|
||||
has(target: Target, key: any) {
|
||||
const value = this.get(target, key)
|
||||
return value !== undefined
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(target: Target, key) {
|
||||
getOwnPropertyDescriptor(target: Target, key: any) {
|
||||
// const { context, objectId } = target
|
||||
const value = this.get(target, key)
|
||||
if (typeof value !== "undefined") {
|
||||
|
@ -287,11 +335,20 @@ const MapHandler = {
|
|||
}
|
||||
|
||||
const ListHandler = {
|
||||
get(target: Target, index) {
|
||||
get<T extends Target>(
|
||||
target: T,
|
||||
index: any
|
||||
):
|
||||
| ValueType<T>
|
||||
| boolean
|
||||
| ObjID
|
||||
| { handle: Automerge }
|
||||
| number
|
||||
| ((_: any) => boolean) {
|
||||
const { context, objectId, heads } = target
|
||||
index = parseListIndex(index)
|
||||
if (index === Symbol.hasInstance) {
|
||||
return instance => {
|
||||
return (instance: any) => {
|
||||
return Array.isArray(instance)
|
||||
}
|
||||
}
|
||||
|
@ -304,13 +361,13 @@ const ListHandler = {
|
|||
if (index === STATE) return { handle: context }
|
||||
if (index === "length") return context.length(objectId, heads)
|
||||
if (typeof index === "number") {
|
||||
return valueAt(target, index)
|
||||
return valueAt(target, index) as ValueType<T>
|
||||
} else {
|
||||
return listMethods(target)[index]
|
||||
}
|
||||
},
|
||||
|
||||
set(target: Target, index, val) {
|
||||
set(target: Target, index: any, val: any) {
|
||||
const { context, objectId, path, readonly, frozen, textV2 } = target
|
||||
index = parseListIndex(index)
|
||||
if (val && val[OBJECT_ID]) {
|
||||
|
@ -334,7 +391,7 @@ const ListHandler = {
|
|||
}
|
||||
switch (datatype) {
|
||||
case "list": {
|
||||
let list
|
||||
let list: ObjID
|
||||
if (index >= context.length(objectId)) {
|
||||
list = context.insertObject(objectId, index, [])
|
||||
} else {
|
||||
|
@ -352,13 +409,15 @@ const ListHandler = {
|
|||
}
|
||||
case "text": {
|
||||
if (textV2) {
|
||||
assertString(value)
|
||||
if (index >= context.length(objectId)) {
|
||||
context.insertObject(objectId, index, value)
|
||||
} else {
|
||||
context.putObject(objectId, index, value)
|
||||
}
|
||||
} else {
|
||||
let text
|
||||
let text: ObjID
|
||||
assertText(value)
|
||||
if (index >= context.length(objectId)) {
|
||||
text = context.insertObject(objectId, index, "")
|
||||
} else {
|
||||
|
@ -370,7 +429,7 @@ const ListHandler = {
|
|||
break
|
||||
}
|
||||
case "map": {
|
||||
let map
|
||||
let map: ObjID
|
||||
if (index >= context.length(objectId)) {
|
||||
map = context.insertObject(objectId, index, {})
|
||||
} else {
|
||||
|
@ -398,7 +457,7 @@ const ListHandler = {
|
|||
return true
|
||||
},
|
||||
|
||||
deleteProperty(target: Target, index) {
|
||||
deleteProperty(target: Target, index: any) {
|
||||
const { context, objectId } = target
|
||||
index = parseListIndex(index)
|
||||
const elem = context.get(objectId, index)
|
||||
|
@ -411,7 +470,7 @@ const ListHandler = {
|
|||
return true
|
||||
},
|
||||
|
||||
has(target: Target, index) {
|
||||
has(target: Target, index: any) {
|
||||
const { context, objectId, heads } = target
|
||||
index = parseListIndex(index)
|
||||
if (typeof index === "number") {
|
||||
|
@ -420,7 +479,7 @@ const ListHandler = {
|
|||
return index === "length"
|
||||
},
|
||||
|
||||
getOwnPropertyDescriptor(target: Target, index) {
|
||||
getOwnPropertyDescriptor(target: Target, index: any) {
|
||||
const { context, objectId, heads } = target
|
||||
|
||||
if (index === "length")
|
||||
|
@ -434,7 +493,7 @@ const ListHandler = {
|
|||
return { configurable: true, enumerable: true, value }
|
||||
},
|
||||
|
||||
getPrototypeOf(target) {
|
||||
getPrototypeOf(target: Target) {
|
||||
return Object.getPrototypeOf(target)
|
||||
},
|
||||
ownKeys(/*target*/): string[] {
|
||||
|
@ -476,14 +535,14 @@ const TextHandler = Object.assign({}, ListHandler, {
|
|||
},
|
||||
})
|
||||
|
||||
export function mapProxy(
|
||||
export function mapProxy<T extends Target>(
|
||||
context: Automerge,
|
||||
objectId: ObjID,
|
||||
textV2: boolean,
|
||||
path?: Prop[],
|
||||
readonly?: boolean,
|
||||
heads?: Heads
|
||||
): MapValue {
|
||||
): MapValueType<T> {
|
||||
const target: Target = {
|
||||
context,
|
||||
objectId,
|
||||
|
@ -496,19 +555,19 @@ export function mapProxy(
|
|||
}
|
||||
const proxied = {}
|
||||
Object.assign(proxied, target)
|
||||
let result = new Proxy(proxied, MapHandler)
|
||||
const result = new Proxy(proxied, MapHandler)
|
||||
// conversion through unknown is necessary because the types are so different
|
||||
return result as unknown as MapValue
|
||||
return result as unknown as MapValueType<T>
|
||||
}
|
||||
|
||||
export function listProxy(
|
||||
export function listProxy<T extends Target>(
|
||||
context: Automerge,
|
||||
objectId: ObjID,
|
||||
textV2: boolean,
|
||||
path?: Prop[],
|
||||
readonly?: boolean,
|
||||
heads?: Heads
|
||||
): ListValue {
|
||||
): ListValueType<T> {
|
||||
const target: Target = {
|
||||
context,
|
||||
objectId,
|
||||
|
@ -521,17 +580,22 @@ export function listProxy(
|
|||
}
|
||||
const proxied = []
|
||||
Object.assign(proxied, target)
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return new Proxy(proxied, ListHandler) as unknown as ListValue
|
||||
}
|
||||
|
||||
interface TextProxy extends Text {
|
||||
splice: (index: any, del: any, ...vals: any[]) => void
|
||||
}
|
||||
|
||||
export function textProxy(
|
||||
context: Automerge,
|
||||
objectId: ObjID,
|
||||
path?: Prop[],
|
||||
readonly?: boolean,
|
||||
heads?: Heads
|
||||
): TextValue {
|
||||
): TextProxy {
|
||||
const target: Target = {
|
||||
context,
|
||||
objectId,
|
||||
|
@ -542,7 +606,9 @@ export function textProxy(
|
|||
cache: {},
|
||||
textV2: false,
|
||||
}
|
||||
return new Proxy(target, TextHandler) as unknown as TextValue
|
||||
const proxied = {}
|
||||
Object.assign(proxied, target)
|
||||
return new Proxy(proxied, TextHandler) as unknown as TextProxy
|
||||
}
|
||||
|
||||
export function rootProxy<T>(
|
||||
|
@ -554,10 +620,10 @@ export function rootProxy<T>(
|
|||
return <any>mapProxy(context, "_root", textV2, [], !!readonly)
|
||||
}
|
||||
|
||||
function listMethods(target: Target) {
|
||||
function listMethods<T extends Target>(target: T) {
|
||||
const { context, objectId, path, readonly, frozen, heads, textV2 } = target
|
||||
const methods = {
|
||||
deleteAt(index, numDelete) {
|
||||
deleteAt(index: number, numDelete: number) {
|
||||
if (typeof numDelete === "number") {
|
||||
context.splice(objectId, index, numDelete)
|
||||
} else {
|
||||
|
@ -572,8 +638,20 @@ function listMethods(target: Target) {
|
|||
start = parseListIndex(start || 0)
|
||||
end = parseListIndex(end || length)
|
||||
for (let i = start; i < Math.min(end, length); i++) {
|
||||
if (datatype === "text" || datatype === "list" || datatype === "map") {
|
||||
if (datatype === "list" || datatype === "map") {
|
||||
context.putObject(objectId, i, value)
|
||||
} else if (datatype === "text") {
|
||||
if (textV2) {
|
||||
assertString(value)
|
||||
context.putObject(objectId, i, value)
|
||||
} else {
|
||||
assertText(value)
|
||||
const text = context.putObject(objectId, i, "")
|
||||
const proxyText = textProxy(context, text, [...path, i], readonly)
|
||||
for (let i = 0; i < value.length; i++) {
|
||||
proxyText[i] = value.get(i)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
context.put(objectId, i, value, datatype)
|
||||
}
|
||||
|
@ -581,7 +659,7 @@ function listMethods(target: Target) {
|
|||
return this
|
||||
},
|
||||
|
||||
indexOf(o, start = 0) {
|
||||
indexOf(o: any, start = 0) {
|
||||
const length = context.length(objectId)
|
||||
for (let i = start; i < length; i++) {
|
||||
const value = context.getWithType(objectId, i, heads)
|
||||
|
@ -592,7 +670,7 @@ function listMethods(target: Target) {
|
|||
return -1
|
||||
},
|
||||
|
||||
insertAt(index, ...values) {
|
||||
insertAt(index: number, ...values: any[]) {
|
||||
this.splice(index, 0, ...values)
|
||||
return this
|
||||
},
|
||||
|
@ -607,7 +685,7 @@ function listMethods(target: Target) {
|
|||
return last
|
||||
},
|
||||
|
||||
push(...values) {
|
||||
push(...values: any[]) {
|
||||
const len = context.length(objectId)
|
||||
this.splice(len, 0, ...values)
|
||||
return context.length(objectId)
|
||||
|
@ -620,7 +698,7 @@ function listMethods(target: Target) {
|
|||
return first
|
||||
},
|
||||
|
||||
splice(index, del, ...vals) {
|
||||
splice(index: any, del: any, ...vals: any[]) {
|
||||
index = parseListIndex(index)
|
||||
del = parseListIndex(del)
|
||||
for (const val of vals) {
|
||||
|
@ -638,9 +716,9 @@ function listMethods(target: Target) {
|
|||
"Sequence object cannot be modified outside of a change block"
|
||||
)
|
||||
}
|
||||
const result: AutomergeValue[] = []
|
||||
const result: ValueType<T>[] = []
|
||||
for (let i = 0; i < del; i++) {
|
||||
const value = valueAt(target, index)
|
||||
const value = valueAt<T>(target, index)
|
||||
if (value !== undefined) {
|
||||
result.push(value)
|
||||
}
|
||||
|
@ -663,6 +741,7 @@ function listMethods(target: Target) {
|
|||
}
|
||||
case "text": {
|
||||
if (textV2) {
|
||||
assertString(value)
|
||||
context.insertObject(objectId, index, value)
|
||||
} else {
|
||||
const text = context.insertObject(objectId, index, "")
|
||||
|
@ -698,7 +777,7 @@ function listMethods(target: Target) {
|
|||
return result
|
||||
},
|
||||
|
||||
unshift(...values) {
|
||||
unshift(...values: any) {
|
||||
this.splice(0, 0, ...values)
|
||||
return context.length(objectId)
|
||||
},
|
||||
|
@ -749,11 +828,11 @@ function listMethods(target: Target) {
|
|||
return iterator
|
||||
},
|
||||
|
||||
toArray(): AutomergeValue[] {
|
||||
const list: AutomergeValue = []
|
||||
let value
|
||||
toArray(): ValueType<T>[] {
|
||||
const list: Array<ValueType<T>> = []
|
||||
let value: ValueType<T> | undefined
|
||||
do {
|
||||
value = valueAt(target, list.length)
|
||||
value = valueAt<T>(target, list.length)
|
||||
if (value !== undefined) {
|
||||
list.push(value)
|
||||
}
|
||||
|
@ -762,7 +841,7 @@ function listMethods(target: Target) {
|
|||
return list
|
||||
},
|
||||
|
||||
map<T>(f: (AutomergeValue, number) => T): T[] {
|
||||
map<U>(f: (_a: ValueType<T>, _n: number) => U): U[] {
|
||||
return this.toArray().map(f)
|
||||
},
|
||||
|
||||
|
@ -774,24 +853,26 @@ function listMethods(target: Target) {
|
|||
return this.toArray().toLocaleString()
|
||||
},
|
||||
|
||||
forEach(f: (AutomergeValue, number) => undefined) {
|
||||
forEach(f: (_a: ValueType<T>, _n: number) => undefined) {
|
||||
return this.toArray().forEach(f)
|
||||
},
|
||||
|
||||
// todo: real concat function is different
|
||||
concat(other: AutomergeValue[]): AutomergeValue[] {
|
||||
concat(other: ValueType<T>[]): ValueType<T>[] {
|
||||
return this.toArray().concat(other)
|
||||
},
|
||||
|
||||
every(f: (AutomergeValue, number) => boolean): boolean {
|
||||
every(f: (_a: ValueType<T>, _n: number) => boolean): boolean {
|
||||
return this.toArray().every(f)
|
||||
},
|
||||
|
||||
filter(f: (AutomergeValue, number) => boolean): AutomergeValue[] {
|
||||
filter(f: (_a: ValueType<T>, _n: number) => boolean): ValueType<T>[] {
|
||||
return this.toArray().filter(f)
|
||||
},
|
||||
|
||||
find(f: (AutomergeValue, number) => boolean): AutomergeValue | undefined {
|
||||
find(
|
||||
f: (_a: ValueType<T>, _n: number) => boolean
|
||||
): ValueType<T> | undefined {
|
||||
let index = 0
|
||||
for (const v of this) {
|
||||
if (f(v, index)) {
|
||||
|
@ -801,7 +882,7 @@ function listMethods(target: Target) {
|
|||
}
|
||||
},
|
||||
|
||||
findIndex(f: (AutomergeValue, number) => boolean): number {
|
||||
findIndex(f: (_a: ValueType<T>, _n: number) => boolean): number {
|
||||
let index = 0
|
||||
for (const v of this) {
|
||||
if (f(v, index)) {
|
||||
|
@ -812,7 +893,7 @@ function listMethods(target: Target) {
|
|||
return -1
|
||||
},
|
||||
|
||||
includes(elem: AutomergeValue): boolean {
|
||||
includes(elem: ValueType<T>): boolean {
|
||||
return this.find(e => e === elem) !== undefined
|
||||
},
|
||||
|
||||
|
@ -820,29 +901,30 @@ function listMethods(target: Target) {
|
|||
return this.toArray().join(sep)
|
||||
},
|
||||
|
||||
// todo: remove the any
|
||||
reduce<T>(f: (any, AutomergeValue) => T, initalValue?: T): T | undefined {
|
||||
return this.toArray().reduce(f, initalValue)
|
||||
reduce<U>(
|
||||
f: (acc: U, currentValue: ValueType<T>) => U,
|
||||
initialValue: U
|
||||
): U | undefined {
|
||||
return this.toArray().reduce<U>(f, initialValue)
|
||||
},
|
||||
|
||||
// todo: remove the any
|
||||
reduceRight<T>(
|
||||
f: (any, AutomergeValue) => T,
|
||||
initalValue?: T
|
||||
): T | undefined {
|
||||
return this.toArray().reduceRight(f, initalValue)
|
||||
reduceRight<U>(
|
||||
f: (acc: U, item: ValueType<T>) => U,
|
||||
initialValue: U
|
||||
): U | undefined {
|
||||
return this.toArray().reduceRight(f, initialValue)
|
||||
},
|
||||
|
||||
lastIndexOf(search: AutomergeValue, fromIndex = +Infinity): number {
|
||||
lastIndexOf(search: ValueType<T>, fromIndex = +Infinity): number {
|
||||
// this can be faster
|
||||
return this.toArray().lastIndexOf(search, fromIndex)
|
||||
},
|
||||
|
||||
slice(index?: number, num?: number): AutomergeValue[] {
|
||||
slice(index?: number, num?: number): ValueType<T>[] {
|
||||
return this.toArray().slice(index, num)
|
||||
},
|
||||
|
||||
some(f: (AutomergeValue, number) => boolean): boolean {
|
||||
some(f: (v: ValueType<T>, i: number) => boolean): boolean {
|
||||
let index = 0
|
||||
for (const v of this) {
|
||||
if (f(v, index)) {
|
||||
|
@ -869,7 +951,7 @@ function listMethods(target: Target) {
|
|||
function textMethods(target: Target) {
|
||||
const { context, objectId, heads } = target
|
||||
const methods = {
|
||||
set(index: number, value) {
|
||||
set(index: number, value: any) {
|
||||
return (this[index] = value)
|
||||
},
|
||||
get(index: number): AutomergeValue {
|
||||
|
@ -902,10 +984,22 @@ function textMethods(target: Target) {
|
|||
toJSON(): string {
|
||||
return this.toString()
|
||||
},
|
||||
indexOf(o, start = 0) {
|
||||
indexOf(o: any, start = 0) {
|
||||
const text = context.text(objectId)
|
||||
return text.indexOf(o, start)
|
||||
},
|
||||
}
|
||||
return methods
|
||||
}
|
||||
|
||||
function assertText(value: Text | string): asserts value is Text {
|
||||
if (!(value instanceof Text)) {
|
||||
throw new Error("value was not a Text instance")
|
||||
}
|
||||
}
|
||||
|
||||
function assertString(value: Text | string): asserts value is string {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error("value was not a string")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/** @hidden **/
|
||||
export { /** @hidden */ uuid } from "./uuid"
|
||||
|
||||
import { rootProxy, listProxy, mapProxy, textProxy } from "./proxies"
|
||||
import { rootProxy } from "./proxies"
|
||||
import { STATE } from "./constants"
|
||||
|
||||
import {
|
||||
|
@ -20,10 +20,10 @@ export {
|
|||
type Patch,
|
||||
type PatchCallback,
|
||||
type ScalarValue,
|
||||
Text,
|
||||
} from "./types"
|
||||
|
||||
import { Text } from "./text"
|
||||
export { Text } from "./text"
|
||||
|
||||
import type {
|
||||
API,
|
||||
|
@ -54,6 +54,8 @@ import { RawString } from "./raw_string"
|
|||
|
||||
import { _state, _is_proxy, _trace, _obj } from "./internal_state"
|
||||
|
||||
import { stableConflictAt } from "./conflicts"
|
||||
|
||||
/** Options passed to {@link change}, and {@link emptyChange}
|
||||
* @typeParam T - The type of value contained in the document
|
||||
*/
|
||||
|
@ -71,13 +73,36 @@ export type ChangeOptions<T> = {
|
|||
*/
|
||||
export type ApplyOptions<T> = { patchCallback?: PatchCallback<T> }
|
||||
|
||||
/**
|
||||
* A List is an extended Array that adds the two helper methods `deleteAt` and `insertAt`.
|
||||
*/
|
||||
export interface List<T> extends Array<T> {
|
||||
insertAt(index: number, ...args: T[]): List<T>
|
||||
deleteAt(index: number, numDelete?: number): List<T>
|
||||
}
|
||||
|
||||
/**
|
||||
* To extend an arbitrary type, we have to turn any arrays that are part of the type's definition into Lists.
|
||||
* So we recurse through the properties of T, turning any Arrays we find into Lists.
|
||||
*/
|
||||
export type Extend<T> =
|
||||
// is it an array? make it a list (we recursively extend the type of the array's elements as well)
|
||||
T extends Array<infer T>
|
||||
? List<Extend<T>>
|
||||
: // is it an object? recursively extend all of its properties
|
||||
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||
T extends Object
|
||||
? { [P in keyof T]: Extend<T[P]> }
|
||||
: // otherwise leave the type alone
|
||||
T
|
||||
|
||||
/**
|
||||
* Function which is called by {@link change} when making changes to a `Doc<T>`
|
||||
* @typeParam T - The type of value contained in the document
|
||||
*
|
||||
* This function may mutate `doc`
|
||||
*/
|
||||
export type ChangeFn<T> = (doc: T) => void
|
||||
export type ChangeFn<T> = (doc: Extend<T>) => void
|
||||
|
||||
/** @hidden **/
|
||||
export interface State<T> {
|
||||
|
@ -136,11 +161,12 @@ export function init<T>(_opts?: ActorId | InitOptions<T>): Doc<T> {
|
|||
const handle = ApiHandler.create(opts.enableTextV2 || false, opts.actor)
|
||||
handle.enablePatches(true)
|
||||
handle.enableFreeze(!!opts.freeze)
|
||||
handle.registerDatatype("counter", (n: any) => new Counter(n))
|
||||
let textV2 = opts.enableTextV2 || false
|
||||
handle.registerDatatype("counter", (n: number) => new Counter(n))
|
||||
const textV2 = opts.enableTextV2 || false
|
||||
if (textV2) {
|
||||
handle.registerDatatype("str", (n: string) => new RawString(n))
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
handle.registerDatatype("text", (n: any) => new Text(n))
|
||||
}
|
||||
const doc = handle.materialize("/", undefined, {
|
||||
|
@ -204,7 +230,7 @@ export function clone<T>(
|
|||
|
||||
// `change` uses the presence of state.heads to determine if we are in a view
|
||||
// set it to undefined to indicate that this is a full fat document
|
||||
const { heads: oldHeads, ...stateSansHeads } = state
|
||||
const { heads: _oldHeads, ...stateSansHeads } = state
|
||||
return handle.applyPatches(doc, { ...stateSansHeads, handle })
|
||||
}
|
||||
|
||||
|
@ -343,7 +369,7 @@ function _change<T>(
|
|||
try {
|
||||
state.heads = heads
|
||||
const root: T = rootProxy(state.handle, state.textV2)
|
||||
callback(root)
|
||||
callback(root as Extend<T>)
|
||||
if (state.handle.pendingOps() === 0) {
|
||||
state.heads = undefined
|
||||
return doc
|
||||
|
@ -541,62 +567,6 @@ export function getActorId<T>(doc: Doc<T>): ActorId {
|
|||
*/
|
||||
type Conflicts = { [key: string]: AutomergeValue }
|
||||
|
||||
function conflictAt(
|
||||
context: Automerge,
|
||||
objectId: ObjID,
|
||||
prop: Prop,
|
||||
textV2: boolean
|
||||
): Conflicts | undefined {
|
||||
const values = context.getAll(objectId, prop)
|
||||
if (values.length <= 1) {
|
||||
return
|
||||
}
|
||||
const result: Conflicts = {}
|
||||
for (const fullVal of values) {
|
||||
switch (fullVal[0]) {
|
||||
case "map":
|
||||
result[fullVal[1]] = mapProxy(context, fullVal[1], textV2, [prop], true)
|
||||
break
|
||||
case "list":
|
||||
result[fullVal[1]] = listProxy(
|
||||
context,
|
||||
fullVal[1],
|
||||
textV2,
|
||||
[prop],
|
||||
true
|
||||
)
|
||||
break
|
||||
case "text":
|
||||
if (textV2) {
|
||||
result[fullVal[1]] = context.text(fullVal[1])
|
||||
} else {
|
||||
result[fullVal[1]] = textProxy(context, objectId, [prop], true)
|
||||
}
|
||||
break
|
||||
//case "table":
|
||||
//case "cursor":
|
||||
case "str":
|
||||
case "uint":
|
||||
case "int":
|
||||
case "f64":
|
||||
case "boolean":
|
||||
case "bytes":
|
||||
case "null":
|
||||
result[fullVal[2]] = fullVal[1]
|
||||
break
|
||||
case "counter":
|
||||
result[fullVal[2]] = new Counter(fullVal[1])
|
||||
break
|
||||
case "timestamp":
|
||||
result[fullVal[2]] = new Date(fullVal[1])
|
||||
break
|
||||
default:
|
||||
throw RangeError(`datatype ${fullVal[0]} unimplemented`)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the conflicts associated with a property
|
||||
*
|
||||
|
@ -646,9 +616,12 @@ export function getConflicts<T>(
|
|||
prop: Prop
|
||||
): Conflicts | undefined {
|
||||
const state = _state(doc, false)
|
||||
if (state.textV2) {
|
||||
throw new Error("use unstable.getConflicts for an unstable document")
|
||||
}
|
||||
const objectId = _obj(doc)
|
||||
if (objectId != null) {
|
||||
return conflictAt(state.handle, objectId, prop, state.textV2)
|
||||
return stableConflictAt(state.handle, objectId, prop)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
|
@ -672,6 +645,7 @@ export function getLastLocalChange<T>(doc: Doc<T>): Change | undefined {
|
|||
* This is useful to determine if something is actually an automerge document,
|
||||
* if `doc` is not an automerge document this will return null.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export function getObjectId(doc: any, prop?: Prop): ObjID | null {
|
||||
if (prop) {
|
||||
const state = _state(doc, false)
|
||||
|
|
|
@ -3,9 +3,12 @@ import { TEXT, STATE } from "./constants"
|
|||
import type { InternalState } from "./internal_state"
|
||||
|
||||
export class Text {
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
elems: Array<any>
|
||||
str: string | undefined
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
spans: Array<any> | undefined;
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[STATE]?: InternalState<any>
|
||||
|
||||
constructor(text?: string | string[] | Value[]) {
|
||||
|
@ -25,6 +28,7 @@ export class Text {
|
|||
return this.elems.length
|
||||
}
|
||||
|
||||
//eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
get(index: number): any {
|
||||
return this.elems[index]
|
||||
}
|
||||
|
@ -73,7 +77,7 @@ export class Text {
|
|||
* For example, the value `['a', 'b', {x: 3}, 'c', 'd']` has spans:
|
||||
* `=> ['ab', {x: 3}, 'cd']`
|
||||
*/
|
||||
toSpans(): Array<Value | Object> {
|
||||
toSpans(): Array<Value | object> {
|
||||
if (!this.spans) {
|
||||
this.spans = []
|
||||
let chars = ""
|
||||
|
@ -118,7 +122,7 @@ export class Text {
|
|||
/**
|
||||
* Inserts new list items `values` starting at position `index`.
|
||||
*/
|
||||
insertAt(index: number, ...values: Array<Value | Object>) {
|
||||
insertAt(index: number, ...values: Array<Value | object>) {
|
||||
if (this[STATE]) {
|
||||
throw new RangeError(
|
||||
"object cannot be modified outside of a change block"
|
||||
|
@ -140,7 +144,7 @@ export class Text {
|
|||
this.elems.splice(index, numDelete)
|
||||
}
|
||||
|
||||
map<T>(callback: (e: Value | Object) => T) {
|
||||
map<T>(callback: (e: Value | object) => T) {
|
||||
this.elems.map(callback)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
export { Text } from "./text"
|
||||
import { Text } from "./text"
|
||||
export { Counter } from "./counter"
|
||||
export { Int, Uint, Float64 } from "./numbers"
|
||||
|
||||
|
@ -10,9 +11,9 @@ export type AutomergeValue =
|
|||
| ScalarValue
|
||||
| { [key: string]: AutomergeValue }
|
||||
| Array<AutomergeValue>
|
||||
| Text
|
||||
export type MapValue = { [key: string]: AutomergeValue }
|
||||
export type ListValue = Array<AutomergeValue>
|
||||
export type TextValue = Array<AutomergeValue>
|
||||
export type ScalarValue =
|
||||
| string
|
||||
| number
|
||||
|
|
|
@ -22,9 +22,9 @@
|
|||
* This leads to the following differences from `stable`:
|
||||
*
|
||||
* * There is no `unstable.Text` class, all strings are text objects
|
||||
* * Reading strings in a `future` document is the same as reading any other
|
||||
* * Reading strings in an `unstable` document is the same as reading any other
|
||||
* javascript string
|
||||
* * To modify strings in a `future` document use {@link splice}
|
||||
* * To modify strings in an `unstable` document use {@link splice}
|
||||
* * The {@link AutomergeValue} type does not include the {@link Text}
|
||||
* class but the {@link RawString} class is included in the {@link ScalarValue}
|
||||
* type
|
||||
|
@ -35,7 +35,6 @@
|
|||
*
|
||||
* @module
|
||||
*/
|
||||
import { Counter } from "./types"
|
||||
|
||||
export {
|
||||
Counter,
|
||||
|
@ -45,27 +44,14 @@ export {
|
|||
Float64,
|
||||
type Patch,
|
||||
type PatchCallback,
|
||||
} from "./types"
|
||||
type AutomergeValue,
|
||||
type ScalarValue,
|
||||
} from "./unstable_types"
|
||||
|
||||
import type { PatchCallback } from "./stable"
|
||||
|
||||
export type AutomergeValue =
|
||||
| ScalarValue
|
||||
| { [key: string]: AutomergeValue }
|
||||
| Array<AutomergeValue>
|
||||
export type MapValue = { [key: string]: AutomergeValue }
|
||||
export type ListValue = Array<AutomergeValue>
|
||||
export type ScalarValue =
|
||||
| string
|
||||
| number
|
||||
| null
|
||||
| boolean
|
||||
| Date
|
||||
| Counter
|
||||
| Uint8Array
|
||||
| RawString
|
||||
|
||||
export type Conflicts = { [key: string]: AutomergeValue }
|
||||
import { type UnstableConflicts as Conflicts } from "./conflicts"
|
||||
import { unstableConflictAt } from "./conflicts"
|
||||
|
||||
export type {
|
||||
PutPatch,
|
||||
|
@ -125,7 +111,6 @@ export { RawString } from "./raw_string"
|
|||
export const getBackend = stable.getBackend
|
||||
|
||||
import { _is_proxy, _state, _obj } from "./internal_state"
|
||||
import { RawString } from "./raw_string"
|
||||
|
||||
/**
|
||||
* Create a new automerge document
|
||||
|
@ -137,7 +122,7 @@ import { RawString } from "./raw_string"
|
|||
* random actor ID
|
||||
*/
|
||||
export function init<T>(_opts?: ActorId | InitOptions<T>): Doc<T> {
|
||||
let opts = importOpts(_opts)
|
||||
const opts = importOpts(_opts)
|
||||
opts.enableTextV2 = true
|
||||
return stable.init(opts)
|
||||
}
|
||||
|
@ -161,7 +146,7 @@ export function clone<T>(
|
|||
doc: Doc<T>,
|
||||
_opts?: ActorId | InitOptions<T>
|
||||
): Doc<T> {
|
||||
let opts = importOpts(_opts)
|
||||
const opts = importOpts(_opts)
|
||||
opts.enableTextV2 = true
|
||||
return stable.clone(doc, opts)
|
||||
}
|
||||
|
@ -296,6 +281,14 @@ export function getConflicts<T>(
|
|||
doc: Doc<T>,
|
||||
prop: stable.Prop
|
||||
): Conflicts | undefined {
|
||||
// this function only exists to get the types to line up with future.AutomergeValue
|
||||
return stable.getConflicts(doc, prop)
|
||||
const state = _state(doc, false)
|
||||
if (!state.textV2) {
|
||||
throw new Error("use getConflicts for a stable document")
|
||||
}
|
||||
const objectId = _obj(doc)
|
||||
if (objectId != null) {
|
||||
return unstableConflictAt(state.handle, objectId, prop)
|
||||
} else {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
|
30
javascript/src/unstable_types.ts
Normal file
30
javascript/src/unstable_types.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { Counter } from "./types"
|
||||
|
||||
export {
|
||||
Counter,
|
||||
type Doc,
|
||||
Int,
|
||||
Uint,
|
||||
Float64,
|
||||
type Patch,
|
||||
type PatchCallback,
|
||||
} from "./types"
|
||||
|
||||
import { RawString } from "./raw_string"
|
||||
export { RawString } from "./raw_string"
|
||||
|
||||
export type AutomergeValue =
|
||||
| ScalarValue
|
||||
| { [key: string]: AutomergeValue }
|
||||
| Array<AutomergeValue>
|
||||
export type MapValue = { [key: string]: AutomergeValue }
|
||||
export type ListValue = Array<AutomergeValue>
|
||||
export type ScalarValue =
|
||||
| string
|
||||
| number
|
||||
| null
|
||||
| boolean
|
||||
| Date
|
||||
| Counter
|
||||
| Uint8Array
|
||||
| RawString
|
|
@ -267,7 +267,6 @@ describe("Automerge", () => {
|
|||
})
|
||||
assert.deepEqual(doc5, { list: [2, 1, 9, 10, 3, 11, 12] })
|
||||
let doc6 = Automerge.change(doc5, d => {
|
||||
// @ts-ignore
|
||||
d.list.insertAt(3, 100, 101)
|
||||
})
|
||||
assert.deepEqual(doc6, { list: [2, 1, 9, 100, 101, 10, 3, 11, 12] })
|
||||
|
|
|
@ -461,12 +461,12 @@ describe("Automerge", () => {
|
|||
s1 = Automerge.change(s1, "set foo", doc => {
|
||||
doc.foo = "bar"
|
||||
})
|
||||
let deleted
|
||||
let deleted: any
|
||||
s1 = Automerge.change(s1, "del foo", doc => {
|
||||
deleted = delete doc.foo
|
||||
})
|
||||
assert.strictEqual(deleted, true)
|
||||
let deleted2
|
||||
let deleted2: any
|
||||
assert.doesNotThrow(() => {
|
||||
s1 = Automerge.change(s1, "del baz", doc => {
|
||||
deleted2 = delete doc.baz
|
||||
|
@ -515,7 +515,7 @@ describe("Automerge", () => {
|
|||
s1 = Automerge.change(s1, doc => {
|
||||
doc.nested = {}
|
||||
})
|
||||
let id = Automerge.getObjectId(s1.nested)
|
||||
Automerge.getObjectId(s1.nested)
|
||||
assert.strictEqual(
|
||||
OPID_PATTERN.test(Automerge.getObjectId(s1.nested)!),
|
||||
true
|
||||
|
@ -975,6 +975,7 @@ describe("Automerge", () => {
|
|||
it("should allow adding and removing list elements in the same change callback", () => {
|
||||
let s1 = Automerge.change(
|
||||
Automerge.init<{ noodles: Array<string> }>(),
|
||||
// @ts-ignore
|
||||
doc => (doc.noodles = [])
|
||||
)
|
||||
s1 = Automerge.change(s1, doc => {
|
||||
|
|
|
@ -38,4 +38,62 @@ describe("stable/unstable interop", () => {
|
|||
stableDoc = unstable.merge(stableDoc, unstableDoc)
|
||||
assert.deepStrictEqual(stableDoc.text, "abc")
|
||||
})
|
||||
|
||||
it("should show conflicts on text objects", () => {
|
||||
let doc1 = stable.from({ text: new stable.Text("abc") }, "bb")
|
||||
let doc2 = stable.from({ text: new stable.Text("def") }, "aa")
|
||||
doc1 = stable.merge(doc1, doc2)
|
||||
let conflicts = stable.getConflicts(doc1, "text")!
|
||||
assert.equal(conflicts["1@bb"]!.toString(), "abc")
|
||||
assert.equal(conflicts["1@aa"]!.toString(), "def")
|
||||
|
||||
let unstableDoc = unstable.init<any>()
|
||||
unstableDoc = unstable.merge(unstableDoc, doc1)
|
||||
let conflicts2 = unstable.getConflicts(unstableDoc, "text")!
|
||||
assert.equal(conflicts2["1@bb"]!.toString(), "abc")
|
||||
assert.equal(conflicts2["1@aa"]!.toString(), "def")
|
||||
})
|
||||
|
||||
it("should allow filling a list with text in stable", () => {
|
||||
let doc = stable.from<{ list: Array<stable.Text | null> }>({
|
||||
list: [null, null, null],
|
||||
})
|
||||
doc = stable.change(doc, doc => {
|
||||
doc.list.fill(new stable.Text("abc"), 0, 3)
|
||||
})
|
||||
assert.deepStrictEqual(doc.list, [
|
||||
new stable.Text("abc"),
|
||||
new stable.Text("abc"),
|
||||
new stable.Text("abc"),
|
||||
])
|
||||
})
|
||||
|
||||
it("should allow filling a list with text in unstable", () => {
|
||||
let doc = unstable.from<{ list: Array<string | null> }>({
|
||||
list: [null, null, null],
|
||||
})
|
||||
doc = stable.change(doc, doc => {
|
||||
doc.list.fill("abc", 0, 3)
|
||||
})
|
||||
assert.deepStrictEqual(doc.list, ["abc", "abc", "abc"])
|
||||
})
|
||||
|
||||
it("should allow splicing text into a list on stable", () => {
|
||||
let doc = stable.from<{ list: Array<stable.Text> }>({ list: [] })
|
||||
doc = stable.change(doc, doc => {
|
||||
doc.list.splice(0, 0, new stable.Text("abc"), new stable.Text("def"))
|
||||
})
|
||||
assert.deepStrictEqual(doc.list, [
|
||||
new stable.Text("abc"),
|
||||
new stable.Text("def"),
|
||||
])
|
||||
})
|
||||
|
||||
it("should allow splicing text into a list on unstable", () => {
|
||||
let doc = unstable.from<{ list: Array<string> }>({ list: [] })
|
||||
doc = unstable.change(doc, doc => {
|
||||
doc.list.splice(0, 0, "abc", "def")
|
||||
})
|
||||
assert.deepStrictEqual(doc.list, ["abc", "def"])
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue