mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-12 23:56:36 +08:00
Merge remote-tracking branch 'origin/develop' into feature/new-grid-drop
This commit is contained in:
Vendored
+2
@@ -4,6 +4,7 @@
|
||||
"editor.formatOnSaveMode": "file",
|
||||
"prettier.prettierPath": "./node_modules/prettier",
|
||||
"cSpell.words": [
|
||||
"AUTOINCREMENT",
|
||||
"Backlinks",
|
||||
"blockdb",
|
||||
"booktitle",
|
||||
@@ -12,6 +13,7 @@
|
||||
"cssmodule",
|
||||
"datasource",
|
||||
"fflate",
|
||||
"fstore",
|
||||
"groq",
|
||||
"howpublished",
|
||||
"immer",
|
||||
|
||||
@@ -46,8 +46,8 @@ See https://github.com/all-?/all-contributors/issues/361#issuecomment-637166066
|
||||
|
||||
# How to use
|
||||
|
||||
If you have experience in front-end development, please [refer to here](https://affine.gitbook.io/affine/basic-documentation/contribute-to-affine); if you want to experience our latest version, please wait a moment, we will launch a web version in the near future.
|
||||
And, thanks to Lee who [made a desktop build with Tauri](https://github.com/m1911star/affine-client) for you to try out.
|
||||
If you have experience in front-end development, you may wish to refer to our [documentation](https://affine.gitbook.io/affine/basic-documentation/contribute-to-affine) to learn more about deploying your own version or contributing further to development. For those intersting in trying our latest version, please bear with us as we are planning to launch a web version soon.
|
||||
Also, thanks to Lee who has made a [desktop build with Tauri](https://github.com/m1911star/affine-client) for you to try out.
|
||||
Please notice that AFFiNE is still under Alpha stage and is not ready for production use.
|
||||
|
||||
# Table of contents
|
||||
@@ -85,13 +85,13 @@ Please notice that AFFiNE is still under Alpha stage and is not ready for produc
|
||||
|
||||
## Create your story
|
||||
|
||||
We want your data always to be yours, and we don't want to make any sacrifice to your accessibility. Your data is always local-stored first, yet we support real-time collaboration on a peer-to-peer basis. We don't think "privacy-first" is a good excuse for not supporting modern web features.
|
||||
Collaboration isn't only necessary for teams -- you may take and insert pics on your phone, then edit them on your desktop, and share them with your collaborators.
|
||||
Affine is fully built with web technologies so that consistency and accessibility are always guaranteed on Mac, Windows and Linux. The local file system support will be available when version 0.0.1beta is released.
|
||||
We want your data always to be yours, without any sacrifice to your accessibility. Your data is always stored local first, yet we support real-time collaboration on a peer-to-peer basis. We don't think "privacy-first" is a good excuse for not supporting modern web features.
|
||||
And when it comes to collaboration, these features are not just necessarily for teams -- you can take and insert pictures on your phone, edit them from your desktop, and then share them with your collaborators.
|
||||
Affine is fully built with web technologies to ensure consistency and accessibility on Mac, Windows and Linux. The local file system support will be available when version 0.0.1beta is released.
|
||||
|
||||
# Documentation
|
||||
|
||||
AFFiNE is not yet ready for production use. To install, you may check how to build or deploy the AFFiNE in [quick-start](https://affine.gitbook.io/affine/basic-documentation/contribute-to-affine/quick-start). For the full documentation, please view it [here](https://affine.gitbook.io/affine/).
|
||||
AFFiNE is not yet ready for production use. For installation, you may check how to build or deploy AFFiNE from our [quick-start](https://affine.gitbook.io/affine/basic-documentation/contribute-to-affine/quick-start) guide. Alternatively, you can view our [full documentation](https://affine.gitbook.io/affine/).
|
||||
|
||||
## Getting Started with development
|
||||
|
||||
@@ -107,28 +107,28 @@ Get our latest [release notes](https://github.com/toeverything/AFFiNE/wiki) from
|
||||
|
||||
# Feature requests
|
||||
|
||||
Please go to [Feature request](https://github.com/toeverything/AFFiNE/issues).
|
||||
Please go to [feature requests](https://github.com/toeverything/AFFiNE/issues).
|
||||
|
||||
# FAQ
|
||||
|
||||
Get quick help on [Telegram](https://t.me/affineworkos) and [Discord](https://discord.gg/yz6tGVsf5p) along with other developers and contributors.
|
||||
Get quick help on [Telegram](https://t.me/affineworkos) or [Discord](https://discord.gg/yz6tGVsf5p) and join our community of developers and contributors.
|
||||
|
||||
Latest news and technology sharing on [Twitter](https://twitter.com/AffineOfficial), [Medium](https://medium.com/@affineworkos) and [AFFiNE Blog](https://blog.affine.pro/).
|
||||
Our latest news can be found on [Twitter](https://twitter.com/AffineOfficial), [Medium](https://medium.com/@affineworkos) and the [AFFiNE Blog](https://blog.affine.pro/).
|
||||
|
||||
# The Philosophy of AFFiNE
|
||||
|
||||
Timothy Berners-Lee once taught us about the idea of the semantic web, where all the data can be interpreted in any form while the "truth" is kept. This gives our best image of an ideal knowledge base by far, that sorting of information, planning of project and goals as well as creating of knowledge can be all together.
|
||||
We have witnessed waves of paradigm shift so many times. At first, everything was noted on office-like apps or DSL like LaTeX, then we found todo-list apps and WYSIWYG markdown editors better for writing and planning. Finally, here comes Notion and Miro, who take advantage of the idea of blocks to further liberate our creativity.
|
||||
It is all perfect... If there are not so many waste operations and redundant information. And, we insist that privacy first should always be given by default.
|
||||
It is all perfect... without waste operations and redundant information. And, we insist that privacy first should always be given by default.
|
||||
That's why we are making AFFiNE. Some of the most important features are:
|
||||
|
||||
- Transformable
|
||||
- Every block can be transformed equally well as a database
|
||||
- e.g. you can now set up a to-do with MarkDown in text view and edit it in kanban view.
|
||||
- Every doc can be turned into a whiteboard
|
||||
- Every block can be transformed equally
|
||||
- e.g. you can create a todo in Markdown in the text view and then later edit it in the kanban view.
|
||||
- Every document can be turned into a whiteboard
|
||||
- An always good-to-read, structured docs-form page is the best for your notes, but a boundless doodle surface is better for collaboration and creativity.
|
||||
- Atomic
|
||||
- The basic element of affine are blocks, not pages.
|
||||
- The basic elements of AFFiNE are blocks, not pages.
|
||||
- Blocks can be directly reused and synced between pages.
|
||||
- Pages and blocks are searched and organized based on connected graphs, not tree-like paths.
|
||||
- Dual-link and semantic search are fully supported.
|
||||
@@ -136,8 +136,7 @@ That's why we are making AFFiNE. Some of the most important features are:
|
||||
- Data is always stored locally by default
|
||||
- CRDTs are applied so that peer-to-peer collaboration is possible.
|
||||
|
||||
We really appreciate the idea of Monday, Airtable and Notion databases. They inspired what we think is right for task management. But we don't like the repeated works -- we don't want to set a todo easily with markdown but end up re-write it again in kanban or other databases.
|
||||
With AFFiNE, every block group has infinite views, for you to keep your single source of truth.
|
||||
We appreciate the ideas of Monday, Airtable, and Notion databases. They have inspired us and shaped our product, helping us get it right when it comes to task management. But we also do things differently. We don't like doing things again and again. It's easy to set a todo with Markdown, but then why do you need to repeat and recreate data for a kanban or other databases. This is the power of AFFiNE. With AFFiNE, every block group has infinite views, for you to keep your single source of data, a signle source of truth.
|
||||
|
||||
We would like to give special thanks to the innovators and pioneers who greatly inspired us:
|
||||
|
||||
|
||||
@@ -1,34 +1,32 @@
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import {
|
||||
MuiBox as Box,
|
||||
MuiButton as Button,
|
||||
MuiCollapse as Collapse,
|
||||
MuiIconButton as IconButton,
|
||||
styled,
|
||||
} from '@toeverything/components/ui';
|
||||
import {
|
||||
AddIcon,
|
||||
ArrowDropDownIcon,
|
||||
ArrowRightIcon,
|
||||
} from '@toeverything/components/icons';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import {
|
||||
usePageTree,
|
||||
useCalendarHeatmap,
|
||||
usePageTree,
|
||||
} from '@toeverything/components/layout';
|
||||
import { AddIcon } from '@toeverything/components/icons';
|
||||
import {
|
||||
MuiBox as Box,
|
||||
MuiCollapse as Collapse,
|
||||
styled,
|
||||
} from '@toeverything/components/ui';
|
||||
import { services } from '@toeverything/datasource/db-service';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
const StyledContainer = styled('div')({
|
||||
const StyledBtn = styled('div')({
|
||||
height: '32px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const StyledBtn = styled('div')({
|
||||
color: '#98ACBD',
|
||||
textTransform: 'none',
|
||||
fontSize: '12px',
|
||||
fontWeight: '600',
|
||||
cursor: 'pointer',
|
||||
userSelect: 'none',
|
||||
flex: 1,
|
||||
});
|
||||
|
||||
export type CollapsiblePageTreeProps = {
|
||||
@@ -81,16 +79,14 @@ export function CollapsiblePageTree(props: CollapsiblePageTreeProps) {
|
||||
onMouseEnter={() => setNewPageBtnVisible(true)}
|
||||
onMouseLeave={() => setNewPageBtnVisible(false)}
|
||||
>
|
||||
<StyledContainer>
|
||||
<StyledBtn onClick={() => setOpen(prev => !prev)}>
|
||||
{open ? (
|
||||
<ArrowDropDownIcon sx={{ color: '#566B7D' }} />
|
||||
) : (
|
||||
<ArrowRightIcon sx={{ color: '#566B7D' }} />
|
||||
)}
|
||||
<StyledBtn onClick={() => setOpen(prev => !prev)}>
|
||||
{title}
|
||||
</StyledBtn>
|
||||
</StyledContainer>
|
||||
{title}
|
||||
</StyledBtn>
|
||||
|
||||
{newPageBtnVisible && (
|
||||
<AddIcon
|
||||
|
||||
@@ -148,6 +148,12 @@ module.exports = function (webpackConfig) {
|
||||
}
|
||||
}
|
||||
|
||||
config.module.rules.unshift({
|
||||
test: /\.wasm$/,
|
||||
type: 'asset/resource',
|
||||
});
|
||||
config.resolve.fallback = { crypto: false, fs: false, path: false };
|
||||
|
||||
addEmotionBabelPlugin(config);
|
||||
|
||||
config.plugins = [
|
||||
|
||||
@@ -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',
|
||||
};
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
Executable → Regular
+95
-80
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -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);
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
export { IndexedDBProvider } from './indexeddb';
|
||||
export { WebsocketProvider } from './provider';
|
||||
export { SQLiteProvider } from './sqlite';
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Generated
+21
-11
@@ -594,7 +594,6 @@ importers:
|
||||
sift: ^16.0.0
|
||||
ts-debounce: ^4.0.0
|
||||
uuid: ^8.3.2
|
||||
y-indexeddb: ^9.0.9
|
||||
y-protocols: ^1.0.5
|
||||
yjs: ^13.5.41
|
||||
dependencies:
|
||||
@@ -622,17 +621,21 @@ importers:
|
||||
lib0: 0.2.52
|
||||
lru-cache: 7.13.2
|
||||
ts-debounce: 4.0.0
|
||||
y-indexeddb: 9.0.9_yjs@13.5.41
|
||||
|
||||
libs/datasource/jwt-rpc:
|
||||
specifiers:
|
||||
'@types/sql.js': ^1.4.3
|
||||
lib0: ^0.2.52
|
||||
sql.js: ^1.7.0
|
||||
y-protocols: ^1.0.5
|
||||
yjs: ^13.5.41
|
||||
dependencies:
|
||||
lib0: 0.2.52
|
||||
sql.js: 1.7.0
|
||||
y-protocols: 1.0.5
|
||||
yjs: 13.5.41
|
||||
devDependencies:
|
||||
'@types/sql.js': 1.4.3
|
||||
|
||||
libs/datasource/remote-kv:
|
||||
specifiers:
|
||||
@@ -6691,6 +6694,10 @@ packages:
|
||||
resolution: {integrity: sha512-uw8eYMIReOwstQ0QKF0sICefSy8cNO/v7gOTiIy9SbwuHyEecJUm7qlgueOO5S1udZ5I/irVydHVwMchgzbKTg==}
|
||||
dev: true
|
||||
|
||||
/@types/emscripten/1.39.6:
|
||||
resolution: {integrity: sha512-H90aoynNhhkQP6DRweEjJp5vfUVdIj7tdPLsu7pq89vODD/lcugKfZOsfgwpvM6XUewEp2N5dCg1Uf3Qe55Dcg==}
|
||||
dev: true
|
||||
|
||||
/@types/eslint-scope/3.7.4:
|
||||
resolution: {integrity: sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==}
|
||||
dependencies:
|
||||
@@ -6988,6 +6995,13 @@ packages:
|
||||
'@types/node': 18.0.1
|
||||
dev: true
|
||||
|
||||
/@types/sql.js/1.4.3:
|
||||
resolution: {integrity: sha512-3bz1LJIiJtKMEL8tYf7c9Nrb1lYcFeWQkE8vhWvobE29ZzizW79DtoTjqx1bR82DS2Ch2K30nOwNhuLclZ1vYg==}
|
||||
dependencies:
|
||||
'@types/emscripten': 1.39.6
|
||||
'@types/node': 18.0.1
|
||||
dev: true
|
||||
|
||||
/@types/stack-utils/2.0.1:
|
||||
resolution: {integrity: sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==}
|
||||
dev: true
|
||||
@@ -17879,6 +17893,10 @@ packages:
|
||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||
dev: true
|
||||
|
||||
/sql.js/1.7.0:
|
||||
resolution: {integrity: sha512-qAfft3xkSgHqmmfNugWTp/59PsqIw8gbeao5TZmpmzQQsAJ49de3iDDKuxVixidYs6dkHNksY8m27v2dZNn2jw==}
|
||||
dev: false
|
||||
|
||||
/sshpk/1.17.0:
|
||||
resolution: {integrity: sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -19572,15 +19590,6 @@ packages:
|
||||
engines: {node: '>=0.4'}
|
||||
dev: true
|
||||
|
||||
/y-indexeddb/9.0.9_yjs@13.5.41:
|
||||
resolution: {integrity: sha512-GcJbiJa2eD5hankj46Hea9z4hbDnDjvh1fT62E5SpZRsv8GcEemw34l1hwI2eknGcv5Ih9JfusT37JLx9q3LFg==}
|
||||
peerDependencies:
|
||||
yjs: ^13.0.0
|
||||
dependencies:
|
||||
lib0: 0.2.52
|
||||
yjs: 13.5.41
|
||||
dev: true
|
||||
|
||||
/y-protocols/1.0.5:
|
||||
resolution: {integrity: sha512-Wil92b7cGk712lRHDqS4T90IczF6RkcvCwAD0A2OPg+adKmOe+nOiT/N2hvpQIWS3zfjmtL4CPaH5sIW1Hkm/A==}
|
||||
dependencies:
|
||||
@@ -19649,6 +19658,7 @@ packages:
|
||||
resolution: {integrity: sha512-4eSTrrs8OeI0heXKKioRY4ag7V5Bk85Z4MeniUyown3o3y0G7G4JpAZWrZWfTp7pzw2b53GkAQWKqHsHi9j9JA==}
|
||||
dependencies:
|
||||
lib0: 0.2.52
|
||||
dev: false
|
||||
|
||||
/yn/3.1.1:
|
||||
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
|
||||
|
||||
Reference in New Issue
Block a user