Merge f22dbb7036a4cff3140a55cc7580e91d6538abd8 into ad0d915794713a4cfb6c0fc32a3b998278301b36
This commit is contained in:
commit
f9bf59fba4
@ -21,12 +21,37 @@ import * as iterator from 'lib0/iterator'
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {Extract<keyof T, string>} StringKey<T>
|
||||
*
|
||||
* Like keyof, but guarantees the returned type is a subset of string.
|
||||
* `keyof` will include number and Symbol even if the input type requires string keys.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {readonly {
|
||||
* [K in StringKey<T>]: [K, T[K]];
|
||||
* }[StringKey<T>][]} EntriesOf<T>
|
||||
*
|
||||
* Converts an object schema into a readonly array containing valid key-value pairs.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This works around some weird JSDoc+TS circular reference issues: https://github.com/microsoft/TypeScript/issues/46369
|
||||
* @typedef {boolean|null|string|number|Uint8Array|JsonArray|JsonObject} Json
|
||||
* @typedef {Json[]} JsonArray
|
||||
* @typedef {{ [key: string]: Json }} JsonObject
|
||||
* @typedef {Json|AbstractType<any>|Doc} MapValue
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template {Record<string, MapValue>} T
|
||||
* @extends YEvent<YMap<T>>
|
||||
* Event that describes the changes on a YMap.
|
||||
*/
|
||||
export class YMapEvent extends YEvent {
|
||||
/**
|
||||
* @param {YMap<T>} ymap The YArray that changed.
|
||||
* @param {YMap<T>} ymap The YMap that changed.
|
||||
* @param {Transaction} transaction
|
||||
* @param {Set<any>} subs The keys that changed.
|
||||
*/
|
||||
@ -37,21 +62,21 @@ export class YMapEvent extends YEvent {
|
||||
}
|
||||
|
||||
/**
|
||||
* @template MapType
|
||||
* @template {Record<string, MapValue>} MapType
|
||||
* A shared Map implementation.
|
||||
*
|
||||
* @extends AbstractType<YMapEvent<MapType>>
|
||||
* @implements {Iterable<[string, MapType]>}
|
||||
* @implements {Iterable<[StringKey<MapType>, MapType[StringKey<MapType>]]>}
|
||||
*/
|
||||
export class YMap extends AbstractType {
|
||||
/**
|
||||
*
|
||||
* @param {Iterable<readonly [string, any]>=} entries - an optional iterable to initialize the YMap
|
||||
* @param {EntriesOf<MapType>=} entries - an optional iterable to initialize the YMap
|
||||
*/
|
||||
constructor (entries) {
|
||||
super()
|
||||
/**
|
||||
* @type {Map<string,any>?}
|
||||
* @type {Map<StringKey<MapType>, MapType[StringKey<MapType>]>?}
|
||||
* @private
|
||||
*/
|
||||
this._prelimContent = null
|
||||
@ -75,7 +100,7 @@ export class YMap extends AbstractType {
|
||||
*/
|
||||
_integrate (y, item) {
|
||||
super._integrate(y, item)
|
||||
;/** @type {Map<string, any>} */ (this._prelimContent).forEach((value, key) => {
|
||||
this._prelimContent?.forEach((value, key) => {
|
||||
this.set(key, value)
|
||||
})
|
||||
this._prelimContent = null
|
||||
@ -119,12 +144,12 @@ export class YMap extends AbstractType {
|
||||
/**
|
||||
* Transforms this Shared Type to a JSON object.
|
||||
*
|
||||
* @return {Object<string,any>}
|
||||
* @return {Partial<MapType>}
|
||||
*/
|
||||
toJSON () {
|
||||
this.doc ?? warnPrematureAccess()
|
||||
/**
|
||||
* @type {Object<string,MapType>}
|
||||
* @type {any}
|
||||
*/
|
||||
const map = {}
|
||||
this._map.forEach((item, key) => {
|
||||
@ -157,7 +182,7 @@ export class YMap extends AbstractType {
|
||||
/**
|
||||
* Returns the values for each element in the YMap Type.
|
||||
*
|
||||
* @return {IterableIterator<MapType>}
|
||||
* @return {IterableIterator<MapType[keyof MapType]>}
|
||||
*/
|
||||
values () {
|
||||
return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => v[1].content.getContent()[v[1].length - 1])
|
||||
@ -166,7 +191,7 @@ export class YMap extends AbstractType {
|
||||
/**
|
||||
* Returns an Iterator of [key, value] pairs
|
||||
*
|
||||
* @return {IterableIterator<[string, MapType]>}
|
||||
* @return {IterableIterator<[StringKey<MapType>, MapType[StringKey<MapType>]]>}
|
||||
*/
|
||||
entries () {
|
||||
return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => /** @type {any} */ ([v[0], v[1].content.getContent()[v[1].length - 1]]))
|
||||
@ -175,13 +200,13 @@ export class YMap extends AbstractType {
|
||||
/**
|
||||
* Executes a provided function on once on every key-value pair.
|
||||
*
|
||||
* @param {function(MapType,string,YMap<MapType>):void} f A function to execute on every element of this YArray.
|
||||
* @param {function(MapType[StringKey<MapType>],StringKey<MapType>,YMap<MapType>):void} f A function to execute on every element of this YArray.
|
||||
*/
|
||||
forEach (f) {
|
||||
this.doc ?? warnPrematureAccess()
|
||||
this._map.forEach((item, key) => {
|
||||
if (!item.deleted) {
|
||||
f(item.content.getContent()[item.length - 1], key, this)
|
||||
f(item.content.getContent()[item.length - 1], /** @type {StringKey<MapType>} */ (key), this)
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -189,7 +214,7 @@ export class YMap extends AbstractType {
|
||||
/**
|
||||
* Returns an Iterator of [key, value] pairs
|
||||
*
|
||||
* @return {IterableIterator<[string, MapType]>}
|
||||
* @return {IterableIterator<[StringKey<MapType>, MapType[StringKey<MapType>]]>}
|
||||
*/
|
||||
[Symbol.iterator] () {
|
||||
return this.entries()
|
||||
@ -211,17 +236,18 @@ export class YMap extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {StringKey<MapType>} Key
|
||||
* @template {MapType[Key]} Value
|
||||
* Adds or updates an element with a specified key and value.
|
||||
* @template {MapType} VAL
|
||||
*
|
||||
* @param {string} key The key of the element to add to this YMap
|
||||
* @param {VAL} value The value of the element to add
|
||||
* @return {VAL}
|
||||
* @param {Key} key The key of the element to add to this YMap
|
||||
* @param {Value} value The value of the element to add
|
||||
* @return {Value}
|
||||
*/
|
||||
set (key, value) {
|
||||
if (this.doc !== null) {
|
||||
transact(this.doc, transaction => {
|
||||
typeMapSet(transaction, this, key, /** @type {any} */ (value))
|
||||
typeMapSet(transaction, this, key, value)
|
||||
})
|
||||
} else {
|
||||
/** @type {Map<string, any>} */ (this._prelimContent).set(key, value)
|
||||
@ -230,13 +256,14 @@ export class YMap extends AbstractType {
|
||||
}
|
||||
|
||||
/**
|
||||
* @template {StringKey<MapType>} Key
|
||||
* Returns a specified element from this YMap.
|
||||
*
|
||||
* @param {string} key
|
||||
* @return {MapType|undefined}
|
||||
* @param {Key} key
|
||||
* @return {MapType[Key]|undefined}
|
||||
*/
|
||||
get (key) {
|
||||
return /** @type {any} */ (typeMapGet(this, key))
|
||||
return /** @type {MapType[Key]|undefined} */ (typeMapGet(this, key))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -268,14 +268,14 @@ export class Doc extends ObservableV2 {
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @template {Record<string, import("../internals.js").MapValue>} T
|
||||
* @param {string} [name]
|
||||
* @return {YMap<T>}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
getMap (name = '') {
|
||||
return /** @type {YMap<T>} */ (this.get(name, YMap))
|
||||
return /** @type {YMap<T>} */ /** @type {any} */ (this.get(name, YMap))
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -114,6 +114,7 @@ export const testSubdoc = _tc => {
|
||||
doc.on('subdocs', subdocs => {
|
||||
event = [Array.from(subdocs.added).map(x => x.guid), Array.from(subdocs.removed).map(x => x.guid), Array.from(subdocs.loaded).map(x => x.guid)]
|
||||
})
|
||||
/** @type {Y.Map<{ a: Y.Doc; b: Y.Doc, c: Y.Doc; }>} */
|
||||
const subdocs = doc.getMap('mysubdocs')
|
||||
const docA = new Y.Doc({ guid: 'a' })
|
||||
docA.load()
|
||||
@ -121,18 +122,18 @@ export const testSubdoc = _tc => {
|
||||
t.compare(event, [['a'], [], ['a']])
|
||||
|
||||
event = null
|
||||
subdocs.get('a').load()
|
||||
subdocs.get('a')?.load()
|
||||
t.assert(event === null)
|
||||
|
||||
event = null
|
||||
subdocs.get('a').destroy()
|
||||
subdocs.get('a')?.destroy()
|
||||
t.compare(event, [['a'], ['a'], []])
|
||||
subdocs.get('a').load()
|
||||
subdocs.get('a')?.load()
|
||||
t.compare(event, [[], [], ['a']])
|
||||
|
||||
subdocs.set('b', new Y.Doc({ guid: 'a', shouldLoad: false }))
|
||||
t.compare(event, [['a'], [], []])
|
||||
subdocs.get('b').load()
|
||||
subdocs.get('b')?.load()
|
||||
t.compare(event, [[], [], ['a']])
|
||||
|
||||
const docC = new Y.Doc({ guid: 'c' })
|
||||
@ -156,7 +157,9 @@ export const testSubdoc = _tc => {
|
||||
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
|
||||
t.compare(event, [['a', 'a', 'c'], [], []])
|
||||
|
||||
doc2.getMap('mysubdocs').get('a').load()
|
||||
/** @type {Y.Map<Record<string, Y.Doc>>} */
|
||||
const mysubdocs = doc2.getMap('mysubdocs')
|
||||
mysubdocs.get('a')?.load()
|
||||
t.compare(event, [[], [], ['a']])
|
||||
|
||||
t.compare(Array.from(doc2.getSubdocGuids()), ['a', 'c'])
|
||||
|
92
tests/test-types.ts
Normal file
92
tests/test-types.ts
Normal file
@ -0,0 +1,92 @@
|
||||
/**
|
||||
* This file serves to validate the public TypeScript API for YJS.
|
||||
*
|
||||
* It is not included in `npm run lint` or any other automated type checking, but can be used
|
||||
* by those working on YJS types to ensure that the public-facing type interface remains valid.
|
||||
*
|
||||
* Any lines which are supposed to demonstrate statements that _would_ generate type errors
|
||||
* should be clearly marked with the type error that is expected to result, to provide a
|
||||
* negative test case.
|
||||
*/
|
||||
|
||||
import * as Y from "../dist/src/index";
|
||||
|
||||
/*
|
||||
* Typed maps
|
||||
*
|
||||
* - Key names are autocompleted in first parameter of `get` and `set`.
|
||||
* - `MapType` value types are constrained to valid Y.Map contents.
|
||||
*/
|
||||
type MyType = {
|
||||
foo: string;
|
||||
bar: number | null;
|
||||
baz?: boolean;
|
||||
};
|
||||
|
||||
// Constructor argument keys & values are typechecked, and keys are autocompleted.
|
||||
// Multiple items for each key and partial initialization are allowed.
|
||||
const map = new Y.Map<MyType>([
|
||||
["foo", ""],
|
||||
["foo", "even better"],
|
||||
// ERROR: Type '["baz", number]' is not assignable to type '["foo", string] | ["bar", number | null] | ["baz", boolean | undefined]'.
|
||||
["baz", 3],
|
||||
]);
|
||||
|
||||
// Entries are still allowed to be omitted, so get() still returns <type> | undefined.
|
||||
const defaultMap = new Y.Map<MyType>();
|
||||
|
||||
// `json` has a type of `Partial<MyType>`
|
||||
const json = defaultMap.toJSON();
|
||||
|
||||
// string | undefined
|
||||
const fooValue = map.get("foo");
|
||||
// literal "hi" (string)
|
||||
const fooSet = map.set("foo", "hi");
|
||||
// number | null | undefined
|
||||
const barValue = map.get("bar");
|
||||
// ERROR: Argument of type '"hi"' is not assignable to parameter of type 'number | null'.
|
||||
const barSet = map.set("bar", "hi");
|
||||
// ERROR: Argument of type '"bomb"' is not assignable to parameter of type 'keyof MyType'.
|
||||
const missingGet = map.get("bomb");
|
||||
// Escape hatch: get<any>()
|
||||
const migrateGet = map.get<any>("extraneousKey");
|
||||
|
||||
// ERROR: Type '<type>' does not satisfy the constraint 'Record<string, MapValue>'.
|
||||
const invalidMap = new Y.Map<{ invalid: () => void }>();
|
||||
const invalidMap2 = new Y.Map<{ invalid: Blob }>();
|
||||
// Arbitrarily complex valid types are still allowed
|
||||
type ComplexType = {
|
||||
n: null;
|
||||
b: boolean;
|
||||
s: string;
|
||||
i: number;
|
||||
u: Uint8Array;
|
||||
a: null | boolean | string | number | Uint8Array[];
|
||||
};
|
||||
const complexValidType = new Y.Map<
|
||||
ComplexType & { nested: ComplexType & { deeper: ComplexType[] } }
|
||||
>();
|
||||
|
||||
/*
|
||||
* Default behavior
|
||||
*
|
||||
* Provides basic typechecking over the range of possible map values.
|
||||
*/
|
||||
const untyped = new Y.Map();
|
||||
|
||||
// MapValue | undefined
|
||||
const boop = untyped.get("default");
|
||||
// Still validates value types: ERROR: Argument of type '() => string' is not assignable to parameter of type 'MapValue'.
|
||||
const moop = untyped.set("anything", () => "whoops");
|
||||
|
||||
/*
|
||||
* `any` maps (bypass typechecking)
|
||||
*/
|
||||
const anyMap = new Y.Map<any>();
|
||||
|
||||
// any
|
||||
const fooValueAny = anyMap.get("foo");
|
||||
// literal "hi" (string)
|
||||
const fooSetAny = anyMap.set("foo", "hi");
|
||||
// Allowed because `any` unlocks cowboy mode
|
||||
const barSetAny = anyMap.set("bar", () => "hi");
|
@ -435,6 +435,7 @@ export const testUndoUntilChangePerformed = _tc => {
|
||||
const yMap = new Y.Map()
|
||||
yMap.set('hello', 'world')
|
||||
yArray.push([yMap])
|
||||
/** @type {Y.Map<{ key: string }>} */
|
||||
const yMap2 = new Y.Map()
|
||||
yMap2.set('key', 'value')
|
||||
yArray.push([yMap2])
|
||||
@ -448,7 +449,7 @@ export const testUndoUntilChangePerformed = _tc => {
|
||||
Y.transact(doc2, () => yArray2.delete(0), doc2.clientID)
|
||||
undoManager2.undo()
|
||||
undoManager.undo()
|
||||
t.compareStrings(yMap2.get('key'), 'value')
|
||||
t.compareStrings(yMap2.get('key') || '', 'value')
|
||||
}
|
||||
|
||||
/**
|
||||
@ -513,6 +514,7 @@ export const testUndoNestedUndoIssue = _tc => {
|
||||
*/
|
||||
export const testConsecutiveRedoBug = _tc => {
|
||||
const doc = new Y.Doc()
|
||||
/** @type {Y.Map<Record<string, Y.Map<any>>>} */
|
||||
const yRoot = doc.getMap()
|
||||
const undoMgr = new Y.UndoManager(yRoot)
|
||||
|
||||
@ -546,7 +548,7 @@ export const testConsecutiveRedoBug = _tc => {
|
||||
t.compare(yRoot.get('a'), undefined)
|
||||
|
||||
undoMgr.redo() // x=0, y=0
|
||||
yPoint = yRoot.get('a')
|
||||
yPoint = yRoot.get('a') || new Y.Map()
|
||||
|
||||
t.compare(yPoint.toJSON(), { x: 0, y: 0 })
|
||||
undoMgr.redo() // x=100, y=100
|
||||
|
@ -14,7 +14,7 @@ import * as prng from 'lib0/prng'
|
||||
export const testIterators = _tc => {
|
||||
const ydoc = new Y.Doc()
|
||||
/**
|
||||
* @type {Y.Map<number>}
|
||||
* @type {Y.Map<Record<string, number>>}
|
||||
*/
|
||||
const ymap = ydoc.getMap()
|
||||
// we are only checking if the type assumptions are correct
|
||||
@ -67,14 +67,16 @@ export const testMapHavingIterableAsConstructorParamTests = tc => {
|
||||
t.assert(m1.get('number') === 1)
|
||||
t.assert(m1.get('string') === 'hello')
|
||||
|
||||
/** @type {Y.Map<{ object: { x: number; }; boolean: boolean; }>} */
|
||||
const m2 = new Y.Map([
|
||||
['object', { x: 1 }],
|
||||
['boolean', true]
|
||||
])
|
||||
map0.set('m2', m2)
|
||||
t.assert(m2.get('object').x === 1)
|
||||
t.assert(m2.get('object')?.x === 1)
|
||||
t.assert(m2.get('boolean') === true)
|
||||
|
||||
/** @type {Y.Map<any>} */
|
||||
const m3 = new Y.Map([...m1, ...m2])
|
||||
map0.set('m3', m3)
|
||||
t.assert(m3.get('number') === 1)
|
||||
@ -329,7 +331,7 @@ export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => {
|
||||
*/
|
||||
export const testObserveDeepProperties = tc => {
|
||||
const { testConnector, users, map1, map2, map3 } = init(tc, { users: 4 })
|
||||
const _map1 = map1.set('map', new Y.Map())
|
||||
const _map1 = map1.set('map', /** @type {Y.Map<{ deepmap: Y.Map<any> }>} */ (new Y.Map()))
|
||||
let calls = 0
|
||||
let dmapid
|
||||
map1.observeDeep(events => {
|
||||
@ -354,10 +356,10 @@ export const testObserveDeepProperties = tc => {
|
||||
const dmap2 = _map2.get('deepmap')
|
||||
const dmap3 = _map3.get('deepmap')
|
||||
t.assert(calls > 0)
|
||||
t.assert(compareIDs(dmap1._item.id, dmap2._item.id))
|
||||
t.assert(compareIDs(dmap1._item.id, dmap3._item.id))
|
||||
t.assert(compareIDs(dmap1?._item?.id || null, dmap2._item.id))
|
||||
t.assert(compareIDs(dmap1?._item?.id || null, dmap3._item.id))
|
||||
// @ts-ignore we want the possibility of dmapid being undefined
|
||||
t.assert(compareIDs(dmap1._item.id, dmapid))
|
||||
t.assert(compareIDs(dmap1?._item?.id || null, dmapid))
|
||||
compare(users)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user