feat: store local data to local db (#2037)

This commit is contained in:
Peng Xiao
2023-04-21 18:06:54 +08:00
committed by GitHub
parent acc5afdd4f
commit 4bb50e8c25
35 changed files with 1103 additions and 167 deletions

View File

@@ -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__'

View File

@@ -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' },
],
},
},
{

View File

@@ -0,0 +1,3 @@
import log from 'electron-log';
export const logger = log;

View File

@@ -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;
});
};

View 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;

View 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);
// });
// }

View 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);
}

View 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);
}

View 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();
};

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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; };
}

View File

@@ -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

View File

@@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

View File

@@ -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,
},

View File

@@ -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'));

View File

@@ -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)),
];
});
});
}
};
/**

View File

@@ -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>
</>

View File

@@ -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 ? (

View File

@@ -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);

View File

@@ -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(() => {

View File

@@ -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: {

View File

@@ -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.",

View File

@@ -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": {
"": "此操作会在你的设备上占用少许空间。"

View File

@@ -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();

View 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);
},
},
};
};

View File

@@ -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;
},
};

View File

@@ -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
});

View File

@@ -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));
};

View File

@@ -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';
}

View File

@@ -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
View File

@@ -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"