Compare commits

...

8 Commits

Author SHA1 Message Date
Kevin Jahns
90675be3ab 13.6.13 2024-02-29 17:37:25 +01:00
Kevin Jahns
541306b254 migrate to ObservableV2 2024-02-29 17:08:57 +01:00
Kevin Jahns
53173a9ea7 Merge branch 'mylesj-feat/undomanager-doingstackitem' 2024-02-29 14:47:00 +01:00
Kevin Jahns
29fa60ccf9 [Undo] add UndoManager.currStackItem 2024-02-29 14:46:43 +01:00
Myles J
917261a1ce Facilitate referencing UndoManager StackItem inside Type observers 2024-02-29 00:50:36 +00:00
Kevin Jahns
a9dc72fcc0 Merge pull request #612 from MentalGear/patch-1
docs: fix typo
2024-02-25 10:31:20 +01:00
Kevin Jahns
90a90ab010 add y-fire to provider list #189 2024-02-20 19:52:58 +01:00
MentalGear
009f6ab551 docs: fix typo 2024-02-17 20:38:53 +01:00
9 changed files with 104 additions and 55 deletions

View File

@@ -226,6 +226,10 @@ y-websocket provider.
<dd> <dd>
Like y-indexeddb, but with sub-documents support and fully TypeScript. Like y-indexeddb, but with sub-documents support and fully TypeScript.
</dd> </dd>
<dt><a href="https://github.com/podraven/y-fire">y-fire</a></dt>
<dd>
A database and connection provider for Yjs based on Firestore.
</dd>
</dl> </dl>
# Ports # Ports
@@ -878,7 +882,7 @@ ydoc2.getText().toString() // => "00000000000"
#### Using V2 update format #### Using V2 update format
Yjs implements two update formats. By default you are using the V1 update format. Yjs implements two update formats. By default you are using the V1 update format.
You can opt-in into the V2 update format wich provides much better compression. You can opt-in into the V2 update format which provides much better compression.
It is not yet used by all providers. However, you can already use it if It is not yet used by all providers. However, you can already use it if
you are building your own provider. All below functions are available with the you are building your own provider. All below functions are available with the
suffix "V2". E.g. `Y.applyUpdate``Y.applyUpdateV2`. Also when listening to updates suffix "V2". E.g. `Y.applyUpdate``Y.applyUpdateV2`. Also when listening to updates

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.6.12", "version": "13.6.13",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "yjs", "name": "yjs",
"version": "13.6.12", "version": "13.6.13",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lib0": "^0.2.86" "lib0": "^0.2.86"

View File

@@ -1,6 +1,6 @@
{ {
"name": "yjs", "name": "yjs",
"version": "13.6.12", "version": "13.6.13",
"description": "Shared Editing Library", "description": "Shared Editing Library",
"main": "./dist/yjs.cjs", "main": "./dist/yjs.cjs",
"module": "./dist/yjs.mjs", "module": "./dist/yjs.mjs",
@@ -12,9 +12,10 @@
"url": "https://github.com/sponsors/dmonad" "url": "https://github.com/sponsors/dmonad"
}, },
"scripts": { "scripts": {
"clean": "rm -rf dist docs",
"test": "npm run dist && node ./dist/tests.cjs --repetition-time 50", "test": "npm run dist && node ./dist/tests.cjs --repetition-time 50",
"test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000", "test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000",
"dist": "rm -rf dist && rollup -c && tsc", "dist": "npm run clean && rollup -c && tsc",
"watch": "rollup -wc", "watch": "rollup -wc",
"lint": "markdownlint README.md && standard && tsc", "lint": "markdownlint README.md && standard && tsc",
"docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true", "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",

View File

@@ -1,4 +1,4 @@
import { Observable } from 'lib0/observable' import { ObservableV2 } from 'lib0/observable'
import { import {
Doc // eslint-disable-line Doc // eslint-disable-line
@@ -10,9 +10,9 @@ import {
* @note This interface is experimental and it is not advised to actually inherit this class. * @note This interface is experimental and it is not advised to actually inherit this class.
* It just serves as typing information. * It just serves as typing information.
* *
* @extends {Observable<any>} * @extends {ObservableV2<any>}
*/ */
export class AbstractConnector extends Observable { export class AbstractConnector extends ObservableV2 {
/** /**
* @param {Doc} ydoc * @param {Doc} ydoc
* @param {any} awareness * @param {any} awareness

View File

@@ -14,7 +14,7 @@ import {
ContentDoc, Item, Transaction, YEvent // eslint-disable-line ContentDoc, Item, Transaction, YEvent // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import { Observable } from 'lib0/observable' import { ObservableV2 } from 'lib0/observable'
import * as random from 'lib0/random' import * as random from 'lib0/random'
import * as map from 'lib0/map' import * as map from 'lib0/map'
import * as array from 'lib0/array' import * as array from 'lib0/array'
@@ -34,10 +34,26 @@ export const generateNewClientId = random.uint32
*/ */
/** /**
* A Yjs instance handles the state of shared data. * @typedef {Object} DocEvents
* @extends Observable<string> * @property {function(Doc):void} DocEvents.destroy
* @property {function(Doc):void} DocEvents.load
* @property {function(boolean, Doc):void} DocEvents.sync
* @property {function(Uint8Array, any, Doc, Transaction):void} DocEvents.update
* @property {function(Uint8Array, any, Doc, Transaction):void} DocEvents.updateV2
* @property {function(Doc):void} DocEvents.beforeAllTransactions
* @property {function(Transaction, Doc):void} DocEvents.beforeTransaction
* @property {function(Transaction, Doc):void} DocEvents.beforeObserverCalls
* @property {function(Transaction, Doc):void} DocEvents.afterTransaction
* @property {function(Transaction, Doc):void} DocEvents.afterTransactionCleanup
* @property {function(Doc, Array<Transaction>):void} DocEvents.afterAllTransactions
* @property {function({ loaded: Set<Doc>, added: Set<Doc>, removed: Set<Doc> }, Doc, Transaction):void} DocEvents.subdocs
*/ */
export class Doc extends Observable {
/**
* A Yjs instance handles the state of shared data.
* @extends ObservableV2<DocEvents>
*/
export class Doc extends ObservableV2 {
/** /**
* @param {DocOpts} opts configuration * @param {DocOpts} opts configuration
*/ */
@@ -115,7 +131,7 @@ export class Doc extends Observable {
} }
this.isSynced = isSynced === undefined || isSynced === true this.isSynced = isSynced === undefined || isSynced === true
if (this.isSynced && !this.isLoaded) { if (this.isSynced && !this.isLoaded) {
this.emit('load', []) this.emit('load', [this])
} }
}) })
/** /**
@@ -321,24 +337,9 @@ export class Doc extends Observable {
transaction.subdocsRemoved.add(this) transaction.subdocsRemoved.add(this)
}, null, true) }, null, true)
} }
this.emit('destroyed', [true]) // @ts-ignore
this.emit('destroyed', [true]) // DEPRECATED!
this.emit('destroy', [this]) this.emit('destroy', [this])
super.destroy() super.destroy()
} }
/**
* @param {string} eventName
* @param {function(...any):any} f
*/
on (eventName, f) {
super.on(eventName, f)
}
/**
* @param {string} eventName
* @param {function} f
*/
off (eventName, f) {
super.off(eventName, f)
}
} }

View File

@@ -10,13 +10,13 @@ import {
getItemCleanStart, getItemCleanStart,
isDeleted, isDeleted,
addToDeleteSet, addToDeleteSet,
Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line YEvent, Transaction, Doc, Item, GC, DeleteSet, AbstractType // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as time from 'lib0/time' import * as time from 'lib0/time'
import * as array from 'lib0/array' import * as array from 'lib0/array'
import * as logging from 'lib0/logging' import * as logging from 'lib0/logging'
import { Observable } from 'lib0/observable' import { ObservableV2 } from 'lib0/observable'
export class StackItem { export class StackItem {
/** /**
@@ -48,15 +48,10 @@ const clearUndoManagerStackItem = (tr, um, stackItem) => {
/** /**
* @param {UndoManager} undoManager * @param {UndoManager} undoManager
* @param {Array<StackItem>} stack * @param {Array<StackItem>} stack
* @param {string} eventType * @param {'undo'|'redo'} eventType
* @return {StackItem?} * @return {StackItem?}
*/ */
const popStackItem = (undoManager, stack, eventType) => { const popStackItem = (undoManager, stack, eventType) => {
/**
* Whether a change happened
* @type {StackItem?}
*/
let result = null
/** /**
* Keep a reference to the transaction so we can fire the event with the changedParentTypes * Keep a reference to the transaction so we can fire the event with the changedParentTypes
* @type {any} * @type {any}
@@ -65,7 +60,7 @@ const popStackItem = (undoManager, stack, eventType) => {
const doc = undoManager.doc const doc = undoManager.doc
const scope = undoManager.scope const scope = undoManager.scope
transact(doc, transaction => { transact(doc, transaction => {
while (stack.length > 0 && result === null) { while (stack.length > 0 && undoManager.currStackItem === null) {
const store = doc.store const store = doc.store
const stackItem = /** @type {StackItem} */ (stack.pop()) const stackItem = /** @type {StackItem} */ (stack.pop())
/** /**
@@ -113,7 +108,7 @@ const popStackItem = (undoManager, stack, eventType) => {
performedChange = true performedChange = true
} }
} }
result = performedChange ? stackItem : null undoManager.currStackItem = performedChange ? stackItem : null
} }
transaction.changed.forEach((subProps, type) => { transaction.changed.forEach((subProps, type) => {
// destroy search marker if necessary // destroy search marker if necessary
@@ -123,11 +118,12 @@ const popStackItem = (undoManager, stack, eventType) => {
}) })
_tr = transaction _tr = transaction
}, undoManager) }, undoManager)
if (result != null) { if (undoManager.currStackItem != null) {
const changedParentTypes = _tr.changedParentTypes const changedParentTypes = _tr.changedParentTypes
undoManager.emit('stack-item-popped', [{ stackItem: result, type: eventType, changedParentTypes }, undoManager]) undoManager.emit('stack-item-popped', [{ stackItem: undoManager.currStackItem, type: eventType, changedParentTypes, origin: undoManager }, undoManager])
undoManager.currStackItem = null
} }
return result return undoManager.currStackItem
} }
/** /**
@@ -143,6 +139,14 @@ const popStackItem = (undoManager, stack, eventType) => {
* @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty. * @property {Doc} [doc] The document that this UndoManager operates on. Only needed if typeScope is empty.
*/ */
/**
* @typedef {Object} StackItemEvent
* @property {StackItem} StackItemEvent.stackItem
* @property {any} StackItemEvent.origin
* @property {'undo'|'redo'} StackItemEvent.type
* @property {Map<AbstractType<YEvent<any>>,Array<YEvent<any>>>} StackItemEvent.changedParentTypes
*/
/** /**
* Fires 'stack-item-added' event when a stack item was added to either the undo- or * Fires 'stack-item-added' event when a stack item was added to either the undo- or
* the redo-stack. You may store additional stack information via the * the redo-stack. You may store additional stack information via the
@@ -150,9 +154,9 @@ const popStackItem = (undoManager, stack, eventType) => {
* Fires 'stack-item-popped' event when a stack item was popped from either the * Fires 'stack-item-popped' event when a stack item was popped from either the
* undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`. * undo- or the redo-stack. You may restore the saved stack information from `event.stackItem.meta`.
* *
* @extends {Observable<'stack-item-added'|'stack-item-popped'|'stack-cleared'|'stack-item-updated'>} * @extends {ObservableV2<{'stack-item-added':function(StackItemEvent, UndoManager):void, 'stack-item-popped': function(StackItemEvent, UndoManager):void, 'stack-cleared': function({ undoStackCleared: boolean, redoStackCleared: boolean }):void, 'stack-item-updated': function(StackItemEvent, UndoManager):void }>}
*/ */
export class UndoManager extends Observable { export class UndoManager extends ObservableV2 {
/** /**
* @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types * @param {AbstractType<any>|Array<AbstractType<any>>} typeScope Accepts either a single type, or an array of types
* @param {UndoManagerOptions} options * @param {UndoManagerOptions} options
@@ -191,6 +195,12 @@ export class UndoManager extends Observable {
*/ */
this.undoing = false this.undoing = false
this.redoing = false this.redoing = false
/**
* The currently popped stack item if UndoManager.undoing or UndoManager.redoing
*
* @type {StackItem|null}
*/
this.currStackItem = null
this.lastChange = 0 this.lastChange = 0
this.ignoreRemoteMapChanges = ignoreRemoteMapChanges this.ignoreRemoteMapChanges = ignoreRemoteMapChanges
this.captureTimeout = captureTimeout this.captureTimeout = captureTimeout
@@ -244,6 +254,9 @@ export class UndoManager extends Observable {
keepItem(item, true) keepItem(item, true)
} }
}) })
/**
* @type {[StackItemEvent, UndoManager]}
*/
const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this] const changeEvent = [{ stackItem: stack[stack.length - 1], origin: transaction.origin, type: undoing ? 'redo' : 'undo', changedParentTypes: transaction.changedParentTypes }, this]
if (didAdd) { if (didAdd) {
this.emit('stack-item-added', changeEvent) this.emit('stack-item-added', changeEvent)

View File

@@ -34,7 +34,7 @@ export const encV1 = {
mergeUpdates: Y.mergeUpdates, mergeUpdates: Y.mergeUpdates,
applyUpdate: Y.applyUpdate, applyUpdate: Y.applyUpdate,
logUpdate: Y.logUpdate, logUpdate: Y.logUpdate,
updateEventName: 'update', updateEventName: /** @type {'update'} */ ('update'),
diffUpdate: Y.diffUpdate diffUpdate: Y.diffUpdate
} }
@@ -43,7 +43,7 @@ export const encV2 = {
mergeUpdates: Y.mergeUpdatesV2, mergeUpdates: Y.mergeUpdatesV2,
applyUpdate: Y.applyUpdateV2, applyUpdate: Y.applyUpdateV2,
logUpdate: Y.logUpdateV2, logUpdate: Y.logUpdateV2,
updateEventName: 'updateV2', updateEventName: /** @type {'updateV2'} */ ('updateV2'),
diffUpdate: Y.diffUpdateV2 diffUpdate: Y.diffUpdateV2
} }

View File

@@ -715,3 +715,33 @@ export const testUndoDeleteInMap = (tc) => {
undoManager.undo() undoManager.undo()
t.compare(map0.toJSON(), { a: 'a' }) t.compare(map0.toJSON(), { a: 'a' })
} }
/**
* It should expose the StackItem being processed if undoing
*
* @param {t.TestCase} _tc
*/
export const testUndoDoingStackItem = async (_tc) => {
const doc = new Y.Doc()
const text = doc.getText('text')
const undoManager = new Y.UndoManager([text])
undoManager.on('stack-item-added', /** @param {any} event */ event => {
event.stackItem.meta.set('str', '42')
})
let metaUndo = /** @type {any} */ (null)
let metaRedo = /** @type {any} */ (null)
text.observe((event) => {
const /** @type {Y.UndoManager} */ origin = event.transaction.origin
if (origin === undoManager && origin.undoing) {
metaUndo = origin.currStackItem?.meta.get('str')
} else if (origin === undoManager && origin.redoing) {
metaRedo = origin.currStackItem?.meta.get('str')
}
})
text.insert(0, 'abc')
undoManager.undo()
undoManager.redo()
t.compare(metaUndo, '42', 'currStackItem is accessible while undoing')
t.compare(metaRedo, '42', 'currStackItem is accessible while redoing')
t.compare(undoManager.currStackItem, null, 'currStackItem is null after observe/transaction')
}

View File

@@ -15,7 +15,7 @@ import * as object from 'lib0/object'
* @property {function(Uint8Array):{from:Map<number,number>,to:Map<number,number>}} Enc.parseUpdateMeta * @property {function(Uint8Array):{from:Map<number,number>,to:Map<number,number>}} Enc.parseUpdateMeta
* @property {function(Y.Doc):Uint8Array} Enc.encodeStateVector * @property {function(Y.Doc):Uint8Array} Enc.encodeStateVector
* @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate * @property {function(Uint8Array):Uint8Array} Enc.encodeStateVectorFromUpdate
* @property {string} Enc.updateEventName * @property {'update'|'updateV2'} Enc.updateEventName
* @property {string} Enc.description * @property {string} Enc.description
* @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate * @property {function(Uint8Array, Uint8Array):Uint8Array} Enc.diffUpdate
*/ */
@@ -169,7 +169,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
// t.info('Target State: ') // t.info('Target State: ')
// enc.logUpdate(targetState) // enc.logUpdate(targetState)
cases.forEach((mergedUpdates, i) => { cases.forEach((mergedUpdates) => {
// t.info('State Case $' + i + ':') // t.info('State Case $' + i + ':')
// enc.logUpdate(updates) // enc.logUpdate(updates)
const merged = new Y.Doc({ gc: false }) const merged = new Y.Doc({ gc: false })
@@ -218,10 +218,10 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
} }
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} _tc
*/ */
export const testMergeUpdates1 = tc => { export const testMergeUpdates1 = _tc => {
encoders.forEach((enc, i) => { encoders.forEach((enc) => {
t.info(`Using encoder: ${enc.description}`) t.info(`Using encoder: ${enc.description}`)
const ydoc = new Y.Doc({ gc: false }) const ydoc = new Y.Doc({ gc: false })
const updates = /** @type {Array<Uint8Array>} */ ([]) const updates = /** @type {Array<Uint8Array>} */ ([])
@@ -299,16 +299,16 @@ export const testMergePendingUpdates = tc => {
Y.applyUpdate(yDoc5, update4) Y.applyUpdate(yDoc5, update4)
Y.applyUpdate(yDoc5, serverUpdates[4]) Y.applyUpdate(yDoc5, serverUpdates[4])
// @ts-ignore // @ts-ignore
const update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line const _update5 = Y.encodeStateAsUpdate(yDoc5) // eslint-disable-line
const yText5 = yDoc5.getText('textBlock') const yText5 = yDoc5.getText('textBlock')
t.compareStrings(yText5.toString(), 'nenor') t.compareStrings(yText5.toString(), 'nenor')
} }
/** /**
* @param {t.TestCase} tc * @param {t.TestCase} _tc
*/ */
export const testObfuscateUpdates = tc => { export const testObfuscateUpdates = _tc => {
const ydoc = new Y.Doc() const ydoc = new Y.Doc()
const ytext = ydoc.getText('text') const ytext = ydoc.getText('text')
const ymap = ydoc.getMap('map') const ymap = ydoc.getMap('map')