Merge remote-tracking branch 'origin/develop' into feature/new-grid-drop

This commit is contained in:
SaikaSakura
2022-08-10 15:16:44 +08:00
16 changed files with 643 additions and 263 deletions

View File

@@ -44,10 +44,37 @@ export const LayoutHeader = () => {
<EditorBoardSwitcher />
</StyledContainerForEditorBoardSwitcher>
</StyledHeaderRoot>
<StyledUnstableTips>
<StyledUnstableTipsText>
AFFiNE now under active development, the version is
UNSTABLE, please DO NOT store important data in this version
</StyledUnstableTipsText>
</StyledUnstableTips>
</StyledContainerForHeaderRoot>
);
};
const StyledUnstableTips = styled('div')(({ theme }) => {
return {
width: '100%',
height: '2em',
display: 'flex',
zIndex: theme.affine.zIndex.header,
backgroundColor: '#fff8c5',
borderWidth: '1px 0',
borderColor: '#e4e49588',
borderStyle: 'solid',
};
});
const StyledUnstableTipsText = styled('span')(({ theme }) => {
return {
margin: 'auto 36px',
width: '100%',
textAlign: 'center',
};
});
const StyledContainerForHeaderRoot = styled('div')(({ theme }) => {
return {
width: '100%',
@@ -114,7 +141,9 @@ const StyledLogoIcon = styled(LogoIcon)(({ theme }) => {
const StyledContainerForEditorBoardSwitcher = styled('div')(({ theme }) => {
return {
width: '100%',
position: 'absolute',
left: '50%',
display: 'flex',
justifyContent: 'center',
};
});

View File

@@ -1,20 +1,18 @@
import styles from './tree-item.module.scss';
import { AddIcon, MoreIcon } from '@toeverything/components/icons';
import {
MuiSnackbar as Snackbar,
Cascader,
CascaderItemProps,
MuiDivider as Divider,
MuiClickAwayListener as ClickAwayListener,
IconButton,
MuiClickAwayListener as ClickAwayListener,
MuiSnackbar as Snackbar,
styled,
} from '@toeverything/components/ui';
import React from 'react';
import { NavLink, useNavigate } from 'react-router-dom';
import { copyToClipboard } from '@toeverything/utils';
import { services, TemplateFactory } from '@toeverything/datasource/db-service';
import { NewFromTemplatePortal } from './NewFromTemplatePortal';
import { useFlag } from '@toeverything/datasource/feature-flags';
import { MoreIcon, AddIcon } from '@toeverything/components/icons';
import { copyToClipboard } from '@toeverything/utils';
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { TreeItemMoreActions } from './styles';
const MESSAGES = {
COPY_LINK_SUCCESS: 'Copyed link to clipboard',
@@ -47,6 +45,10 @@ function DndTreeItemMoreActions(props: ActionsProps) {
set_alert_open(false);
};
const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {
if (anchorEl) {
setAnchorEl(null);
return;
}
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
@@ -246,10 +248,11 @@ function DndTreeItemMoreActions(props: ActionsProps) {
return (
<ClickAwayListener onClickAway={() => handleClose()}>
<div>
<div className={styles['TreeItemMoreActions']}>
<TreeItemMoreActions>
<StyledAction>
<IconButton
size="small"
hoverColor="#E0E6EB"
onClick={handle_new_child_page}
>
<AddIcon />
@@ -262,14 +265,15 @@ function DndTreeItemMoreActions(props: ActionsProps) {
<MoreIcon />
</IconButton>
</StyledAction>
</div>
</TreeItemMoreActions>
<Cascader
items={menuList}
anchorEl={anchorEl}
placement="right-start"
open={open}
onClose={handleClose}
></Cascader>
/>
<Snackbar
anchorOrigin={{ vertical: 'bottom', horizontal: 'center' }}
open={alert_open}

View File

@@ -1,21 +1,22 @@
import React, {
forwardRef,
type CSSProperties,
type HTMLAttributes,
} from 'react';
import { useParams, Link } from 'react-router-dom';
import cx from 'clsx';
import { CloseIcon } from '@toeverything/components/common';
import {
ArrowDropDownIcon,
ArrowRightIcon,
} from '@toeverything/components/icons';
import { forwardRef, type HTMLAttributes } from 'react';
import { useParams } from 'react-router-dom';
import styles from './tree-item.module.scss';
import { useFlag } from '@toeverything/datasource/feature-flags';
import MoreActions from './MoreActions';
import { useTheme } from '@toeverything/components/ui';
import {
ActionButton,
Counter,
TextLink,
TreeItemContainer,
TreeItemContent,
Wrapper,
} from './styles';
export type TreeItemProps = {
/** The main text to display on this line */
value: string;
@@ -67,56 +68,34 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
'BooleanPageTreeItemMoreActions',
true
);
const theme = useTheme();
return (
<li
<Wrapper
ref={wrapperRef}
className={cx(
styles['Wrapper'],
clone && styles['clone'],
ghost && styles['ghost'],
indicator && styles['indicator'],
disableSelection && styles['disableSelection'],
disableInteraction && styles['disableInteraction']
)}
style={
{
'--spacing': `${indentationWidth * depth}px`,
paddingTop: 0,
paddingBottom: 0,
} as CSSProperties
}
clone={clone}
ghost={ghost}
disableSelection={disableSelection}
disableInteraction={disableInteraction}
spacing={`${indentationWidth * depth}px`}
{...props}
>
<div
ref={ref}
className={styles['TreeItem']}
style={style}
title={value}
>
<Action onClick={onCollapse}>
<TreeItemContainer ref={ref} style={style} title={value}>
<ActionButton tabIndex={0} onClick={onCollapse}>
{childCount !== 0 &&
(collapsed ? (
<ArrowRightIcon />
) : (
<ArrowDropDownIcon />
))}
</Action>
</ActionButton>
<div className={styles['ItemContent']}>
<Link
className={styles['Text']}
{...handleProps}
<TreeItemContent {...handleProps}>
<TextLink
to={`/${workspace_id}/${pageId}`}
style={{
...(pageId === page_id && {
color: theme.affine.palette.primary,
}),
}}
active={pageId === page_id}
>
{value}
</Link>
</TextLink>
{BooleanPageTreeItemMoreActions && (
<MoreActions
workspaceId={workspace_id}
@@ -127,71 +106,11 @@ export const TreeItem = forwardRef<HTMLDivElement, TreeItemProps>(
{/*{!clone && onRemove && <Remove onClick={onRemove} />}*/}
{clone && childCount && childCount > 1 ? (
<span className={styles['Count']}>
{childCount}
</span>
<Counter>{childCount}</Counter>
) : null}
</div>
</div>
</li>
</TreeItemContent>
</TreeItemContainer>
</Wrapper>
);
}
);
export interface ActionProps extends React.HTMLAttributes<HTMLButtonElement> {
active?: {
fill: string;
background: string;
};
// cursor?: CSSProperties['cursor'];
cursor?: 'pointer' | 'grab';
}
/** Customizable buttons */
export function Action({
active,
className,
cursor,
style,
...props
}: ActionProps) {
return (
<button
{...props}
className={cx(styles['Action'], className)}
tabIndex={0}
style={
{
...style,
'--fill': active?.fill,
'--background': active?.background,
} as CSSProperties
}
/>
);
}
export function Handle(props: ActionProps) {
return (
<Action cursor="grab" data-cypress="draggable-handle" {...props}>
<ArrowDropDownIcon />
</Action>
);
}
export function Remove(props: ActionProps) {
return (
<Action
{...props}
active={{
fill: 'rgba(255, 70, 70, 0.95)',
background: 'rgba(255, 70, 70, 0.1)',
}}
>
<CloseIcon style={{ fontSize: 12 }} />
{/* <svg width="8" viewBox="0 0 22 22" xmlns="http://www.w3.org/2000/svg">
<path d="M2.99998 -0.000206962C2.7441 -0.000206962 2.48794 0.0972617 2.29294 0.292762L0.292945 2.29276C-0.0980552 2.68376 -0.0980552 3.31682 0.292945 3.70682L7.58591 10.9998L0.292945 18.2928C-0.0980552 18.6838 -0.0980552 19.3168 0.292945 19.7068L2.29294 21.7068C2.68394 22.0978 3.31701 22.0978 3.70701 21.7068L11 14.4139L18.2929 21.7068C18.6829 22.0978 19.317 22.0978 19.707 21.7068L21.707 19.7068C22.098 19.3158 22.098 18.6828 21.707 18.2928L14.414 10.9998L21.707 3.70682C22.098 3.31682 22.098 2.68276 21.707 2.29276L19.707 0.292762C19.316 -0.0982383 18.6829 -0.0982383 18.2929 0.292762L11 7.58573L3.70701 0.292762C3.51151 0.0972617 3.25585 -0.000206962 2.99998 -0.000206962Z" />
</svg> */}
</Action>
);
}

View File

@@ -1,10 +1,46 @@
.Wrapper {
import { styled } from '@toeverything/components/ui';
import { Link } from 'react-router-dom';
export const TreeItemContainer = styled('div')`
box-sizing: border-box;
padding-left: var(--spacing);
display: flex;
align-items: center;
color: #4c6275;
`;
export const Wrapper = styled('li')<{
spacing: string;
clone?: boolean;
ghost?: boolean;
indicator?: boolean;
disableSelection?: boolean;
disableInteraction?: boolean;
}>`
box-sizing: border-box;
padding-left: ${({ spacing }) => spacing};
list-style: none;
padding-top: 6px;
padding-bottom: 6px;
font-size: 14px;
${({ clone, disableSelection }) =>
(clone || disableSelection) &&
`width: 100%;
.Text,
.Count {
user-select: none;
-webkit-user-select: none;
}`}
${({ indicator }) =>
indicator &&
`width: 100%;
.Text,
.Count {
user-select: none;
-webkit-user-select: none;
}`}
${({ disableInteraction }) => disableInteraction && `pointer-events: none;`}
&:hover {
background: #f5f7f8;
border-radius: 5px;
@@ -17,7 +53,7 @@
margin-top: 5px;
pointer-events: none;
.TreeItem {
${TreeItemContainer} {
padding-right: 20px;
border-radius: 4px;
box-shadow: 0px 15px 15px 0 rgba(34, 33, 81, 0.1);
@@ -31,7 +67,7 @@
z-index: 1;
margin-bottom: -1px;
.TreeItem {
${TreeItemContainer} {
position: relative;
padding: 0;
height: 8px;
@@ -62,56 +98,14 @@
opacity: 0.5;
}
.TreeItem > * {
${TreeItemContainer} > * {
box-shadow: none;
background-color: transparent;
}
}
}
`;
.TreeItem {
box-sizing: border-box;
display: flex;
align-items: center;
color: #4c6275;
}
.ItemContent {
box-sizing: border-box;
width: 100%;
height: 32px;
position: relative;
display: flex;
align-items: center;
justify-content: space-around;
color: #4c6275;
padding-right: 0.5rem;
overflow: hidden;
.TreeItemMoreActions {
visibility: hidden;
cursor: pointer;
}
&:hover {
.TreeItemMoreActions {
visibility: visible;
display: block;
}
}
}
.Text {
flex-grow: 1;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
cursor: pointer;
appearance: none;
color: unset;
text-decoration: none;
}
.Count {
export const Counter = styled('span')`
position: absolute;
top: 8px;
right: 0;
@@ -125,33 +119,12 @@
font-size: 0.9rem;
font-weight: 500;
color: #fff;
}
`;
.disableInteraction {
pointer-events: none;
}
.disableSelection,
.clone {
width: 100%;
.Text,
.Count {
user-select: none;
-webkit-user-select: none;
}
}
.Collapse {
svg {
transition: transform 250ms ease;
}
&.collapsed svg {
transform: rotate(-90deg);
}
}
.Action {
export const ActionButton = styled('button')<{
background?: string;
fill?: string;
}>`
display: flex;
width: 12px;
padding: 0 15px;
@@ -176,10 +149,11 @@
}
&:active {
background-color: var(--background, rgba(0, 0, 0, 0.05));
background-color: ${({ background }) =>
background ?? 'rgba(0, 0, 0, 0.05)'};
svg {
fill: var(--fill, #788491);
fill: ${({ fill }) => fill ?? '#788491'};
}
}
@@ -187,4 +161,45 @@
outline: none;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0), 0 0px 0px 2px #4c9ffe;
}
}
`;
export const TreeItemMoreActions = styled('div')`
display: block;
visibility: hidden;
`;
export const TextLink = styled(Link)<{ active?: boolean }>`
display: flex;
align-items: center;
flex-grow: 1;
height: 100%;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
cursor: pointer;
appearance: none;
text-decoration: none;
user-select: none;
color: ${({ theme, active }) =>
active ? theme.affine.palette.primary : 'unset'};
`;
export const TreeItemContent = styled('div')`
box-sizing: border-box;
width: 100%;
height: 32px;
position: relative;
display: flex;
align-items: center;
justify-content: space-around;
color: #4c6275;
padding-right: 0.5rem;
overflow: hidden;
&:hover {
${TreeItemMoreActions} {
visibility: visible;
cursor: pointer;
}
}
`;

View File

@@ -78,7 +78,10 @@ export class Database {
}
async getDatabase(workspace: string, options?: BlockInitOptions) {
const db = await _getBlockDatabase(workspace, options);
const db = await _getBlockDatabase(workspace, {
...this.#options,
...options,
});
return db;
}
@@ -87,7 +90,7 @@ export class Database {
name: string,
listener: (connectivity: Connectivity) => void
) {
const db = await _getBlockDatabase(workspace);
const db = await _getBlockDatabase(workspace, this.#options);
return db.addConnectivityListener(name, state => {
const connectivity = state.get(name);
if (connectivity) listener(connectivity);

View File

@@ -5,7 +5,11 @@
"author": "DarkSky <darksky2048@gmail.com>",
"dependencies": {
"lib0": "^0.2.52",
"sql.js": "^1.7.0",
"yjs": "^13.5.41",
"y-protocols": "^1.0.5"
},
"devDependencies": {
"@types/sql.js": "^1.4.3"
}
}

View File

@@ -1 +1,3 @@
export { IndexedDBProvider } from './indexeddb';
export { WebsocketProvider } from './provider';
export { SQLiteProvider } from './sqlite';

View File

@@ -0,0 +1,185 @@
import * as Y from 'yjs';
import * as idb from 'lib0/indexeddb.js';
import * as mutex from 'lib0/mutex.js';
import { Observable } from 'lib0/observable.js';
const customStoreName = 'custom';
const updatesStoreName = 'updates';
const PREFERRED_TRIM_SIZE = 500;
const fetchUpdates = async (provider: IndexedDBProvider) => {
const [updatesStore] = idb.transact(provider.db as IDBDatabase, [
updatesStoreName,
]); // , 'readonly')
const updates = await idb.getAll(
updatesStore,
idb.createIDBKeyRangeLowerBound(provider._dbref, false)
);
Y.transact(
provider.doc,
() => {
updates.forEach(val => Y.applyUpdate(provider.doc, val));
},
provider,
false
);
const lastKey = await idb.getLastKey(updatesStore);
provider._dbref = lastKey + 1;
const cnt = await idb.count(updatesStore);
provider._dbsize = cnt;
return updatesStore;
};
const storeState = (provider: IndexedDBProvider, forceStore = true) =>
fetchUpdates(provider).then(updatesStore => {
if (forceStore || provider._dbsize >= PREFERRED_TRIM_SIZE) {
idb.addAutoKey(updatesStore, Y.encodeStateAsUpdate(provider.doc))
.then(() =>
idb.del(
updatesStore,
idb.createIDBKeyRangeUpperBound(provider._dbref, true)
)
)
.then(() =>
idb.count(updatesStore).then(cnt => {
provider._dbsize = cnt;
})
);
}
});
export class IndexedDBProvider extends Observable<string> {
doc: Y.Doc;
name: string;
private _mux: mutex.mutex;
_dbref: number;
_dbsize: number;
private _destroyed: boolean;
whenSynced: Promise<IndexedDBProvider>;
db: IDBDatabase | null;
private _db: Promise<IDBDatabase>;
private synced: boolean;
private _storeTimeout: number;
private _storeTimeoutId: NodeJS.Timeout | null;
private _storeUpdate: (update: Uint8Array, origin: any) => void;
constructor(name: string, doc: Y.Doc) {
super();
this.doc = doc;
this.name = name;
this._mux = mutex.createMutex();
this._dbref = 0;
this._dbsize = 0;
this._destroyed = false;
this.db = null;
this.synced = false;
this._db = idb.openDB(name, db =>
idb.createStores(db, [
['updates', { autoIncrement: true }],
['custom'],
])
);
this.whenSynced = this._db.then(async db => {
this.db = db;
const currState = Y.encodeStateAsUpdate(doc);
const updatesStore = await fetchUpdates(this);
await idb.addAutoKey(updatesStore, currState);
if (this._destroyed) return this;
this.emit('synced', [this]);
this.synced = true;
return this;
});
// Timeout in ms untill data is merged and persisted in idb.
this._storeTimeout = 1000;
this._storeTimeoutId = null;
this._storeUpdate = (update: Uint8Array, origin: any) => {
if (this.db && origin !== this) {
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);
}
override destroy() {
if (this._storeTimeoutId) {
clearTimeout(this._storeTimeoutId);
}
this.doc.off('update', this._storeUpdate);
this.doc.off('destroy', this.destroy);
this._destroyed = true;
return this._db.then(db => {
db.close();
});
}
/**
* Destroys this instance and removes all data from SQLite.
*
* @return {Promise<void>}
*/
async clearData(): Promise<void> {
return this.destroy().then(() => {
idb.deleteDB(this.name);
});
}
/**
* @param {String | number | ArrayBuffer | Date} key
* @return {Promise<String | number | ArrayBuffer | Date | any>}
*/
async get(
key: string | number | ArrayBuffer | Date
): Promise<string | number | ArrayBuffer | Date | any> {
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<String | number | ArrayBuffer | Date>}
*/
async set(
key: string | number | ArrayBuffer | Date,
value: string | number | ArrayBuffer | Date
): Promise<string | number | ArrayBuffer | Date> {
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<undefined>}
*/
async del(key: string | number | ArrayBuffer | Date): Promise<undefined> {
return this._db.then(db => {
const [custom] = idb.transact(db, [customStoreName]);
return idb.del(custom, key);
});
}
}

View File

@@ -0,0 +1,180 @@
import * as Y from 'yjs';
import sqlite, { Database, SqlJsStatic } from 'sql.js';
import { Observable } from 'lib0/observable.js';
const PREFERRED_TRIM_SIZE = 500;
const _stmts = {
create: 'CREATE TABLE updates (key INTEGER PRIMARY KEY AUTOINCREMENT, value BLOB);',
selectAll: 'SELECT * FROM updates where key >= $idx',
selectCount: 'SELECT count(*) FROM updates',
insert: 'INSERT INTO updates VALUES (null, $data);',
delete: 'DELETE FROM updates WHERE key < $idx',
drop: 'DROP TABLE updates;',
};
const countUpdates = (db: Database) => {
const [cnt] = db.exec(_stmts.selectCount);
return cnt.values[0]?.[0] as number;
};
const clearUpdates = (db: Database, idx: number) => {
db.exec(_stmts.delete, { $idx: idx });
};
const getAllUpdates = (db: Database, idx: number) => {
return db
.exec(_stmts.selectAll, { $idx: idx })
.flatMap(val => val.values as [number, Uint8Array][])
.sort(([a], [b]) => a - b);
};
let _sqliteInstance: SqlJsStatic | undefined;
let _sqliteProcessing = false;
const sleep = () => new Promise(resolve => setTimeout(resolve, 500));
const initSQLiteInstance = async () => {
while (_sqliteProcessing) {
await sleep();
}
if (_sqliteInstance) return _sqliteInstance;
_sqliteProcessing = true;
_sqliteInstance = await sqlite({
locateFile: () =>
new URL('sql.js/dist/sql-wasm.wasm', import.meta.url).href,
});
_sqliteProcessing = false;
return _sqliteInstance;
};
export class SQLiteProvider extends Observable<string> {
doc: Y.Doc;
name: string;
db: Database | null;
whenSynced: Promise<SQLiteProvider>;
synced: boolean;
private _ref: number;
private _size: number;
private _destroyed: boolean;
private _db: Promise<Database>;
private _saver?: (binary: Uint8Array) => void;
private _destroy: () => void;
constructor(name: string, doc: Y.Doc, origin?: Uint8Array) {
super();
this.doc = doc;
this.name = name;
this._ref = 0;
this._size = 0;
this._destroyed = false;
this.db = null;
this.synced = false;
this._db = initSQLiteInstance().then(db => {
const sqlite = new db.Database(origin);
return sqlite.run(_stmts.create);
});
this.whenSynced = this._db.then(async db => {
this.db = db;
const currState = Y.encodeStateAsUpdate(doc);
await this._fetchUpdates();
db.exec(_stmts.insert, { $data: currState });
if (this._destroyed) return this;
this.emit('synced', [this]);
this.synced = true;
return this;
});
// Timeout in ms until data is merged and persisted in sqlite.
const storeTimeout = 1000;
let storeTimeoutId: NodeJS.Timer | undefined = undefined;
const storeUpdate = (update: Uint8Array, origin: any) => {
if (this._saver && this.db && origin !== this) {
this.db.exec(_stmts.insert, { $data: update });
if (++this._size >= PREFERRED_TRIM_SIZE) {
// debounce store call
if (storeTimeoutId) clearTimeout(storeTimeoutId);
storeTimeoutId = setTimeout(() => {
this._storeState();
storeTimeoutId = undefined;
}, storeTimeout);
}
}
};
doc.on('update', storeUpdate);
this.destroy = this.destroy.bind(this);
doc.on('destroy', this.destroy);
this._destroy = () => {
if (storeTimeoutId) clearTimeout(storeTimeoutId);
this.doc.off('update', storeUpdate);
this.doc.off('destroy', this.destroy);
};
}
registerExporter(saver: (binary: Uint8Array) => void) {
this._saver = saver;
}
private async _storeState() {
await this._fetchUpdates();
if (this.db && this._size >= PREFERRED_TRIM_SIZE) {
this.db.exec(_stmts.insert, {
$data: Y.encodeStateAsUpdate(this.doc),
});
clearUpdates(this.db, this._ref);
this._size = countUpdates(this.db);
this._saver?.(this.db?.export());
}
}
private async _fetchUpdates() {
if (this.db) {
const updates = getAllUpdates(this.db, this._ref);
Y.transact(
this.doc,
() => {
updates.forEach(([, update]) =>
Y.applyUpdate(this.doc, update)
);
},
this,
false
);
const lastKey = Math.max(...updates.map(([idx]) => idx));
this._ref = lastKey + 1;
this._size = countUpdates(this.db);
}
}
override destroy(): Promise<void> {
this._destroy();
this._destroyed = true;
return this._db.then(db => {
db.close();
});
}
// Destroys this instance and removes all data from SQLite.
async clearData(): Promise<void> {
return this._db.then(db => {
db.exec(_stmts.drop);
return this.destroy();
});
}
}

View File

@@ -13,8 +13,7 @@
"flexsearch": "^0.7.21",
"lib0": "^0.2.52",
"lru-cache": "^7.13.2",
"ts-debounce": "^4.0.0",
"y-indexeddb": "^9.0.9"
"ts-debounce": "^4.0.0"
},
"dependencies": {
"@types/flexsearch": "^0.7.3",

View File

@@ -7,7 +7,6 @@ import { fromEvent } from 'file-selector';
import LRUCache from 'lru-cache';
import { debounce } from 'ts-debounce';
import { nanoid } from 'nanoid';
import { IndexeddbPersistence } from 'y-indexeddb';
import { Awareness } from 'y-protocols/awareness.js';
import {
Doc,
@@ -19,7 +18,11 @@ import {
snapshot,
} from 'yjs';
import { WebsocketProvider } from '@toeverything/datasource/jwt-rpc';
import {
IndexedDBProvider,
SQLiteProvider,
WebsocketProvider,
} from '@toeverything/datasource/jwt-rpc';
import {
AsyncDatabaseAdapter,
@@ -46,8 +49,9 @@ const logger = getLogger('BlockDB:yjs');
type YjsProviders = {
awareness: Awareness;
idb: IndexeddbPersistence;
binariesIdb: IndexeddbPersistence;
idb: IndexedDBProvider;
binariesIdb: IndexedDBProvider;
fstore?: SQLiteProvider;
ws?: WebsocketProvider;
backend: string;
gatekeeper: GateKeeper;
@@ -98,6 +102,8 @@ async function _initYjsDatabase(
params: YjsInitOptions['params'];
userId: string;
token?: string;
importData?: Uint8Array;
exportData?: (binary: Uint8Array) => void;
}
): Promise<YjsProviders> {
if (_asyncInitLoading.has(workspace)) {
@@ -117,7 +123,11 @@ async function _initYjsDatabase(
const doc = new Doc({ autoLoad: true, shouldLoad: true });
const idbp = new IndexeddbPersistence(workspace, doc).whenSynced;
const idbp = new IndexedDBProvider(workspace, doc).whenSynced;
const fs = new SQLiteProvider(workspace, doc, options.importData);
if (options.exportData) fs.registerExporter(options.exportData);
const wsp = _initWebsocketProvider(
backend,
workspace,
@@ -126,10 +136,14 @@ async function _initYjsDatabase(
params
);
const [idb, [awareness, ws]] = await Promise.all([idbp, wsp]);
const [idb, [awareness, ws], fstore] = await Promise.all([
idbp,
wsp,
fs.whenSynced,
]);
const binaries = new Doc({ autoLoad: true, shouldLoad: true });
const binariesIdb = await new IndexeddbPersistence(
const binariesIdb = await new IndexedDBProvider(
`${workspace}_binaries`,
binaries
).whenSynced;
@@ -147,6 +161,7 @@ async function _initYjsDatabase(
awareness,
idb,
binariesIdb,
fstore,
ws,
backend,
gatekeeper,
@@ -159,6 +174,7 @@ async function _initYjsDatabase(
awareness,
idb,
binariesIdb,
fstore,
ws,
backend,
gatekeeper,
@@ -175,6 +191,8 @@ export type YjsInitOptions = {
params?: Record<string, string>;
userId?: string;
token?: string;
importData?: Uint8Array;
exportData?: (binary: Uint8Array) => void;
};
export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
@@ -199,11 +217,20 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
workspace: string,
options: YjsInitOptions
): Promise<YjsAdapter> {
const { backend, params = {}, userId = 'default', token } = options;
const {
backend,
params = {},
userId = 'default',
token,
importData,
exportData,
} = options;
const providers = await _initYjsDatabase(backend, workspace, {
params,
userId,
token,
importData,
exportData,
});
return new YjsAdapter(providers);
}
@@ -374,7 +401,7 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
};
check();
});
await new IndexeddbPersistence(this._provider.idb.name, doc)
await new IndexedDBProvider(this._provider.idb.name, doc)
.whenSynced;
applyUpdate(doc, new Uint8Array(binary));
await update_check;