mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat: store local data to local db (#2037)
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
cacheFolder: '../../.yarn/cache'
|
||||
deferredVersionFolder: '../../.yarn/versions'
|
||||
globalFolder: '../../.yarn/global'
|
||||
installStatePath: '../../.yarn/install-state.gz'
|
||||
patchFolder: '../../.yarn/patches'
|
||||
pnpUnpluggedFolder: '../../.yarn/unplugged'
|
||||
yarnPath: '../../.yarn/releases/yarn-3.5.0.cjs'
|
||||
virtualFolder: '../../.yarn/__virtual__'
|
||||
@@ -3,8 +3,10 @@ const {
|
||||
utils: { fromBuildIdentifier },
|
||||
} = require('@electron-forge/core');
|
||||
|
||||
const isCanary = process.env.BUILD_TYPE === 'canary';
|
||||
const path = require('node:path');
|
||||
|
||||
const isCanary = process.env.BUILD_TYPE === 'canary';
|
||||
const buildType = isCanary ? 'canary' : 'stable';
|
||||
const productName = isCanary ? 'AFFiNE-Canary' : 'AFFiNE';
|
||||
const icoPath = isCanary
|
||||
? './resources/icons/icon_canary.ico'
|
||||
@@ -13,6 +15,11 @@ const icnsPath = isCanary
|
||||
? './resources/icons/icon_canary.icns'
|
||||
: './resources/icons/icon.icns';
|
||||
|
||||
const arch =
|
||||
process.argv.indexOf('--arch') > 0
|
||||
? process.argv[process.argv.indexOf('--arch') + 1]
|
||||
: process.arch;
|
||||
|
||||
/**
|
||||
* @type {import('@electron-forge/shared-types').ForgeConfig}
|
||||
*/
|
||||
@@ -25,10 +32,10 @@ module.exports = {
|
||||
stable: 'pro.affine.app',
|
||||
}),
|
||||
icon: icnsPath,
|
||||
osxSign: {
|
||||
identity: 'Developer ID Application: TOEVERYTHING PTE. LTD.',
|
||||
'hardened-runtime': true,
|
||||
},
|
||||
// osxSign: {
|
||||
// identity: 'Developer ID Application: TOEVERYTHING PTE. LTD.',
|
||||
// 'hardened-runtime': true,
|
||||
// },
|
||||
osxNotarize: process.env.APPLE_ID
|
||||
? {
|
||||
tool: 'notarytool',
|
||||
@@ -42,9 +49,25 @@ module.exports = {
|
||||
{
|
||||
name: '@electron-forge/maker-dmg',
|
||||
config: {
|
||||
format: 'ULFO',
|
||||
icon: icnsPath,
|
||||
name: 'AFFiNE',
|
||||
'icon-size': 128,
|
||||
background: './resources/icons/dmg-background.png',
|
||||
contents: [
|
||||
{
|
||||
x: 176,
|
||||
y: 192,
|
||||
type: 'file',
|
||||
path: path.resolve(
|
||||
__dirname,
|
||||
'out',
|
||||
buildType,
|
||||
`${productName}-darwin-${arch}`,
|
||||
`${productName}.app`
|
||||
),
|
||||
},
|
||||
{ x: 432, y: 192, type: 'link', path: '/Applications' },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
3
apps/electron/layers/logger.ts
Normal file
3
apps/electron/layers/logger.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import log from 'electron-log';
|
||||
|
||||
export const logger = log;
|
||||
@@ -1,69 +0,0 @@
|
||||
import * as os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
import { app, shell } from 'electron';
|
||||
import { BrowserWindow, ipcMain, nativeTheme } from 'electron';
|
||||
import fs from 'fs-extra';
|
||||
import { parse } from 'url';
|
||||
|
||||
import { isMacOS } from '../../../utils';
|
||||
import { getExchangeTokenParams, oauthEndpoint } from './google-auth';
|
||||
|
||||
const AFFINE_ROOT = path.join(os.homedir(), '.affine');
|
||||
|
||||
fs.ensureDirSync(AFFINE_ROOT);
|
||||
|
||||
const logger = console;
|
||||
|
||||
export const registerHandlers = () => {
|
||||
ipcMain.handle('ui:theme-change', async (_, theme) => {
|
||||
nativeTheme.themeSource = theme;
|
||||
logger.info('theme change', theme);
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:sidebar-visibility-change', async (_, visible) => {
|
||||
// todo
|
||||
// detect if os is macos
|
||||
if (isMacOS()) {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
windows.forEach(w => {
|
||||
// hide window buttons when sidebar is not visible
|
||||
w.setWindowButtonVisibility(visible);
|
||||
});
|
||||
logger.info('sidebar visibility change', visible);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:get-google-oauth-code', async () => {
|
||||
logger.info('starting google sign in ...');
|
||||
shell.openExternal(oauthEndpoint);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const handleOpenUrl = async (_: any, url: string) => {
|
||||
const mainWindow = BrowserWindow.getAllWindows().find(
|
||||
w => !w.isDestroyed()
|
||||
);
|
||||
const urlObj = parse(url.replace('??', '?'), true);
|
||||
if (!mainWindow || !url.startsWith('affine://auth-callback')) return;
|
||||
const code = urlObj.query['code'] as string;
|
||||
if (!code) return;
|
||||
|
||||
logger.info('google sign in code received from callback', code);
|
||||
|
||||
app.removeListener('open-url', handleOpenUrl);
|
||||
resolve(getExchangeTokenParams(code));
|
||||
};
|
||||
|
||||
app.on('open-url', handleOpenUrl);
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timed out'));
|
||||
app.removeListener('open-url', handleOpenUrl);
|
||||
}, 30000);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('main:env-update', async (_, env, value) => {
|
||||
process.env[env] = value;
|
||||
});
|
||||
};
|
||||
9
apps/electron/layers/main/src/context.ts
Normal file
9
apps/electron/layers/main/src/context.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { app } from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
export const appContext = {
|
||||
appName: app.name,
|
||||
appDataPath: path.join(app.getPath('appData'), app.name),
|
||||
};
|
||||
|
||||
export type AppContext = typeof appContext;
|
||||
34
apps/electron/layers/main/src/data/export.ts
Normal file
34
apps/electron/layers/main/src/data/export.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import { logger } from '../../../logger';
|
||||
import type { WorkspaceDatabase } from './sqlite';
|
||||
|
||||
/**
|
||||
* Start a backup of the database to the given destination.
|
||||
*/
|
||||
export async function exportDatabase(db: WorkspaceDatabase, dest: string) {
|
||||
await fs.copyFile(db.path, dest);
|
||||
logger.log('export: ', dest);
|
||||
}
|
||||
|
||||
// export async function startBackup(db: WorkspaceDatabase, dest: string) {
|
||||
// let timeout: NodeJS.Timeout | null;
|
||||
// async function backup() {
|
||||
// await fs.copyFile(db.path, dest);
|
||||
// logger.log('backup: ', dest);
|
||||
// }
|
||||
|
||||
// backup();
|
||||
|
||||
// const _db = await db.sqliteDB$;
|
||||
|
||||
// _db.on('change', () => {
|
||||
// if (timeout) {
|
||||
// clearTimeout(timeout);
|
||||
// }
|
||||
// timeout = setTimeout(async () => {
|
||||
// await backup();
|
||||
// timeout = null;
|
||||
// }, 1000);
|
||||
// });
|
||||
// }
|
||||
216
apps/electron/layers/main/src/data/sqlite.ts
Normal file
216
apps/electron/layers/main/src/data/sqlite.ts
Normal file
@@ -0,0 +1,216 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import type { Database } from 'sqlite3';
|
||||
import sqlite3Default from 'sqlite3';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
import { logger } from '../../../logger';
|
||||
import type { AppContext } from '../context';
|
||||
|
||||
const sqlite3 = sqlite3Default.verbose();
|
||||
|
||||
const schemas = [
|
||||
`CREATE TABLE IF NOT EXISTS "updates" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
data BLOB NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS "blobs" (
|
||||
key TEXT PRIMARY KEY NOT NULL,
|
||||
data BLOB NOT NULL,
|
||||
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL
|
||||
)`,
|
||||
];
|
||||
|
||||
sqlite3.verbose();
|
||||
|
||||
interface UpdateRow {
|
||||
id: number;
|
||||
data: Buffer;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
interface BlobRow {
|
||||
key: string;
|
||||
data: Buffer;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
export class WorkspaceDatabase {
|
||||
sqliteDB$: Promise<Database>;
|
||||
ydoc = new Y.Doc();
|
||||
_db: Database | null = null;
|
||||
|
||||
ready: Promise<Uint8Array>;
|
||||
|
||||
constructor(public path: string) {
|
||||
this.sqliteDB$ = this.reconnectDB();
|
||||
logger.log('open db', path);
|
||||
|
||||
this.ydoc.on('update', update => {
|
||||
this.addUpdateToSQLite(update);
|
||||
});
|
||||
|
||||
this.ready = (async () => {
|
||||
const updates = await this.getUpdates();
|
||||
updates.forEach(update => {
|
||||
Y.applyUpdate(this.ydoc, update.data);
|
||||
});
|
||||
return this.getEncodedDocUpdates();
|
||||
})();
|
||||
}
|
||||
|
||||
// release resources
|
||||
destroy = () => {
|
||||
this._db?.close();
|
||||
this.ydoc.destroy();
|
||||
};
|
||||
|
||||
reconnectDB = async () => {
|
||||
logger.log('open db', this.path);
|
||||
if (this._db) {
|
||||
const _db = this._db;
|
||||
await new Promise<void>(res =>
|
||||
_db.close(() => {
|
||||
res();
|
||||
})
|
||||
);
|
||||
}
|
||||
return (this.sqliteDB$ = new Promise(res => {
|
||||
// use cached version?
|
||||
const db = new sqlite3.Database(this.path, error => {
|
||||
if (error) {
|
||||
logger.error('open db error', error);
|
||||
}
|
||||
});
|
||||
this._db = db;
|
||||
|
||||
db.exec(schemas.join(';'), () => {
|
||||
res(db);
|
||||
});
|
||||
}));
|
||||
};
|
||||
|
||||
getEncodedDocUpdates = () => {
|
||||
return Y.encodeStateAsUpdate(this.ydoc);
|
||||
};
|
||||
|
||||
// non-blocking and use yDoc to validate the update
|
||||
// after that, the update is added to the db
|
||||
applyUpdate = (data: Uint8Array) => {
|
||||
Y.applyUpdate(this.ydoc, data);
|
||||
// todo: trim the updates when the number of records is too large
|
||||
// 1. store the current ydoc state in the db
|
||||
// 2. then delete the old updates
|
||||
// yjs-idb will always trim the db for the first time after DB is loaded
|
||||
};
|
||||
|
||||
addBlob = async (key: string, data: Uint8Array) => {
|
||||
const db = await this.sqliteDB$;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
db.run(
|
||||
'INSERT INTO blobs (key, data) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET data = ?',
|
||||
[key, data, data],
|
||||
err => {
|
||||
if (err) {
|
||||
logger.error('addBlob', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
getBlob = async (key: string) => {
|
||||
const db = await this.sqliteDB$;
|
||||
return new Promise<Uint8Array | null>((resolve, reject) => {
|
||||
db.get<BlobRow>(
|
||||
'SELECT data FROM blobs WHERE key = ?',
|
||||
[key],
|
||||
(err, row) => {
|
||||
if (err) {
|
||||
logger.error('getBlob', err);
|
||||
reject(err);
|
||||
} else if (!row) {
|
||||
logger.error('getBlob', 'not found');
|
||||
resolve(null);
|
||||
} else {
|
||||
resolve(row.data);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
deleteBlob = async (key: string) => {
|
||||
const db = await this.sqliteDB$;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
db.run('DELETE FROM blobs WHERE key = ?', [key], err => {
|
||||
if (err) {
|
||||
logger.error('deleteBlob', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
getPersistentBlobKeys = async () => {
|
||||
const db = await this.sqliteDB$;
|
||||
return new Promise<string[]>((resolve, reject) => {
|
||||
db.all<BlobRow>('SELECT key FROM blobs', (err, rows) => {
|
||||
if (err) {
|
||||
logger.error('getPersistentBlobKeys', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows.map(row => row.key));
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private getUpdates = async () => {
|
||||
const db = await this.sqliteDB$;
|
||||
return new Promise<{ id: number; data: any }[]>((resolve, reject) => {
|
||||
// do we need to order by id?
|
||||
db.all<UpdateRow>('SELECT * FROM updates', (err, rows) => {
|
||||
if (err) {
|
||||
logger.error('getUpdates', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(rows);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
private addUpdateToSQLite = async (data: Uint8Array) => {
|
||||
const db = await this.sqliteDB$;
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
db.run('INSERT INTO updates (data) VALUES (?)', [data], err => {
|
||||
if (err) {
|
||||
logger.error('addUpdateToSQLite', err);
|
||||
reject(err);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export async function openWorkspaceDatabase(
|
||||
context: AppContext,
|
||||
workspaceId: string
|
||||
) {
|
||||
const basePath = path.join(context.appDataPath, 'workspaces', workspaceId);
|
||||
// hmmm.... blocking api but it should be fine, right?
|
||||
await fs.ensureDir(basePath);
|
||||
const dbPath = path.join(basePath, 'storage.db');
|
||||
|
||||
return new WorkspaceDatabase(dbPath);
|
||||
}
|
||||
20
apps/electron/layers/main/src/data/workspace.ts
Normal file
20
apps/electron/layers/main/src/data/workspace.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import fs from 'fs-extra';
|
||||
|
||||
import type { AppContext } from '../context';
|
||||
|
||||
export async function listWorkspaces(context: AppContext) {
|
||||
const basePath = path.join(context.appDataPath, 'workspaces');
|
||||
return fs.readdir(basePath);
|
||||
}
|
||||
|
||||
export async function deleteWorkspace(context: AppContext, id: string) {
|
||||
const basePath = path.join(context.appDataPath, 'workspaces', id);
|
||||
const movedPath = path.join(
|
||||
context.appDataPath,
|
||||
'delete-workspaces',
|
||||
`${id}`
|
||||
);
|
||||
return fs.move(basePath, movedPath);
|
||||
}
|
||||
184
apps/electron/layers/main/src/handlers.ts
Normal file
184
apps/electron/layers/main/src/handlers.ts
Normal file
@@ -0,0 +1,184 @@
|
||||
import {
|
||||
app,
|
||||
BrowserWindow,
|
||||
dialog,
|
||||
ipcMain,
|
||||
nativeTheme,
|
||||
shell,
|
||||
} from 'electron';
|
||||
import { parse } from 'url';
|
||||
|
||||
import { logger } from '../../logger';
|
||||
import { isMacOS } from '../../utils';
|
||||
import { appContext } from './context';
|
||||
import { exportDatabase } from './data/export';
|
||||
import type { WorkspaceDatabase } from './data/sqlite';
|
||||
import { openWorkspaceDatabase } from './data/sqlite';
|
||||
import { deleteWorkspace, listWorkspaces } from './data/workspace';
|
||||
import { getExchangeTokenParams, oauthEndpoint } from './google-auth';
|
||||
|
||||
let currentWorkspaceId = '';
|
||||
|
||||
const dbMapping = new Map<string, WorkspaceDatabase>();
|
||||
|
||||
async function ensureWorkspaceDB(id: string) {
|
||||
let workspaceDB = dbMapping.get(id);
|
||||
if (!workspaceDB) {
|
||||
// hmm... potential race condition?
|
||||
workspaceDB = await openWorkspaceDatabase(appContext, id);
|
||||
dbMapping.set(id, workspaceDB);
|
||||
}
|
||||
await workspaceDB.ready;
|
||||
return workspaceDB;
|
||||
}
|
||||
|
||||
function registerWorkspaceHandlers() {
|
||||
ipcMain.handle('workspace:list', async _ => {
|
||||
logger.info('list workspaces');
|
||||
return listWorkspaces(appContext);
|
||||
});
|
||||
|
||||
ipcMain.handle('workspace:delete', async (_, id) => {
|
||||
logger.info('delete workspace', id);
|
||||
return deleteWorkspace(appContext, id);
|
||||
});
|
||||
}
|
||||
|
||||
function registerUIHandlers() {
|
||||
ipcMain.handle('ui:theme-change', async (_, theme) => {
|
||||
nativeTheme.themeSource = theme;
|
||||
logger.info('theme change', theme);
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:sidebar-visibility-change', async (_, visible) => {
|
||||
// todo
|
||||
// detect if os is macos
|
||||
if (isMacOS()) {
|
||||
const windows = BrowserWindow.getAllWindows();
|
||||
windows.forEach(w => {
|
||||
// hide window buttons when sidebar is not visible
|
||||
w.setWindowButtonVisibility(visible);
|
||||
});
|
||||
logger.info('sidebar visibility change', visible);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:workspace-change', async (_, workspaceId) => {
|
||||
logger.info('workspace change', workspaceId);
|
||||
currentWorkspaceId = workspaceId;
|
||||
});
|
||||
|
||||
// @deprecated
|
||||
ipcMain.handle('ui:get-google-oauth-code', async () => {
|
||||
logger.info('starting google sign in ...');
|
||||
shell.openExternal(oauthEndpoint);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const handleOpenUrl = async (_: any, url: string) => {
|
||||
const mainWindow = BrowserWindow.getAllWindows().find(
|
||||
w => !w.isDestroyed()
|
||||
);
|
||||
const urlObj = parse(url.replace('??', '?'), true);
|
||||
if (!mainWindow || !url.startsWith('affine://auth-callback')) return;
|
||||
const code = urlObj.query['code'] as string;
|
||||
if (!code) return;
|
||||
|
||||
logger.info('google sign in code received from callback', code);
|
||||
|
||||
app.removeListener('open-url', handleOpenUrl);
|
||||
resolve(getExchangeTokenParams(code));
|
||||
};
|
||||
|
||||
app.on('open-url', handleOpenUrl);
|
||||
|
||||
setTimeout(() => {
|
||||
reject(new Error('Timed out'));
|
||||
app.removeListener('open-url', handleOpenUrl);
|
||||
}, 30000);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.handle('main:env-update', async (_, env, value) => {
|
||||
process.env[env] = value;
|
||||
});
|
||||
}
|
||||
|
||||
function registerDBHandlers() {
|
||||
app.on('activate', () => {
|
||||
for (const [_, workspaceDB] of dbMapping) {
|
||||
workspaceDB.reconnectDB();
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.handle('db:get-doc', async (_, id) => {
|
||||
logger.log('main: get doc', id);
|
||||
const workspaceDB = await ensureWorkspaceDB(id);
|
||||
return workspaceDB.getEncodedDocUpdates();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:apply-doc-update', async (_, id, update) => {
|
||||
logger.log('main: apply doc update', id);
|
||||
const workspaceDB = await ensureWorkspaceDB(id);
|
||||
return workspaceDB.applyUpdate(update);
|
||||
});
|
||||
|
||||
ipcMain.handle('db:add-blob', async (_, workspaceId, key, data) => {
|
||||
logger.log('main: add blob', workspaceId, key);
|
||||
const workspaceDB = await ensureWorkspaceDB(workspaceId);
|
||||
return workspaceDB.addBlob(key, data);
|
||||
});
|
||||
|
||||
ipcMain.handle('db:get-blob', async (_, workspaceId, key) => {
|
||||
logger.log('main: get blob', workspaceId, key);
|
||||
const workspaceDB = await ensureWorkspaceDB(workspaceId);
|
||||
return workspaceDB.getBlob(key);
|
||||
});
|
||||
|
||||
ipcMain.handle('db:get-persisted-blobs', async (_, workspaceId) => {
|
||||
logger.log('main: get persisted blob keys', workspaceId);
|
||||
const workspaceDB = await ensureWorkspaceDB(workspaceId);
|
||||
return workspaceDB.getPersistentBlobKeys();
|
||||
});
|
||||
|
||||
ipcMain.handle('db:delete-blob', async (_, workspaceId, key) => {
|
||||
logger.log('main: delete blob', workspaceId, key);
|
||||
const workspaceDB = await ensureWorkspaceDB(workspaceId);
|
||||
return workspaceDB.deleteBlob(key);
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:open-db-folder', async _ => {
|
||||
const workspaceDB = await ensureWorkspaceDB(currentWorkspaceId);
|
||||
logger.log('main: open db folder', workspaceDB.path);
|
||||
shell.showItemInFolder(workspaceDB.path);
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:open-load-db-file-dialog', async () => {
|
||||
// todo
|
||||
});
|
||||
|
||||
ipcMain.handle('ui:open-save-db-file-dialog', async () => {
|
||||
logger.log('main: open save db file dialog', currentWorkspaceId);
|
||||
const workspaceDB = await ensureWorkspaceDB(currentWorkspaceId);
|
||||
const ret = await dialog.showSaveDialog({
|
||||
properties: ['showOverwriteConfirmation'],
|
||||
title: 'Save Workspace',
|
||||
buttonLabel: 'Save',
|
||||
defaultPath: currentWorkspaceId + '.db',
|
||||
message: 'Save Workspace as SQLite Database',
|
||||
});
|
||||
const filePath = ret.filePath;
|
||||
if (ret.canceled || !filePath) {
|
||||
return null;
|
||||
}
|
||||
|
||||
await exportDatabase(workspaceDB, filePath);
|
||||
shell.showItemInFolder(filePath);
|
||||
return filePath;
|
||||
});
|
||||
}
|
||||
|
||||
export const registerHandlers = () => {
|
||||
registerWorkspaceHandlers();
|
||||
registerUIHandlers();
|
||||
registerDBHandlers();
|
||||
};
|
||||
@@ -3,7 +3,8 @@ import './security-restrictions';
|
||||
import { app } from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
import { registerHandlers } from './app-state';
|
||||
import { logger } from '../../logger';
|
||||
import { registerHandlers } from './handlers';
|
||||
import { restoreOrCreateWindow } from './main-window';
|
||||
import { registerProtocol } from './protocol';
|
||||
|
||||
@@ -22,6 +23,7 @@ if (process.defaultApp) {
|
||||
*/
|
||||
const isSingleInstance = app.requestSingleInstanceLock();
|
||||
if (!isSingleInstance) {
|
||||
logger.info('Another instance is running, exiting...');
|
||||
app.quit();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BrowserWindow, nativeTheme } from 'electron';
|
||||
import electronWindowState from 'electron-window-state';
|
||||
import { join } from 'path';
|
||||
|
||||
import { logger } from '../../logger';
|
||||
import { isMacOS } from '../../utils';
|
||||
|
||||
const IS_DEV = process.env.NODE_ENV === 'development';
|
||||
@@ -87,7 +88,7 @@ export async function restoreOrCreateWindow() {
|
||||
browserWindow.restore();
|
||||
}
|
||||
|
||||
browserWindow.focus();
|
||||
logger.info('Create main window');
|
||||
|
||||
return browserWindow;
|
||||
}
|
||||
|
||||
2
apps/electron/layers/preload/preload.d.ts
vendored
2
apps/electron/layers/preload/preload.d.ts
vendored
@@ -7,6 +7,6 @@ interface Window {
|
||||
*
|
||||
* @see https://github.com/cawa-93/dts-for-context-bridge
|
||||
*/
|
||||
readonly apis: { workspaceSync: (id: string) => Promise<any>; onThemeChange: (theme: string) => Promise<any>; onSidebarVisibilityChange: (visible: boolean) => Promise<any>; getGoogleOauthCode: () => Promise<{ requestInit: RequestInit; url: string; }>; updateEnv: (env: string, value: string) => void; };
|
||||
readonly apis: { db: { getDoc: (id: string) => Promise<Uint8Array>; applyDocUpdate: (id: string, update: Uint8Array) => Promise<any>; addBlob: (workspaceId: string, key: string, data: Uint8Array) => Promise<any>; getBlob: (workspaceId: string, key: string) => Promise<Uint8Array>; deleteBlob: (workspaceId: string, key: string) => Promise<any>; getPersistedBlobs: (workspaceId: string) => Promise<string[]>; }; workspace: { list: () => Promise<string[]>; delete: (id: string) => Promise<void>; }; openLoadDBFileDialog: () => Promise<any>; openSaveDBFileDialog: () => Promise<any>; onThemeChange: (theme: string) => Promise<any>; onSidebarVisibilityChange: (visible: boolean) => Promise<any>; onWorkspaceChange: (workspaceId: string) => Promise<any>; openDBFolder: () => Promise<any>; getGoogleOauthCode: () => Promise<{ requestInit: RequestInit; url: string; }>; updateEnv: (env: string, value: string) => void; };
|
||||
readonly appInfo: { electron: boolean; isMacOS: boolean; };
|
||||
}
|
||||
|
||||
@@ -22,7 +22,32 @@ import { isMacOS } from '../../utils';
|
||||
* @see https://github.com/cawa-93/dts-for-context-bridge
|
||||
*/
|
||||
contextBridge.exposeInMainWorld('apis', {
|
||||
workspaceSync: (id: string) => ipcRenderer.invoke('octo:workspace-sync', id),
|
||||
db: {
|
||||
// TODO: do we need to store the workspace list locally?
|
||||
// workspace providers
|
||||
getDoc: (id: string): Promise<Uint8Array | null> =>
|
||||
ipcRenderer.invoke('db:get-doc', id),
|
||||
applyDocUpdate: (id: string, update: Uint8Array) =>
|
||||
ipcRenderer.invoke('db:apply-doc-update', id, update),
|
||||
addBlob: (workspaceId: string, key: string, data: Uint8Array) =>
|
||||
ipcRenderer.invoke('db:add-blob', workspaceId, key, data),
|
||||
getBlob: (workspaceId: string, key: string): Promise<Uint8Array | null> =>
|
||||
ipcRenderer.invoke('db:get-blob', workspaceId, key),
|
||||
deleteBlob: (workspaceId: string, key: string) =>
|
||||
ipcRenderer.invoke('db:delete-blob', workspaceId, key),
|
||||
getPersistedBlobs: (workspaceId: string): Promise<string[]> =>
|
||||
ipcRenderer.invoke('db:get-persisted-blobs', workspaceId),
|
||||
},
|
||||
|
||||
workspace: {
|
||||
list: (): Promise<string[]> => ipcRenderer.invoke('workspace:list'),
|
||||
delete: (id: string): Promise<void> =>
|
||||
ipcRenderer.invoke('workspace:delete', id),
|
||||
// create will be implicitly called by db functions
|
||||
},
|
||||
|
||||
openLoadDBFileDialog: () => ipcRenderer.invoke('ui:open-load-db-file-dialog'),
|
||||
openSaveDBFileDialog: () => ipcRenderer.invoke('ui:open-save-db-file-dialog'),
|
||||
|
||||
// ui
|
||||
onThemeChange: (theme: string) =>
|
||||
@@ -31,6 +56,11 @@ contextBridge.exposeInMainWorld('apis', {
|
||||
onSidebarVisibilityChange: (visible: boolean) =>
|
||||
ipcRenderer.invoke('ui:sidebar-visibility-change', visible),
|
||||
|
||||
onWorkspaceChange: (workspaceId: string) =>
|
||||
ipcRenderer.invoke('ui:workspace-change', workspaceId),
|
||||
|
||||
openDBFolder: () => ipcRenderer.invoke('ui:open-db-folder'),
|
||||
|
||||
/**
|
||||
* Try sign in using Google and return a request object to exchange the code for a token
|
||||
* Not exchange in Node side because it is easier to do it in the renderer with VPN
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
"@electron-forge/shared-types": "^6.1.1",
|
||||
"@electron/rebuild": "^3.2.12",
|
||||
"@electron/remote": "2.0.9",
|
||||
"@types/fs-extra": "^11.0.1",
|
||||
"cross-env": "7.0.3",
|
||||
"dts-for-context-bridge": "^0.7.1",
|
||||
"electron": "24.1.2",
|
||||
"electron-squirrel-startup": "1.0.0",
|
||||
@@ -38,11 +40,12 @@
|
||||
"zx": "^7.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"cross-env": "7.0.3",
|
||||
"electron-log": "^5.0.0-beta.22",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"firebase": "^9.19.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"undici": "^5.21.2"
|
||||
"sqlite3": "^5.1.6",
|
||||
"undici": "^5.21.2",
|
||||
"yjs": "^13.5.53"
|
||||
},
|
||||
"build": {
|
||||
"protocols": [
|
||||
@@ -54,6 +57,8 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"packageManager": "yarn@3.5.0",
|
||||
"stableVersion": "0.5.3"
|
||||
"stableVersion": "0.5.3",
|
||||
"installConfig": {
|
||||
"hoistingLimits": "workspaces"
|
||||
}
|
||||
}
|
||||
|
||||
BIN
apps/electron/resources/icons/dmg-background.png
Normal file
BIN
apps/electron/resources/icons/dmg-background.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 105 KiB |
BIN
apps/electron/resources/icons/dmg-background@2x.png
Normal file
BIN
apps/electron/resources/icons/dmg-background@2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 342 KiB |
@@ -1,4 +1,3 @@
|
||||
|
||||
const NODE_MAJOR_VERSION = 18;
|
||||
|
||||
const nativeNodeModulesPlugin = {
|
||||
@@ -29,7 +28,7 @@ export default () => {
|
||||
bundle: true,
|
||||
target: `node${NODE_MAJOR_VERSION}`,
|
||||
platform: 'node',
|
||||
external: ['electron'],
|
||||
external: ['electron', 'sqlite3'],
|
||||
plugins: [nativeNodeModulesPlugin],
|
||||
define: define,
|
||||
},
|
||||
|
||||
@@ -33,39 +33,44 @@ if (process.platform === 'win32') {
|
||||
$.shell = 'powershell.exe';
|
||||
$.prefix = '';
|
||||
}
|
||||
// step 1: build web (nextjs) dist
|
||||
process.env.ENABLE_LEGACY_PROVIDER = 'false';
|
||||
|
||||
cd(repoRootDir);
|
||||
await $`yarn add`;
|
||||
await $`yarn build`;
|
||||
await $`yarn export`;
|
||||
|
||||
// step 1.5: amend sourceMappingURL to allow debugging in devtools
|
||||
await glob('**/*.{js,css}', { cwd: affineWebOutDir }).then(files => {
|
||||
return files.map(async file => {
|
||||
const dir = path.dirname(file);
|
||||
const fullpath = path.join(affineWebOutDir, file);
|
||||
let content = await fs.readFile(fullpath, 'utf-8');
|
||||
// replace # sourceMappingURL=76-6370cd185962bc89.js.map
|
||||
// to # sourceMappingURL=assets://./{dir}/76-6370cd185962bc89.js.map
|
||||
content = content.replace(/# sourceMappingURL=(.*)\.map/g, (_, p1) => {
|
||||
return `# sourceMappingURL=assets://./${dir}/${p1}.map`;
|
||||
});
|
||||
await fs.writeFile(fullpath, content);
|
||||
});
|
||||
});
|
||||
|
||||
await fs.move(affineWebOutDir, publicAffineOutDir, { overwrite: true });
|
||||
|
||||
// step 2: build electron resources
|
||||
// step 1: build electron resources
|
||||
await buildLayers();
|
||||
echo('Build layers done');
|
||||
|
||||
// step 2: build web (nextjs) dist
|
||||
if (!process.env.SKIP_WEB_BUILD) {
|
||||
process.env.ENABLE_LEGACY_PROVIDER = 'false';
|
||||
await $`yarn build`;
|
||||
await $`yarn export`;
|
||||
|
||||
// step 1.5: amend sourceMappingURL to allow debugging in devtools
|
||||
await glob('**/*.{js,css}', { cwd: affineWebOutDir }).then(files => {
|
||||
return files.map(async file => {
|
||||
const dir = path.dirname(file);
|
||||
const fullpath = path.join(affineWebOutDir, file);
|
||||
let content = await fs.readFile(fullpath, 'utf-8');
|
||||
// replace # sourceMappingURL=76-6370cd185962bc89.js.map
|
||||
// to # sourceMappingURL=assets://./{dir}/76-6370cd185962bc89.js.map
|
||||
content = content.replace(/# sourceMappingURL=(.*)\.map/g, (_, p1) => {
|
||||
return `# sourceMappingURL=assets://./${dir}/${p1}.map`;
|
||||
});
|
||||
await fs.writeFile(fullpath, content);
|
||||
});
|
||||
});
|
||||
|
||||
await fs.move(affineWebOutDir, publicAffineOutDir, { overwrite: true });
|
||||
}
|
||||
|
||||
/// --------
|
||||
/// --------
|
||||
/// --------
|
||||
async function cleanup() {
|
||||
await fs.emptyDir(publicAffineOutDir);
|
||||
if (!process.env.SKIP_WEB_BUILD) {
|
||||
await fs.emptyDir(publicAffineOutDir);
|
||||
}
|
||||
await fs.emptyDir(path.join(electronRootDir, 'layers', 'main', 'dist'));
|
||||
await fs.emptyDir(path.join(electronRootDir, 'layers', 'preload', 'dist'));
|
||||
await fs.remove(path.join(electronRootDir, 'out'));
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
rootCurrentWorkspaceIdAtom,
|
||||
rootWorkspacesMetadataAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { atom } from 'jotai';
|
||||
|
||||
@@ -46,6 +47,21 @@ rootWorkspacesMetadataAtom.onMount = setAtom => {
|
||||
}
|
||||
return metadata;
|
||||
});
|
||||
|
||||
if (environment.isDesktop) {
|
||||
window.apis.workspace.list().then(workspaceIDs => {
|
||||
const newMetadata = workspaceIDs.map(w => ({
|
||||
id: w,
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
}));
|
||||
setAtom(metadata => {
|
||||
return [
|
||||
...metadata,
|
||||
...newMetadata.filter(m => !metadata.find(m2 => m2.id === m.id)),
|
||||
];
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -6,7 +6,14 @@ export const ExportPanel = () => {
|
||||
return (
|
||||
<>
|
||||
<Wrapper marginBottom="42px"> {t('Export Description')}</Wrapper>
|
||||
<Button type="light" shape="circle" disabled>
|
||||
<Button
|
||||
type="light"
|
||||
shape="circle"
|
||||
disabled={!environment.isDesktop}
|
||||
onClick={() => {
|
||||
window.apis.openSaveDBFileDialog();
|
||||
}}
|
||||
>
|
||||
{t('Export AFFiNE backup file')}
|
||||
</Button>
|
||||
</>
|
||||
|
||||
@@ -158,7 +158,13 @@ export const GeneralPanel: React.FC<PanelProps> = ({
|
||||
{/* </StyledRow>*/}
|
||||
{/*)}*/}
|
||||
|
||||
<StyledRow>
|
||||
<StyledRow
|
||||
onClick={() => {
|
||||
if (environment.isDesktop) {
|
||||
window.apis.openDBFolder();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<StyledSettingKey>{t('Workspace Type')}</StyledSettingKey>
|
||||
{isOwner ? (
|
||||
workspace.flavour === WorkspaceFlavour.LOCAL ? (
|
||||
|
||||
@@ -7,7 +7,9 @@ export const ThemeModeSwitch = () => {
|
||||
const { setTheme, resolvedTheme } = useTheme();
|
||||
|
||||
useEffect(() => {
|
||||
window.apis?.onThemeChange(resolvedTheme === 'dark' ? 'dark' : 'light');
|
||||
if (environment.isDesktop) {
|
||||
window.apis?.onThemeChange(resolvedTheme === 'dark' ? 'dark' : 'light');
|
||||
}
|
||||
}, [resolvedTheme]);
|
||||
|
||||
const [isHover, setIsHover] = useState(false);
|
||||
|
||||
@@ -95,7 +95,9 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
|
||||
const show = isPublicWorkspace ? false : sidebarOpen;
|
||||
const actualWidth = floatingSlider ? 'calc(10vw + 400px)' : sliderWidth;
|
||||
useEffect(() => {
|
||||
window.apis?.onSidebarVisibilityChange(sidebarOpen);
|
||||
if (environment.isDesktop) {
|
||||
window.apis?.onSidebarVisibilityChange(sidebarOpen);
|
||||
}
|
||||
}, [sidebarOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -28,6 +28,9 @@ export function useSyncRouterWithCurrentWorkspaceId(router: NextRouter) {
|
||||
if (targetWorkspace) {
|
||||
console.log('set workspace id', workspaceId);
|
||||
setCurrentWorkspaceId(targetWorkspace.id);
|
||||
if (environment.isDesktop) {
|
||||
window.apis?.onWorkspaceChange(targetWorkspace.id);
|
||||
}
|
||||
void router.push({
|
||||
pathname: '/workspace/[workspaceId]/all',
|
||||
query: {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"It takes up more space on your device": {
|
||||
"": "It takes up more space on your device."
|
||||
},
|
||||
"Export AFFiNE backup file": "Export AFFiNE backup file (coming soon)",
|
||||
"Export AFFiNE backup file": "Export AFFiNE backup file",
|
||||
"Saved then enable AFFiNE Cloud": "All changes are saved locally, click to enable AFFiNE Cloud.",
|
||||
"Not now": "Not now",
|
||||
"Export Description": "You can export the entire Workspace data for backup, and the exported data can be re-imported.",
|
||||
|
||||
@@ -178,7 +178,7 @@
|
||||
"Owner": "所有者",
|
||||
"Published to Web": "公开到互联网",
|
||||
"Data sync mode": "数据同步模式",
|
||||
"Export AFFiNE backup file": "导出 AFFiNE 备份文件(即将到来)",
|
||||
"Export AFFiNE backup file": "导出 AFFiNE 备份文件",
|
||||
"Export Description": "您可以导出整个工作区数据进行备份,导出的数据可以重新被导入。",
|
||||
"It takes up little space on your device": {
|
||||
"": "此操作会在你的设备上占用少许空间。"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { atomWithSyncStorage } from '@affine/jotai';
|
||||
import type { WorkspaceFlavour } from '@affine/workspace/type';
|
||||
import type { EditorContainer } from '@blocksuite/editor';
|
||||
import { atom, createStore } from 'jotai';
|
||||
import { atomWithStorage, createJSONStorage } from 'jotai/utils';
|
||||
|
||||
import type { WorkspaceFlavour } from './type';
|
||||
|
||||
export type RootWorkspaceMetadata = {
|
||||
id: string;
|
||||
flavour: WorkspaceFlavour;
|
||||
@@ -45,5 +46,13 @@ export const rootCurrentEditorAtom = atom<Readonly<EditorContainer> | null>(
|
||||
);
|
||||
//#endregion
|
||||
|
||||
const getStorage = () => createJSONStorage(() => localStorage);
|
||||
|
||||
export const getStoredWorkspaceMeta = () => {
|
||||
const storage = getStorage();
|
||||
const data = storage.getItem('jotai-workspaces') as RootWorkspaceMetadata[];
|
||||
return data;
|
||||
};
|
||||
|
||||
// global store
|
||||
export const rootStore = createStore();
|
||||
|
||||
25
packages/workspace/src/blob/sqlite-blob-storage.ts
Normal file
25
packages/workspace/src/blob/sqlite-blob-storage.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { BlobStorage } from '@blocksuite/store';
|
||||
|
||||
export const createSQLiteStorage = (workspaceId: string): BlobStorage => {
|
||||
return {
|
||||
crud: {
|
||||
get: async (key: string) => {
|
||||
const buffer = await window.apis.db.getBlob(workspaceId, key);
|
||||
return buffer ? new Blob([buffer]) : null;
|
||||
},
|
||||
set: async (key: string, value: Blob) => {
|
||||
return window.apis.db.addBlob(
|
||||
workspaceId,
|
||||
key,
|
||||
new Uint8Array(await value.arrayBuffer())
|
||||
);
|
||||
},
|
||||
delete: async (key: string) => {
|
||||
return window.apis.db.deleteBlob(workspaceId, key);
|
||||
},
|
||||
list: async () => {
|
||||
return window.apis.db.getPersistedBlobs(workspaceId);
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -85,18 +85,32 @@ export const CRUD: WorkspaceCRUD<WorkspaceFlavour.LOCAL> = {
|
||||
}
|
||||
data.splice(idx, 1);
|
||||
storage.setItem(kStoreKey, [...data]);
|
||||
// flywire
|
||||
if (window.apis && environment.isDesktop) {
|
||||
await window.apis.workspace.delete(workspace.id);
|
||||
}
|
||||
},
|
||||
list: async () => {
|
||||
logger.debug('list');
|
||||
const storage = getStorage();
|
||||
!Array.isArray(storage.getItem(kStoreKey)) &&
|
||||
storage.setItem(kStoreKey, []);
|
||||
return (
|
||||
await Promise.all(
|
||||
(storage.getItem(kStoreKey) as z.infer<typeof schema>).map(id =>
|
||||
CRUD.get(id)
|
||||
)
|
||||
)
|
||||
let allWorkspaceIDs: string[] = Array.isArray(storage.getItem(kStoreKey))
|
||||
? (storage.getItem(kStoreKey) as z.infer<typeof schema>)
|
||||
: [];
|
||||
|
||||
// workspaces in desktop
|
||||
if (window.apis && environment.isDesktop) {
|
||||
const desktopIds = await window.apis.workspace.list();
|
||||
// the ids maybe a subset of the local storage
|
||||
const moreWorkspaces = desktopIds.filter(
|
||||
id => !allWorkspaceIDs.includes(id)
|
||||
);
|
||||
allWorkspaceIDs = [...allWorkspaceIDs, ...moreWorkspaces];
|
||||
storage.setItem(kStoreKey, allWorkspaceIDs);
|
||||
}
|
||||
const workspaces = (
|
||||
await Promise.all(allWorkspaceIDs.map(id => CRUD.get(id)))
|
||||
).filter(item => item !== null) as LocalWorkspace[];
|
||||
|
||||
return workspaces;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import type { SQLiteProvider } from '@affine/workspace/type';
|
||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||
import type { Y as YType } from '@blocksuite/store';
|
||||
import { uuidv4, Workspace } from '@blocksuite/store';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { createSQLiteProvider } from '../index';
|
||||
|
||||
const Y = Workspace.Y;
|
||||
|
||||
let id: string;
|
||||
let workspace: Workspace;
|
||||
let provider: SQLiteProvider;
|
||||
|
||||
let offlineYdoc: YType.Doc;
|
||||
|
||||
vi.stubGlobal('window', {
|
||||
apis: {
|
||||
db: {
|
||||
getDoc: async (id: string) => {
|
||||
return Y.encodeStateAsUpdate(offlineYdoc);
|
||||
},
|
||||
applyDocUpdate: async (id: string, update: Uint8Array) => {
|
||||
Y.applyUpdate(offlineYdoc, update, 'sqlite');
|
||||
},
|
||||
getPersistedBlobs: async (id: string) => {
|
||||
return [];
|
||||
},
|
||||
} satisfies Partial<typeof window.apis.db>,
|
||||
},
|
||||
});
|
||||
|
||||
vi.stubGlobal('environment', {
|
||||
isDesktop: true,
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
id = uuidv4();
|
||||
workspace = new Workspace({
|
||||
id,
|
||||
isSSR: true,
|
||||
});
|
||||
workspace.register(AffineSchemas).register(__unstableSchemas);
|
||||
provider = createSQLiteProvider(workspace);
|
||||
offlineYdoc = new Y.Doc();
|
||||
offlineYdoc.getText('text').insert(0, '');
|
||||
});
|
||||
|
||||
describe('SQLite provider', () => {
|
||||
test('connect', async () => {
|
||||
// on connect, the updates from sqlite should be sync'ed to the existing ydoc
|
||||
// and ydoc should be sync'ed back to sqlite
|
||||
// Workspace.Y.applyUpdate(workspace.doc);
|
||||
workspace.doc.getText('text').insert(0, 'mem-hello');
|
||||
|
||||
expect(offlineYdoc.getText('text').toString()).toBe('');
|
||||
|
||||
await provider.connect();
|
||||
|
||||
expect(offlineYdoc.getText('text').toString()).toBe('mem-hello');
|
||||
expect(workspace.doc.getText('text').toString()).toBe('mem-hello');
|
||||
|
||||
workspace.doc.getText('text').insert(0, 'world');
|
||||
|
||||
// check if the data are sync'ed
|
||||
expect(offlineYdoc.getText('text').toString()).toBe('worldmem-hello');
|
||||
});
|
||||
|
||||
// todo: test disconnect
|
||||
// todo: test blob sync
|
||||
});
|
||||
@@ -4,15 +4,13 @@ import {
|
||||
getLoginStorage,
|
||||
storageChangeSlot,
|
||||
} from '@affine/workspace/affine/login';
|
||||
import type { Provider } from '@affine/workspace/type';
|
||||
import type { Provider, SQLiteProvider } from '@affine/workspace/type';
|
||||
import type {
|
||||
AffineWebSocketProvider,
|
||||
LocalIndexedDBProvider,
|
||||
} from '@affine/workspace/type';
|
||||
import type {
|
||||
Disposable,
|
||||
Workspace as BlockSuiteWorkspace,
|
||||
} from '@blocksuite/store';
|
||||
import type { BlobManager, Disposable } from '@blocksuite/store';
|
||||
import { Workspace as BlockSuiteWorkspace } from '@blocksuite/store';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import {
|
||||
createIndexedDBProvider as create,
|
||||
@@ -20,7 +18,9 @@ import {
|
||||
} from '@toeverything/y-indexeddb';
|
||||
|
||||
import { createBroadCastChannelProvider } from './broad-cast-channel';
|
||||
import { localProviderLogger } from './logger';
|
||||
import { localProviderLogger as logger } from './logger';
|
||||
|
||||
const Y = BlockSuiteWorkspace.Y;
|
||||
|
||||
const createAffineWebSocketProvider = (
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||
@@ -54,12 +54,12 @@ const createAffineWebSocketProvider = (
|
||||
connect: false,
|
||||
}
|
||||
);
|
||||
localProviderLogger.info('connect', webSocketProvider.url);
|
||||
logger.info('connect', webSocketProvider.url);
|
||||
webSocketProvider.connect();
|
||||
},
|
||||
disconnect: () => {
|
||||
assertExists(webSocketProvider);
|
||||
localProviderLogger.info('disconnect', webSocketProvider.url);
|
||||
logger.info('disconnect', webSocketProvider.url);
|
||||
webSocketProvider.destroy();
|
||||
webSocketProvider = null;
|
||||
dispose?.dispose();
|
||||
@@ -119,10 +119,7 @@ const createIndexedDBProvider = (
|
||||
// todo: cleanup data
|
||||
},
|
||||
connect: () => {
|
||||
localProviderLogger.info(
|
||||
'connect indexeddb provider',
|
||||
blockSuiteWorkspace.id
|
||||
);
|
||||
logger.info('connect indexeddb provider', blockSuiteWorkspace.id);
|
||||
indexeddbProvider.connect();
|
||||
indexeddbProvider.whenSynced
|
||||
.then(() => {
|
||||
@@ -139,20 +136,94 @@ const createIndexedDBProvider = (
|
||||
},
|
||||
disconnect: () => {
|
||||
assertExists(indexeddbProvider);
|
||||
localProviderLogger.info(
|
||||
'disconnect indexeddb provider',
|
||||
blockSuiteWorkspace.id
|
||||
);
|
||||
logger.info('disconnect indexeddb provider', blockSuiteWorkspace.id);
|
||||
indexeddbProvider.disconnect();
|
||||
callbacks.ready = false;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const createSQLiteProvider = (
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||
): SQLiteProvider => {
|
||||
const sqliteOrigin = Symbol('sqlite-provider-origin');
|
||||
// make sure it is being used in Electron with APIs
|
||||
assertExists(environment.isDesktop && window.apis);
|
||||
|
||||
function handleUpdate(update: Uint8Array, origin: unknown) {
|
||||
if (origin === sqliteOrigin) {
|
||||
return;
|
||||
}
|
||||
window.apis.db.applyDocUpdate(blockSuiteWorkspace.id, update);
|
||||
}
|
||||
|
||||
async function syncBlobIntoSQLite(bs: BlobManager) {
|
||||
const persistedKeys = await window.apis.db.getPersistedBlobs(
|
||||
blockSuiteWorkspace.id
|
||||
);
|
||||
|
||||
const allKeys = await bs.list();
|
||||
const keysToPersist = allKeys.filter(k => !persistedKeys.includes(k));
|
||||
|
||||
logger.info('persisting blobs', keysToPersist, 'to sqlite');
|
||||
keysToPersist.forEach(async k => {
|
||||
const blob = await bs.get(k);
|
||||
if (!blob) {
|
||||
logger.warn('blob url not found', k);
|
||||
return;
|
||||
}
|
||||
window.apis.db.addBlob(
|
||||
blockSuiteWorkspace.id,
|
||||
k,
|
||||
new Uint8Array(await blob.arrayBuffer())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const provider = {
|
||||
flavour: 'sqlite',
|
||||
background: true,
|
||||
cleanup: () => {
|
||||
throw new Error('Method not implemented.');
|
||||
},
|
||||
connect: async () => {
|
||||
logger.info('connecting sqlite provider', blockSuiteWorkspace.id);
|
||||
const updates = await window.apis.db.getDoc(blockSuiteWorkspace.id);
|
||||
|
||||
if (updates) {
|
||||
Y.applyUpdate(blockSuiteWorkspace.doc, updates, sqliteOrigin);
|
||||
}
|
||||
|
||||
const mergeUpdates = Y.encodeStateAsUpdate(blockSuiteWorkspace.doc);
|
||||
|
||||
// also apply updates to sqlite
|
||||
window.apis.db.applyDocUpdate(blockSuiteWorkspace.id, mergeUpdates);
|
||||
|
||||
blockSuiteWorkspace.doc.on('update', handleUpdate);
|
||||
|
||||
const bs = blockSuiteWorkspace.blobs;
|
||||
|
||||
if (bs) {
|
||||
// this can be non-blocking
|
||||
syncBlobIntoSQLite(bs);
|
||||
}
|
||||
|
||||
// blockSuiteWorkspace.doc.on('destroy', ...);
|
||||
logger.info('connecting sqlite done', blockSuiteWorkspace.id);
|
||||
},
|
||||
disconnect: () => {
|
||||
// todo: not implemented
|
||||
},
|
||||
} satisfies SQLiteProvider;
|
||||
|
||||
return provider;
|
||||
};
|
||||
|
||||
export {
|
||||
createAffineWebSocketProvider,
|
||||
createBroadCastChannelProvider,
|
||||
createIndexedDBProvider,
|
||||
createSQLiteProvider,
|
||||
};
|
||||
|
||||
export const createLocalProviders = (
|
||||
@@ -163,6 +234,7 @@ export const createLocalProviders = (
|
||||
config.enableBroadCastChannelProvider &&
|
||||
createBroadCastChannelProvider(blockSuiteWorkspace),
|
||||
createIndexedDBProvider(blockSuiteWorkspace),
|
||||
environment.isDesktop && createSQLiteProvider(blockSuiteWorkspace),
|
||||
] as any[]
|
||||
).filter(v => Boolean(v));
|
||||
};
|
||||
|
||||
@@ -35,6 +35,10 @@ export interface LocalIndexedDBProvider extends BackgroundProvider {
|
||||
whenSynced: Promise<void>;
|
||||
}
|
||||
|
||||
export interface SQLiteProvider extends BaseProvider {
|
||||
flavour: 'sqlite';
|
||||
}
|
||||
|
||||
export interface AffineWebSocketProvider extends BaseProvider {
|
||||
flavour: 'affine-websocket';
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import type { createWorkspaceApis } from '@affine/workspace/affine/api';
|
||||
import { createAffineBlobStorage } from '@affine/workspace/blob';
|
||||
import { __unstableSchemas, AffineSchemas } from '@blocksuite/blocks/models';
|
||||
import type { Generator } from '@blocksuite/store';
|
||||
import type { Generator, StoreOptions } from '@blocksuite/store';
|
||||
import { createIndexeddbStorage, Workspace } from '@blocksuite/store';
|
||||
|
||||
import { createSQLiteStorage } from './blob/sqlite-blob-storage';
|
||||
import { WorkspaceFlavour } from './type';
|
||||
|
||||
const hashMap = new Map<string, Workspace>();
|
||||
@@ -48,15 +49,26 @@ export function createEmptyBlockSuiteWorkspace(
|
||||
return hashMap.get(cacheKey) as Workspace;
|
||||
}
|
||||
const idGenerator = config?.idGenerator;
|
||||
|
||||
const blobStorages: StoreOptions['blobStorages'] = [];
|
||||
|
||||
if (flavour === WorkspaceFlavour.AFFINE) {
|
||||
blobStorages.push(id =>
|
||||
createAffineBlobStorage(id, config!.workspaceApis!)
|
||||
);
|
||||
} else {
|
||||
if (typeof window !== 'undefined') {
|
||||
blobStorages.push(createIndexeddbStorage);
|
||||
if (environment.isDesktop) {
|
||||
blobStorages.push(createSQLiteStorage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const workspace = new Workspace({
|
||||
id,
|
||||
isSSR: typeof window === 'undefined',
|
||||
blobStorages:
|
||||
flavour === WorkspaceFlavour.AFFINE
|
||||
? [id => createAffineBlobStorage(id, config!.workspaceApis!)]
|
||||
: typeof window === 'undefined'
|
||||
? []
|
||||
: [createIndexeddbStorage],
|
||||
blobStorages: blobStorages,
|
||||
idGenerator,
|
||||
})
|
||||
.register(AffineSchemas)
|
||||
|
||||
261
yarn.lock
261
yarn.lock
@@ -122,15 +122,18 @@ __metadata:
|
||||
"@electron-forge/shared-types": ^6.1.1
|
||||
"@electron/rebuild": ^3.2.12
|
||||
"@electron/remote": 2.0.9
|
||||
"@types/fs-extra": ^11.0.1
|
||||
cross-env: 7.0.3
|
||||
dts-for-context-bridge: ^0.7.1
|
||||
electron: 24.1.2
|
||||
electron-log: ^5.0.0-beta.22
|
||||
electron-squirrel-startup: 1.0.0
|
||||
electron-window-state: ^5.0.3
|
||||
esbuild: ^0.17.17
|
||||
firebase: ^9.19.1
|
||||
fs-extra: ^11.1.1
|
||||
sqlite3: ^5.1.6
|
||||
undici: ^5.21.2
|
||||
yjs: ^13.5.53
|
||||
zx: ^7.2.1
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -3867,7 +3870,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@gar/promisify@npm:^1.1.3":
|
||||
"@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3":
|
||||
version: 1.1.3
|
||||
resolution: "@gar/promisify@npm:1.1.3"
|
||||
checksum: 4059f790e2d07bf3c3ff3e0fec0daa8144fe35c1f6e0111c9921bd32106adaa97a4ab096ad7dab1e28ee6a9060083c4d1a4ada42a7f5f3f7a96b8812e2b757c1
|
||||
@@ -4603,6 +4606,25 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mapbox/node-pre-gyp@npm:^1.0.0":
|
||||
version: 1.0.10
|
||||
resolution: "@mapbox/node-pre-gyp@npm:1.0.10"
|
||||
dependencies:
|
||||
detect-libc: ^2.0.0
|
||||
https-proxy-agent: ^5.0.0
|
||||
make-dir: ^3.1.0
|
||||
node-fetch: ^2.6.7
|
||||
nopt: ^5.0.0
|
||||
npmlog: ^5.0.1
|
||||
rimraf: ^3.0.2
|
||||
semver: ^7.3.5
|
||||
tar: ^6.1.11
|
||||
bin:
|
||||
node-pre-gyp: bin/node-pre-gyp
|
||||
checksum: 1a98db05d955b74dad3814679593df293b9194853698f3f5f1ed00ecd93128cdd4b14fb8767fe44ac6981ef05c23effcfdc88710e7c1de99ccb6f647890597c8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@mdx-js/react@npm:^2.1.5":
|
||||
version: 2.3.0
|
||||
resolution: "@mdx-js/react@npm:2.3.0"
|
||||
@@ -5189,6 +5211,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@npmcli/fs@npm:^1.0.0":
|
||||
version: 1.1.1
|
||||
resolution: "@npmcli/fs@npm:1.1.1"
|
||||
dependencies:
|
||||
"@gar/promisify": ^1.0.1
|
||||
semver: ^7.3.5
|
||||
checksum: f5ad92f157ed222e4e31c352333d0901df02c7c04311e42a81d8eb555d4ec4276ea9c635011757de20cc476755af33e91622838de573b17e52e2e7703f0a9965
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@npmcli/fs@npm:^2.1.0":
|
||||
version: 2.1.2
|
||||
resolution: "@npmcli/fs@npm:2.1.2"
|
||||
@@ -5199,6 +5231,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@npmcli/move-file@npm:^1.0.1":
|
||||
version: 1.1.2
|
||||
resolution: "@npmcli/move-file@npm:1.1.2"
|
||||
dependencies:
|
||||
mkdirp: ^1.0.4
|
||||
rimraf: ^3.0.2
|
||||
checksum: c96381d4a37448ea280951e46233f7e541058cf57a57d4094dd4bdcaae43fa5872b5f2eb6bfb004591a68e29c5877abe3cdc210cb3588cbf20ab2877f31a7de7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@npmcli/move-file@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "@npmcli/move-file@npm:2.0.1"
|
||||
@@ -7592,6 +7634,13 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@tootallnate/once@npm:1":
|
||||
version: 1.1.2
|
||||
resolution: "@tootallnate/once@npm:1.1.2"
|
||||
checksum: e1fb1bbbc12089a0cb9433dc290f97bddd062deadb6178ce9bcb93bb7c1aecde5e60184bc7065aec42fe1663622a213493c48bbd4972d931aae48315f18e1be9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@tootallnate/once@npm:2":
|
||||
version: 2.0.0
|
||||
resolution: "@tootallnate/once@npm:2.0.0"
|
||||
@@ -9042,7 +9091,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"agentkeepalive@npm:^4.2.1":
|
||||
"agentkeepalive@npm:^4.1.3, agentkeepalive@npm:^4.2.1":
|
||||
version: 4.3.0
|
||||
resolution: "agentkeepalive@npm:4.3.0"
|
||||
dependencies:
|
||||
@@ -10108,6 +10157,32 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cacache@npm:^15.2.0":
|
||||
version: 15.3.0
|
||||
resolution: "cacache@npm:15.3.0"
|
||||
dependencies:
|
||||
"@npmcli/fs": ^1.0.0
|
||||
"@npmcli/move-file": ^1.0.1
|
||||
chownr: ^2.0.0
|
||||
fs-minipass: ^2.0.0
|
||||
glob: ^7.1.4
|
||||
infer-owner: ^1.0.4
|
||||
lru-cache: ^6.0.0
|
||||
minipass: ^3.1.1
|
||||
minipass-collect: ^1.0.2
|
||||
minipass-flush: ^1.0.5
|
||||
minipass-pipeline: ^1.2.2
|
||||
mkdirp: ^1.0.3
|
||||
p-map: ^4.0.0
|
||||
promise-inflight: ^1.0.1
|
||||
rimraf: ^3.0.2
|
||||
ssri: ^8.0.1
|
||||
tar: ^6.0.2
|
||||
unique-filename: ^1.1.1
|
||||
checksum: a07327c27a4152c04eb0a831c63c00390d90f94d51bb80624a66f4e14a6b6360bbf02a84421267bd4d00ca73ac9773287d8d7169e8d2eafe378d2ce140579db8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cacache@npm:^16.1.0":
|
||||
version: 16.1.3
|
||||
resolution: "cacache@npm:16.1.3"
|
||||
@@ -11550,7 +11625,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-libc@npm:^2.0.1":
|
||||
"detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "detect-libc@npm:2.0.1"
|
||||
checksum: ccb05fcabbb555beb544d48080179c18523a343face9ee4e1a86605a8715b4169f94d663c21a03c310ac824592f2ba9a5270218819bb411ad7be578a527593d7
|
||||
@@ -11949,6 +12024,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"electron-log@npm:^5.0.0-beta.22":
|
||||
version: 5.0.0-beta.22
|
||||
resolution: "electron-log@npm:5.0.0-beta.22"
|
||||
checksum: 762bf8524079d6960be9fd186fa8b435e9900c2b8522ba38a760368e710a63f8917daef56ed2524c6678d58b692ae8290761ca00828890cb89c474e8c4a29907
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"electron-packager@npm:^17.1.1":
|
||||
version: 17.1.1
|
||||
resolution: "electron-packager@npm:17.1.1"
|
||||
@@ -12079,7 +12161,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"encoding@npm:^0.1.13":
|
||||
"encoding@npm:^0.1.12, encoding@npm:^0.1.13":
|
||||
version: 0.1.13
|
||||
resolution: "encoding@npm:0.1.13"
|
||||
dependencies:
|
||||
@@ -14705,6 +14787,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http-proxy-agent@npm:^4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "http-proxy-agent@npm:4.0.1"
|
||||
dependencies:
|
||||
"@tootallnate/once": 1
|
||||
agent-base: 6
|
||||
debug: 4
|
||||
checksum: c6a5da5a1929416b6bbdf77b1aca13888013fe7eb9d59fc292e25d18e041bb154a8dfada58e223fc7b76b9b2d155a87e92e608235201f77d34aa258707963a82
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"http-proxy-agent@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "http-proxy-agent@npm:5.0.0"
|
||||
@@ -17292,7 +17385,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"make-dir@npm:^3.0.0, make-dir@npm:^3.0.2":
|
||||
"make-dir@npm:^3.0.0, make-dir@npm:^3.0.2, make-dir@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "make-dir@npm:3.1.0"
|
||||
dependencies:
|
||||
@@ -17332,6 +17425,30 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"make-fetch-happen@npm:^9.1.0":
|
||||
version: 9.1.0
|
||||
resolution: "make-fetch-happen@npm:9.1.0"
|
||||
dependencies:
|
||||
agentkeepalive: ^4.1.3
|
||||
cacache: ^15.2.0
|
||||
http-cache-semantics: ^4.1.0
|
||||
http-proxy-agent: ^4.0.1
|
||||
https-proxy-agent: ^5.0.0
|
||||
is-lambda: ^1.0.1
|
||||
lru-cache: ^6.0.0
|
||||
minipass: ^3.1.3
|
||||
minipass-collect: ^1.0.2
|
||||
minipass-fetch: ^1.3.2
|
||||
minipass-flush: ^1.0.5
|
||||
minipass-pipeline: ^1.2.4
|
||||
negotiator: ^0.6.2
|
||||
promise-retry: ^2.0.1
|
||||
socks-proxy-agent: ^6.0.0
|
||||
ssri: ^8.0.0
|
||||
checksum: 0eb371c85fdd0b1584fcfdf3dc3c62395761b3c14658be02620c310305a9a7ecf1617a5e6fb30c1d081c5c8aaf177fa133ee225024313afabb7aa6a10f1e3d04
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"makeerror@npm:1.0.12":
|
||||
version: 1.0.12
|
||||
resolution: "makeerror@npm:1.0.12"
|
||||
@@ -17667,6 +17784,21 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minipass-fetch@npm:^1.3.2":
|
||||
version: 1.4.1
|
||||
resolution: "minipass-fetch@npm:1.4.1"
|
||||
dependencies:
|
||||
encoding: ^0.1.12
|
||||
minipass: ^3.1.0
|
||||
minipass-sized: ^1.0.3
|
||||
minizlib: ^2.0.0
|
||||
dependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
checksum: ec93697bdb62129c4e6c0104138e681e30efef8c15d9429dd172f776f83898471bc76521b539ff913248cc2aa6d2b37b652c993504a51cc53282563640f29216
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minipass-fetch@npm:^2.0.3":
|
||||
version: 2.1.2
|
||||
resolution: "minipass-fetch@npm:2.1.2"
|
||||
@@ -17691,7 +17823,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minipass-pipeline@npm:^1.2.4":
|
||||
"minipass-pipeline@npm:^1.2.2, minipass-pipeline@npm:^1.2.4":
|
||||
version: 1.2.4
|
||||
resolution: "minipass-pipeline@npm:1.2.4"
|
||||
dependencies:
|
||||
@@ -17709,7 +17841,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minipass@npm:^3.0.0, minipass@npm:^3.1.1, minipass@npm:^3.1.6":
|
||||
"minipass@npm:^3.0.0, minipass@npm:^3.1.0, minipass@npm:^3.1.1, minipass@npm:^3.1.3, minipass@npm:^3.1.6":
|
||||
version: 3.3.6
|
||||
resolution: "minipass@npm:3.3.6"
|
||||
dependencies:
|
||||
@@ -17725,7 +17857,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minizlib@npm:^2.1.1, minizlib@npm:^2.1.2":
|
||||
"minizlib@npm:^2.0.0, minizlib@npm:^2.1.1, minizlib@npm:^2.1.2":
|
||||
version: 2.1.2
|
||||
resolution: "minizlib@npm:2.1.2"
|
||||
dependencies:
|
||||
@@ -17934,7 +18066,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"negotiator@npm:0.6.3, negotiator@npm:^0.6.3":
|
||||
"negotiator@npm:0.6.3, negotiator@npm:^0.6.2, negotiator@npm:^0.6.3":
|
||||
version: 0.6.3
|
||||
resolution: "negotiator@npm:0.6.3"
|
||||
checksum: b8ffeb1e262eff7968fc90a2b6767b04cfd9842582a9d0ece0af7049537266e7b2506dfb1d107a32f06dd849ab2aea834d5830f7f4d0e5cb7d36e1ae55d021d9
|
||||
@@ -18070,6 +18202,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-addon-api@npm:^4.2.0":
|
||||
version: 4.3.0
|
||||
resolution: "node-addon-api@npm:4.3.0"
|
||||
dependencies:
|
||||
node-gyp: latest
|
||||
checksum: 3de396e23cc209f539c704583e8e99c148850226f6e389a641b92e8967953713228109f919765abc1f4355e801e8f41842f96210b8d61c7dcc10a477002dcf00
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-api-version@npm:^0.1.4":
|
||||
version: 0.1.4
|
||||
resolution: "node-api-version@npm:0.1.4"
|
||||
@@ -18141,6 +18282,26 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-gyp@npm:8.x":
|
||||
version: 8.4.1
|
||||
resolution: "node-gyp@npm:8.4.1"
|
||||
dependencies:
|
||||
env-paths: ^2.2.0
|
||||
glob: ^7.1.4
|
||||
graceful-fs: ^4.2.6
|
||||
make-fetch-happen: ^9.1.0
|
||||
nopt: ^5.0.0
|
||||
npmlog: ^6.0.0
|
||||
rimraf: ^3.0.2
|
||||
semver: ^7.3.5
|
||||
tar: ^6.1.2
|
||||
which: ^2.0.2
|
||||
bin:
|
||||
node-gyp: bin/node-gyp.js
|
||||
checksum: 341710b5da39d3660e6a886b37e210d33f8282047405c2e62c277bcc744c7552c5b8b972ebc3a7d5c2813794e60cc48c3ebd142c46d6e0321db4db6c92dd0355
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-gyp@npm:^9.0.0, node-gyp@npm:latest":
|
||||
version: 9.3.1
|
||||
resolution: "node-gyp@npm:9.3.1"
|
||||
@@ -18204,6 +18365,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nopt@npm:^5.0.0":
|
||||
version: 5.0.0
|
||||
resolution: "nopt@npm:5.0.0"
|
||||
dependencies:
|
||||
abbrev: 1
|
||||
bin:
|
||||
nopt: bin/nopt.js
|
||||
checksum: d35fdec187269503843924e0114c0c6533fb54bbf1620d0f28b4b60ba01712d6687f62565c55cc20a504eff0fbe5c63e22340c3fad549ad40469ffb611b04f2f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"nopt@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "nopt@npm:6.0.0"
|
||||
@@ -21057,6 +21229,17 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"socks-proxy-agent@npm:^6.0.0":
|
||||
version: 6.2.1
|
||||
resolution: "socks-proxy-agent@npm:6.2.1"
|
||||
dependencies:
|
||||
agent-base: ^6.0.2
|
||||
debug: ^4.3.3
|
||||
socks: ^2.6.2
|
||||
checksum: 9ca089d489e5ee84af06741135c4b0d2022977dad27ac8d649478a114cdce87849e8d82b7c22b51501a4116e231241592946fc7fae0afc93b65030ee57084f58
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"socks-proxy-agent@npm:^7.0.0":
|
||||
version: 7.0.0
|
||||
resolution: "socks-proxy-agent@npm:7.0.0"
|
||||
@@ -21232,6 +21415,35 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sqlite3@npm:^5.1.6":
|
||||
version: 5.1.6
|
||||
resolution: "sqlite3@npm:5.1.6"
|
||||
dependencies:
|
||||
"@mapbox/node-pre-gyp": ^1.0.0
|
||||
node-addon-api: ^4.2.0
|
||||
node-gyp: 8.x
|
||||
tar: ^6.1.11
|
||||
peerDependencies:
|
||||
node-gyp: 8.x
|
||||
dependenciesMeta:
|
||||
node-gyp:
|
||||
optional: true
|
||||
peerDependenciesMeta:
|
||||
node-gyp:
|
||||
optional: true
|
||||
checksum: ea640628843e37a63dfb4bd2c8429dbd7aab845c1a8204574dca3aac61486ab65bc0abfd99b48f1cead1f783171c6111c0cc4115335d5b95bb0b4eb44db162d5
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ssri@npm:^8.0.0, ssri@npm:^8.0.1":
|
||||
version: 8.0.1
|
||||
resolution: "ssri@npm:8.0.1"
|
||||
dependencies:
|
||||
minipass: ^3.1.1
|
||||
checksum: bc447f5af814fa9713aa201ec2522208ae0f4d8f3bda7a1f445a797c7b929a02720436ff7c478fb5edc4045adb02b1b88d2341b436a80798734e2494f1067b36
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ssri@npm:^9.0.0":
|
||||
version: 9.0.1
|
||||
resolution: "ssri@npm:9.0.1"
|
||||
@@ -21841,7 +22053,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar@npm:^6.0.5, tar@npm:^6.1.11, tar@npm:^6.1.13, tar@npm:^6.1.2":
|
||||
"tar@npm:^6.0.2, tar@npm:^6.0.5, tar@npm:^6.1.11, tar@npm:^6.1.13, tar@npm:^6.1.2":
|
||||
version: 6.1.13
|
||||
resolution: "tar@npm:6.1.13"
|
||||
dependencies:
|
||||
@@ -22616,6 +22828,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unique-filename@npm:^1.1.1":
|
||||
version: 1.1.1
|
||||
resolution: "unique-filename@npm:1.1.1"
|
||||
dependencies:
|
||||
unique-slug: ^2.0.0
|
||||
checksum: cf4998c9228cc7647ba7814e255dec51be43673903897b1786eff2ac2d670f54d4d733357eb08dea969aa5e6875d0e1bd391d668fbdb5a179744e7c7551a6f80
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unique-filename@npm:^2.0.0":
|
||||
version: 2.0.1
|
||||
resolution: "unique-filename@npm:2.0.1"
|
||||
@@ -22625,6 +22846,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unique-slug@npm:^2.0.0":
|
||||
version: 2.0.2
|
||||
resolution: "unique-slug@npm:2.0.2"
|
||||
dependencies:
|
||||
imurmurhash: ^0.1.4
|
||||
checksum: 5b6876a645da08d505dedb970d1571f6cebdf87044cb6b740c8dbb24f0d6e1dc8bdbf46825fd09f994d7cf50760e6f6e063cfa197d51c5902c00a861702eb75a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unique-slug@npm:^3.0.0":
|
||||
version: 3.0.0
|
||||
resolution: "unique-slug@npm:3.0.0"
|
||||
@@ -23900,6 +24130,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yjs@npm:^13.5.53":
|
||||
version: 13.5.53
|
||||
resolution: "yjs@npm:13.5.53"
|
||||
dependencies:
|
||||
lib0: ^0.2.72
|
||||
checksum: be04e185d694c0c9de93d15d710d2789587226928dc2e66638ad8c075d825cced96727b43d5c50800ac4ec16120d08a273fa538116f751d48653365877e54422
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"yn@npm:3.1.1":
|
||||
version: 3.1.1
|
||||
resolution: "yn@npm:3.1.1"
|
||||
|
||||
Reference in New Issue
Block a user