From d823775e3ec02be48287d77fcb1cf7ce963e07bd Mon Sep 17 00:00:00 2001 From: fantasticit Date: Fri, 20 May 2022 17:57:05 +0800 Subject: [PATCH] client: use next-pwa, fix import different version yjs --- package.json | 1 + packages/client/next.config.js | 27 +-- packages/client/package.json | 9 +- .../cursor-plugin/index.ts | 9 +- .../extensions/collaboration/collaboration.ts | 2 +- .../collaboration/helpers/is-change-origin.ts | 2 +- .../client/src/tiptap/core/y-indexeddb.js | 189 ++++++++++++++++++ 7 files changed, 210 insertions(+), 29 deletions(-) create mode 100644 packages/client/src/tiptap/core/y-indexeddb.js diff --git a/package.json b/package.json index 9ec08443..c0beb6c9 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "node": ">=16.5.0" }, "devDependencies": { + "@types/node": "^17.0.35", "husky": "^7.0.4", "lint-staged": "^12.4.1", "prettier": "^2.3.2", diff --git a/packages/client/next.config.js b/packages/client/next.config.js index c2df3c36..446e4928 100644 --- a/packages/client/next.config.js +++ b/packages/client/next.config.js @@ -1,6 +1,6 @@ const semi = require('@douyinfe/semi-next').default({}); const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin'); -const withOffline = require('next-offline'); +const withPWA = require('next-pwa'); const { getConfig } = require('@think/config'); const config = getConfig(); @@ -27,26 +27,11 @@ const nextConfig = semi({ compiler: { removeConsole: true, }, - workboxOpts: { - runtimeCaching: [ - { - urlPattern: /.(png|jpg|jpeg|svg|webp)$/, - handler: 'CacheFirst', - }, - { - urlPattern: /api/, - handler: 'NetworkFirst', - options: { - cacheableResponse: { - statuses: [0, 200], - headers: { - 'x-sw': 'true', - }, - }, - }, - }, - ], + pwa: { + disable: process.env.NODE_ENV !== 'production', + dest: '.next', + sw: 'service-worker.js', }, }); -module.exports = withOffline(nextConfig); +module.exports = withPWA(nextConfig); diff --git a/packages/client/package.json b/packages/client/package.json index 630b1efd..7d7514ed 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -71,7 +71,7 @@ "markdown-it-sub": "^1.0.0", "markdown-it-sup": "^1.0.0", "next": "12.0.10", - "next-offline": "^5.0.5", + "next-pwa": "^5.5.2", "prosemirror-markdown": "^1.7.0", "prosemirror-model": "^1.16.1", "prosemirror-schema-list": "^1.1.6", @@ -93,8 +93,6 @@ "tippy.js": "^6.3.7", "toggle-selection": "^1.0.6", "viewerjs": "^1.10.4", - "y-indexeddb": "^9.0.7", - "y-prosemirror": "^1.0.14", "yjs": "^13.5.24" }, "devDependencies": { @@ -102,6 +100,7 @@ "@types/react": "17.0.38", "@typescript-eslint/eslint-plugin": "^5.21.0", "@typescript-eslint/parser": "^5.21.0", + "copy-webpack-plugin": "11.0.0", "eslint": "^8.14.0", "eslint-config-prettier": "^8.5.0", "eslint-plugin-import": "^2.26.0", @@ -109,7 +108,9 @@ "eslint-plugin-react": "^7.29.4", "eslint-plugin-react-hooks": "^4.5.0", "eslint-plugin-simple-import-sort": "^7.0.0", + "fs-extra": "^10.0.0", "tsconfig-paths-webpack-plugin": "^3.5.2", - "typescript": "4.5.5" + "typescript": "4.5.5", + "workbox-webpack-plugin": "^6.5.3" } } \ No newline at end of file diff --git a/packages/client/src/tiptap/core/extensions/collaboration-cursor/cursor-plugin/index.ts b/packages/client/src/tiptap/core/extensions/collaboration-cursor/cursor-plugin/index.ts index cf7ec2e4..cb12f1c5 100644 --- a/packages/client/src/tiptap/core/extensions/collaboration-cursor/cursor-plugin/index.ts +++ b/packages/client/src/tiptap/core/extensions/collaboration-cursor/cursor-plugin/index.ts @@ -1,8 +1,13 @@ import * as math from 'lib0/math'; import { Plugin } from 'prosemirror-state'; // eslint-disable-line import { Decoration, DecorationSet } from 'prosemirror-view'; // eslint-disable-line -import { absolutePositionToRelativePosition, relativePositionToAbsolutePosition, setMeta } from 'y-prosemirror'; -import { yCursorPluginKey, ySyncPluginKey } from 'y-prosemirror'; +import { + absolutePositionToRelativePosition, + relativePositionToAbsolutePosition, + setMeta, + yCursorPluginKey, + ySyncPluginKey, +} from 'tiptap/core/y-prosemirror/y-prosemirror'; import * as Y from 'yjs'; /** diff --git a/packages/client/src/tiptap/core/extensions/collaboration/collaboration.ts b/packages/client/src/tiptap/core/extensions/collaboration/collaboration.ts index a9e7b76d..4687a3d2 100644 --- a/packages/client/src/tiptap/core/extensions/collaboration/collaboration.ts +++ b/packages/client/src/tiptap/core/extensions/collaboration/collaboration.ts @@ -1,5 +1,5 @@ import { Extension } from '@tiptap/core'; -import { redo, undo, ySyncPlugin, yUndoPlugin, yUndoPluginKey } from 'y-prosemirror'; +import { redo, undo, ySyncPlugin, yUndoPlugin, yUndoPluginKey } from 'tiptap/core/y-prosemirror/y-prosemirror'; import { UndoManager } from 'yjs'; declare module '@tiptap/core' { diff --git a/packages/client/src/tiptap/core/extensions/collaboration/helpers/is-change-origin.ts b/packages/client/src/tiptap/core/extensions/collaboration/helpers/is-change-origin.ts index 0325d666..42d90445 100644 --- a/packages/client/src/tiptap/core/extensions/collaboration/helpers/is-change-origin.ts +++ b/packages/client/src/tiptap/core/extensions/collaboration/helpers/is-change-origin.ts @@ -1,5 +1,5 @@ import { Transaction } from 'prosemirror-state'; -import { ySyncPluginKey } from 'y-prosemirror'; +import { ySyncPluginKey } from 'tiptap/core/y-prosemirror/y-prosemirror'; export function isChangeOrigin(transaction: Transaction): boolean { return !!transaction.getMeta(ySyncPluginKey); diff --git a/packages/client/src/tiptap/core/y-indexeddb.js b/packages/client/src/tiptap/core/y-indexeddb.js new file mode 100644 index 00000000..0af203de --- /dev/null +++ b/packages/client/src/tiptap/core/y-indexeddb.js @@ -0,0 +1,189 @@ +import * as idb from 'lib0/indexeddb.js'; +import * as mutex from 'lib0/mutex.js'; +import { Observable } from 'lib0/observable.js'; +import * as Y from 'yjs'; + +const customStoreName = 'custom'; +const updatesStoreName = 'updates'; + +export const PREFERRED_TRIM_SIZE = 500; + +/** + * @param {IndexeddbPersistence} idbPersistence + */ +export const fetchUpdates = (idbPersistence) => { + const [updatesStore] = idb.transact(/** @type {IDBDatabase} */ (idbPersistence.db), [updatesStoreName]); // , 'readonly') + return idb + .getAll(updatesStore, idb.createIDBKeyRangeLowerBound(idbPersistence._dbref, false)) + .then((updates) => + idbPersistence._mux(() => + Y.transact( + idbPersistence.doc, + () => { + updates.forEach((val) => Y.applyUpdate(idbPersistence.doc, val)); + }, + idbPersistence, + false + ) + ) + ) + .then(() => + idb.getLastKey(updatesStore).then((lastKey) => { + idbPersistence._dbref = lastKey + 1; + }) + ) + .then(() => + idb.count(updatesStore).then((cnt) => { + idbPersistence._dbsize = cnt; + }) + ) + .then(() => updatesStore); +}; + +/** + * @param {IndexeddbPersistence} idbPersistence + * @param {boolean} forceStore + */ +export const storeState = (idbPersistence, forceStore = true) => + fetchUpdates(idbPersistence).then((updatesStore) => { + if (forceStore || idbPersistence._dbsize >= PREFERRED_TRIM_SIZE) { + idb + .addAutoKey(updatesStore, Y.encodeStateAsUpdate(idbPersistence.doc)) + .then(() => idb.del(updatesStore, idb.createIDBKeyRangeUpperBound(idbPersistence._dbref, true))) + .then(() => + idb.count(updatesStore).then((cnt) => { + idbPersistence._dbsize = cnt; + }) + ); + } + }); + +/** + * @param {string} name + */ +export const clearDocument = (name) => idb.deleteDB(name); + +/** + * @extends Observable + */ +export class IndexeddbPersistence extends Observable { + /** + * @param {string} name + * @param {Y.Doc} doc + */ + constructor(name, doc) { + super(); + this.doc = doc; + this.name = name; + this._mux = mutex.createMutex(); + this._dbref = 0; + this._dbsize = 0; + /** + * @type {IDBDatabase|null} + */ + this.db = null; + this.synced = false; + this._db = idb.openDB(name, (db) => idb.createStores(db, [['updates', { autoIncrement: true }], ['custom']])); + /** + * @type {Promise} + */ + this.whenSynced = this._db.then((db) => { + this.db = db; + const currState = Y.encodeStateAsUpdate(doc); + return fetchUpdates(this) + .then((updatesStore) => idb.addAutoKey(updatesStore, currState)) + .then(() => { + this.emit('synced', [this]); + this.synced = true; + return this; + }); + }); + /** + * Timeout in ms untill data is merged and persisted in idb. + */ + this._storeTimeout = 1000; + /** + * @type {any} + */ + this._storeTimeoutId = null; + /** + * @param {Uint8Array} update + */ + this._storeUpdate = (update) => + this._mux(() => { + if (this.db) { + const [updatesStore] = idb.transact(/** @type {IDBDatabase} */ (this.db), [updatesStoreName]); + idb.addAutoKey(updatesStore, update); + if (++this._dbsize >= PREFERRED_TRIM_SIZE) { + // debounce store call + if (this._storeTimeoutId !== null) { + clearTimeout(this._storeTimeoutId); + } + this._storeTimeoutId = setTimeout(() => { + storeState(this, false); + this._storeTimeoutId = null; + }, this._storeTimeout); + } + } + }); + doc.on('update', this._storeUpdate); + this.destroy = this.destroy.bind(this); + doc.on('destroy', this.destroy); + } + + destroy() { + if (this._storeTimeoutId) { + clearTimeout(this._storeTimeoutId); + } + this.doc.off('update', this._storeUpdate); + this.doc.off('destroy', this.destroy); + return this._db.then((db) => { + db.close(); + }); + } + + /** + * Destroys this instance and removes all data from indexeddb. + * + * @return {Promise} + */ + clearData() { + return this.destroy().then(() => { + idb.deleteDB(this.name); + }); + } + + /** + * @param {String | number | ArrayBuffer | Date} key + * @return {Promise} + */ + get(key) { + return this._db.then((db) => { + const [custom] = idb.transact(db, [customStoreName], 'readonly'); + return idb.get(custom, key); + }); + } + + /** + * @param {String | number | ArrayBuffer | Date} key + * @param {String | number | ArrayBuffer | Date} value + * @return {Promise} + */ + set(key, value) { + return this._db.then((db) => { + const [custom] = idb.transact(db, [customStoreName]); + return idb.put(custom, value, key); + }); + } + + /** + * @param {String | number | ArrayBuffer | Date} key + * @return {Promise} + */ + del(key) { + return this._db.then((db) => { + const [custom] = idb.transact(db, [customStoreName]); + return idb.del(custom, key); + }); + } +}