Improve typescript types ()

This commit is contained in:
alexjg 2023-01-27 17:23:13 +00:00 committed by GitHub
parent 931ee7e77b
commit f428fe0169
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 450 additions and 186 deletions

View file

@ -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
View 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
}

View file

@ -100,7 +100,7 @@ export function getWriteableCounter(
path: Prop[],
objectId: ObjID,
key: Prop
) {
): WriteableCounter {
return new WriteableCounter(value, context, path, objectId, key)
}

View file

@ -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]
}
}

View file

@ -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")
}
}

View file

@ -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)

View file

@ -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)
}

View file

@ -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

View file

@ -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
}
}

View 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

View file

@ -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] })

View file

@ -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 => {

View file

@ -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"])
})
})