refactor: jwt internal version migration

This commit is contained in:
DarkSky
2022-08-23 17:07:16 +08:00
parent eedb4864df
commit 6df2676c88
18 changed files with 274 additions and 145 deletions

View File

@@ -12,7 +12,7 @@
"file-selector": "^0.6.0", "file-selector": "^0.6.0",
"flexsearch": "^0.7.21", "flexsearch": "^0.7.21",
"lib0": "^0.2.52", "lib0": "^0.2.52",
"lru-cache": "^7.13.2", "lru-cache": "^7.14.0",
"ts-debounce": "^4.0.0" "ts-debounce": "^4.0.0"
}, },
"dependencies": { "dependencies": {

View File

@@ -1,3 +1,10 @@
import {
BlockFlavorKeys,
BlockFlavors,
BlockTypeKeys,
BlockTypes,
} from '../types';
import { getLogger } from '../utils';
import { import {
BlockInstance, BlockInstance,
BlockListener, BlockListener,
@@ -6,14 +13,7 @@ import {
ContentTypes, ContentTypes,
HistoryManager, HistoryManager,
MapOperation, MapOperation,
} from '../adapter'; } from '../yjs/types';
import {
BlockFlavorKeys,
BlockFlavors,
BlockTypeKeys,
BlockTypes,
} from '../types';
import { getLogger } from '../utils';
declare const JWT_DEV: boolean; declare const JWT_DEV: boolean;
const logger = getLogger('BlockDB:block'); const logger = getLogger('BlockDB:block');
@@ -29,10 +29,10 @@ export class AbstractBlock<
private readonly _id: string; private readonly _id: string;
private readonly _block: BlockInstance<C>; private readonly _block: BlockInstance<C>;
private readonly _history: HistoryManager; private readonly _history: HistoryManager;
private readonly _root?: AbstractBlock<B, C>; private readonly _root: AbstractBlock<B, C> | undefined;
private readonly _parentListener: Map<string, BlockListener>; private readonly _parentListener: Map<string, BlockListener>;
private _parent?: AbstractBlock<B, C>; private _parent: AbstractBlock<B, C> | undefined;
private _changeParent?: () => void; private _changeParent?: () => void;
constructor( constructor(
@@ -48,7 +48,9 @@ export class AbstractBlock<
this._parentListener = new Map(); this._parentListener = new Map();
JWT_DEV && logger_debug(`init: exists ${this._id}`); JWT_DEV && logger_debug(`init: exists ${this._id}`);
if (parent) this._refreshParent(parent); if (parent) {
this._refreshParent(parent);
}
} }
public get root() { public get root() {
@@ -146,7 +148,7 @@ export class AbstractBlock<
return new Date(timestamp) return new Date(timestamp)
.toISOString() .toISOString()
.split('T')[0] .split('T')[0]
.replace(/-/g, ''); ?.replace(/-/g, '');
} }
// eslint-disable-next-line no-empty // eslint-disable-next-line no-empty
} catch (e) {} } catch (e) {}
@@ -259,7 +261,7 @@ export class AbstractBlock<
} }
/** /**
* Insert sub-Block * Insert children block
* @param block Block instance * @param block Block instance
* @param position Insertion position, if it is empty, it will be inserted at the end. If the block already exists, the position will be moved * @param position Insertion position, if it is empty, it will be inserted at the end. If the block already exists, the position will be moved
* @returns * @returns
@@ -270,9 +272,12 @@ export class AbstractBlock<
) { ) {
JWT_DEV && logger(`insertChildren: start`); JWT_DEV && logger(`insertChildren: start`);
if (block.id === this._id) return; // avoid self-reference if (block.id === this._id) {
// avoid self-reference
return;
}
if ( if (
this.type !== BlockTypes.block || // binary cannot insert subblocks this.type !== BlockTypes.block || // binary cannot insert children blocks
(block.type !== BlockTypes.block && (block.type !== BlockTypes.block &&
this.flavor !== BlockFlavors.workspace) // binary can only be inserted into workspace this.flavor !== BlockFlavors.workspace) // binary can only be inserted into workspace
) { ) {

View File

@@ -1,3 +1,5 @@
import { BlockItem } from '../types';
import { getLogger } from '../utils';
import { import {
ArrayOperation, ArrayOperation,
BlockInstance, BlockInstance,
@@ -5,9 +7,7 @@ import {
ContentTypes, ContentTypes,
InternalPlainObject, InternalPlainObject,
MapOperation, MapOperation,
} from '../adapter'; } from '../yjs/types';
import { BlockItem } from '../types';
import { getLogger } from '../utils';
import { AbstractBlock } from './abstract'; import { AbstractBlock } from './abstract';
import { BlockCapability } from './capability'; import { BlockCapability } from './capability';
@@ -23,8 +23,8 @@ export interface Decoration extends InternalPlainObject {
type Validator = <T>(value: T | undefined) => boolean | void; type Validator = <T>(value: T | undefined) => boolean | void;
export type IndexMetadata = Readonly<{ export type IndexMetadata = Readonly<{
content?: string; content: string | undefined;
reference?: string; reference: string | undefined;
tags: string[]; tags: string[];
}>; }>;
export type QueryMetadata = Readonly< export type QueryMetadata = Readonly<
@@ -51,7 +51,7 @@ export class BaseBlock<
B extends BlockInstance<C>, B extends BlockInstance<C>,
C extends ContentOperation C extends ContentOperation
> extends AbstractBlock<B, C> { > extends AbstractBlock<B, C> {
private readonly _exporters?: Exporters; private readonly _exporters: Exporters | undefined;
private readonly _contentExportersGetter: () => Map< private readonly _contentExportersGetter: () => Map<
string, string,
ReadableContentExporter<string, any> ReadableContentExporter<string, any>
@@ -68,7 +68,7 @@ export class BaseBlock<
ReadableContentExporter<string[], any> ReadableContentExporter<string[], any>
>; >;
validators: Map<string, Validator> = new Map(); private readonly _validators: Map<string, Validator> = new Map();
constructor( constructor(
block: B, block: B,
@@ -157,14 +157,14 @@ export class BaseBlock<
setValidator(key: string, validator?: Validator) { setValidator(key: string, validator?: Validator) {
if (validator) { if (validator) {
this.validators.set(key, validator); this._validators.set(key, validator);
} else { } else {
this.validators.delete(key); this._validators.delete(key);
} }
} }
private validate(key: string, value: unknown): boolean { private validate(key: string, value: unknown): boolean {
const validate = this.validators.get(key); const validate = this._validators.get(key);
if (validate) { if (validate) {
return validate(value) === false ? false : true; return validate(value) === false ? false : true;
} }
@@ -179,7 +179,7 @@ export class BaseBlock<
} }
/** /**
* Get an instance of the child Block * Get an instance of the child block
* @param blockId block id * @param blockId block id
*/ */
private get_children_instance(blockId?: string): BaseBlock<B, C>[] { private get_children_instance(blockId?: string): BaseBlock<B, C>[] {
@@ -201,9 +201,15 @@ export class BaseBlock<
} }
try { try {
const parent_page = this._getParentPage(false); const parent_page = this._getParentPage(false);
if (parent_page) metadata['page'] = parent_page; if (parent_page) {
if (this.group) metadata['group'] = this.group.id; metadata['page'] = parent_page;
if (this.parent) metadata['parent'] = this.parent.id; }
if (this.group) {
metadata['group'] = this.group.id;
}
if (this.parent) {
metadata['parent'] = this.parent.id;
}
} catch (e) { } catch (e) {
logger(`Failed to export default metadata`, e); logger(`Failed to export default metadata`, e);
} }
@@ -228,7 +234,9 @@ export class BaseBlock<
for (const [name, exporter] of this._contentExportersGetter()) { for (const [name, exporter] of this._contentExportersGetter()) {
try { try {
const content = exporter(this.getContent()); const content = exporter(this.getContent());
if (content) contents.push(content); if (content) {
contents.push(content);
}
} catch (err) { } catch (err) {
logger(`Failed to export content: ${name}`, err); logger(`Failed to export content: ${name}`, err);
} }

View File

@@ -7,14 +7,14 @@ import produce from 'immer';
import LRUCache from 'lru-cache'; import LRUCache from 'lru-cache';
import sift, { Query } from 'sift'; import sift, { Query } from 'sift';
import { BlockFlavors } from '../types';
import { BlockEventBus, getLogger } from '../utils';
import { import {
AsyncDatabaseAdapter, AsyncDatabaseAdapter,
BlockInstance, BlockInstance,
ChangedStates, ChangedStates,
ContentOperation, ContentOperation,
} from '../adapter'; } from '../yjs/types';
import { BlockFlavors } from '../types';
import { BlockEventBus, getLogger } from '../utils';
import { BaseBlock, IndexMetadata, QueryMetadata } from './base'; import { BaseBlock, IndexMetadata, QueryMetadata } from './base';
@@ -315,7 +315,9 @@ export class BlockIndexer<
private _testMetaKey(key: string) { private _testMetaKey(key: string) {
try { try {
const metadata = this._blockMetadata.values().next().value; const metadata = this._blockMetadata.values().next().value;
if (!metadata || typeof metadata !== 'object') return false; if (!metadata || typeof metadata !== 'object') {
return false;
}
return !!(key in metadata); return !!(key in metadata);
} catch (e) { } catch (e) {
return false; return false;
@@ -324,8 +326,11 @@ export class BlockIndexer<
private _getSortedMetadata(sort: string, desc?: boolean) { private _getSortedMetadata(sort: string, desc?: boolean) {
const sorter = naturalSort(Array.from(this._blockMetadata.entries())); const sorter = naturalSort(Array.from(this._blockMetadata.entries()));
if (desc) return sorter.desc(([, m]) => m[sort]); if (desc) {
else return sorter.asc(([, m]) => m[sort]); return sorter.desc(([, m]) => m[sort]);
} else {
return sorter.asc(([, m]) => m[sort]);
}
} }
public query(query: QueryIndexMetadata) { public query(query: QueryIndexMetadata) {
@@ -337,15 +342,23 @@ export class BlockIndexer<
if ($sort && this._testMetaKey($sort)) { if ($sort && this._testMetaKey($sort)) {
const metadata = this._getSortedMetadata($sort, $desc); const metadata = this._getSortedMetadata($sort, $desc);
metadata.forEach(([key, value]) => { metadata.forEach(([key, value]) => {
if (matches.length > limit) return; if (matches.length > limit) {
if (filter(value)) matches.push(key); return;
}
if (filter(value)) {
matches.push(key);
}
}); });
return matches; return matches;
} else { } else {
this._blockMetadata.forEach((value, key) => { this._blockMetadata.forEach((value, key) => {
if (matches.length > limit) return; if (matches.length > limit) {
if (filter(value)) matches.push(key); return;
}
if (filter(value)) {
matches.push(key);
}
}); });
return matches; return matches;

View File

@@ -1,26 +1,7 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import { DocumentSearchOptions } from 'flexsearch'; import { DocumentSearchOptions } from 'flexsearch';
import LRUCache from 'lru-cache'; import LRUCache from 'lru-cache';
import {
AsyncDatabaseAdapter,
BlockInstance,
BlockListener,
ChangedStates,
Connectivity,
ContentOperation,
ContentTypes,
DataExporter,
getDataExporter,
HistoryManager,
YjsAdapter,
YjsContentOperation,
YjsInitOptions,
} from './adapter';
import {
getYjsProviders,
YjsBlockInstance,
YjsProviderOptions,
} from './adapter/yjs';
import { import {
BaseBlock, BaseBlock,
BlockIndexer, BlockIndexer,
@@ -39,6 +20,26 @@ import {
UUID, UUID,
} from './types'; } from './types';
import { BlockEventBus, genUUID, getLogger } from './utils'; import { BlockEventBus, genUUID, getLogger } from './utils';
import {
getYjsProviders,
YjsAdapter,
YjsBlockInstance,
YjsContentOperation,
YjsInitOptions,
YjsProviderOptions,
} from './yjs';
import {
AsyncDatabaseAdapter,
BlockInstance,
BlockListener,
ChangedStates,
Connectivity,
ContentOperation,
ContentTypes,
DataExporter,
getDataExporter,
HistoryManager,
} from './yjs/types';
declare const JWT_DEV: boolean; declare const JWT_DEV: boolean;
@@ -157,8 +158,11 @@ export class BlockClient<
public addBlockListener(tag: string, listener: BlockListener) { public addBlockListener(tag: string, listener: BlockListener) {
const bus = this._eventBus.topic<ChangedStates>('updated'); const bus = this._eventBus.topic<ChangedStates>('updated');
if (tag !== 'index' || !bus.has(tag)) bus.on(tag, listener); if (tag !== 'index' || !bus.has(tag)) {
else console.error(`block listener ${tag} is reserved or exists`); bus.on(tag, listener);
} else {
console.error(`block listener ${tag} is reserved or exists`);
}
} }
public removeBlockListener(tag: string) { public removeBlockListener(tag: string) {
@@ -170,8 +174,11 @@ export class BlockClient<
listener: BlockListener<Set<string>> listener: BlockListener<Set<string>>
) { ) {
const bus = this._eventBus.topic<ChangedStates<Set<string>>>('editing'); const bus = this._eventBus.topic<ChangedStates<Set<string>>>('editing');
if (tag !== 'index' || !bus.has(tag)) bus.on(tag, listener); if (tag !== 'index' || !bus.has(tag)) {
else console.error(`editing listener ${tag} is reserved or exists`); bus.on(tag, listener);
} else {
console.error(`editing listener ${tag} is reserved or exists`);
}
} }
public removeEditingListener(tag: string) { public removeEditingListener(tag: string) {
@@ -184,15 +191,18 @@ export class BlockClient<
) { ) {
const bus = const bus =
this._eventBus.topic<ChangedStates<Connectivity>>('connectivity'); this._eventBus.topic<ChangedStates<Connectivity>>('connectivity');
if (tag !== 'index' || !bus.has(tag)) bus.on(tag, listener); if (tag !== 'index' || !bus.has(tag)) {
else bus.on(tag, listener);
} else {
console.error(`connectivity listener ${tag} is reserved or exists`); console.error(`connectivity listener ${tag} is reserved or exists`);
}
} }
public removeConnectivityListener(tag: string) { public removeConnectivityListener(tag: string) {
this._eventBus.topic('connectivity').off(tag); this._eventBus.topic('connectivity').off(tag);
} }
// @ts-ignore
private inspector() { private inspector() {
return { return {
...this._adapter.inspector(), ...this._adapter.inspector(),
@@ -220,7 +230,9 @@ export class BlockClient<
this.addBlockListener('index', async states => { this.addBlockListener('index', async states => {
await Promise.allSettled( await Promise.allSettled(
Array.from(states.entries()).map(([id, state]) => { Array.from(states.entries()).map(([id, state]) => {
if (state === 'delete') this._blockCaches.delete(id); if (state === 'delete') {
this._blockCaches.delete(id);
}
return this._blockIndexer.refreshIndex(id, state); return this._blockIndexer.refreshIndex(id, state);
}) })
); );
@@ -255,7 +267,7 @@ export class BlockClient<
} }
/** /**
* research all * Full text search
* @param part_of_title_or_content Title or content keyword, support Chinese * @param part_of_title_or_content Title or content keyword, support Chinese
* @param part_of_title_or_content.index search range, optional values: title, ttl, content, reference * @param part_of_title_or_content.index search range, optional values: title, ttl, content, reference
* @param part_of_title_or_content.tag tag, string or array of strings, supports multiple tags * @param part_of_title_or_content.tag tag, string or array of strings, supports multiple tags
@@ -295,7 +307,9 @@ export class BlockClient<
this.search(part_of_title_or_content).flatMap(({ result }) => this.search(part_of_title_or_content).flatMap(({ result }) =>
result.map(async id => { result.map(async id => {
const page = this._pageMapping.get(id as string); const page = this._pageMapping.get(id as string);
if (page) return page; if (page) {
return page;
}
const block = await this.get(id as BlockTypeKeys); const block = await this.get(id as BlockTypeKeys);
return this.set_page(block); return this.set_page(block);
}) })
@@ -307,9 +321,10 @@ export class BlockClient<
} }
return Promise.all( return Promise.all(
this._blockIndexer.getMetadata(pages).map(async page => ({ this._blockIndexer.getMetadata(pages).map(async page => ({
content: this.get_decoded_content( content:
await this._adapter.getBlock(page.id) this.get_decoded_content(
), await this._adapter.getBlock(page.id)
) || '',
...page, ...page,
})) }))
); );
@@ -327,9 +342,10 @@ export class BlockClient<
const ids = this.query(query); const ids = this.query(query);
return Promise.all( return Promise.all(
this._blockIndexer.getMetadata(ids).map(async page => ({ this._blockIndexer.getMetadata(ids).map(async page => ({
content: this.get_decoded_content( content:
await this._adapter.getBlock(page.id) this.get_decoded_content(
), await this._adapter.getBlock(page.id)
) || '',
...page, ...page,
})) }))
); );
@@ -378,7 +394,7 @@ export class BlockClient<
private async get_parent(id: string) { private async get_parent(id: string) {
const parents = this._parentMapping.get(id); const parents = this._parentMapping.get(id);
if (parents) { if (parents && parents[0]) {
const parent_block_id = parents[0]; const parent_block_id = parents[0];
if (!this._blockCaches.has(parent_block_id)) { if (!this._blockCaches.has(parent_block_id)) {
this._blockCaches.set( this._blockCaches.set(
@@ -405,7 +421,9 @@ export class BlockClient<
private set_page(block: BaseBlock<B, C>) { private set_page(block: BaseBlock<B, C>) {
const page = this._pageMapping.get(block.id); const page = this._pageMapping.get(block.id);
if (page) return page; if (page) {
return page;
}
const parent_page = block.parent_page; const parent_page = block.parent_page;
if (parent_page) { if (parent_page) {
this._pageMapping.set(block.id, parent_page); this._pageMapping.set(block.id, parent_page);
@@ -472,8 +490,9 @@ export class BlockClient<
matched += 1; matched += 1;
} }
} }
if (matched === conditions.length) if (matched === conditions.length) {
exporters.push([name, exporter] as const); exporters.push([name, exporter] as const);
}
} }
return exporters; return exporters;
@@ -487,7 +506,9 @@ export class BlockClient<
); );
if (exporter) { if (exporter) {
const op = block.content.asMap(); const op = block.content.asMap();
if (op) return exporter[1](op); if (op) {
return exporter[1](op);
}
} }
} }
return undefined; return undefined;
@@ -566,7 +587,9 @@ export class BlockClient<
this.set_page(abstract_block); this.set_page(abstract_block);
abstract_block.on('parent', 'client_hook', state => { abstract_block.on('parent', 'client_hook', state => {
const [parent] = state.keys(); const [parent] = state.keys();
this.set_parent(parent, abstract_block.id); if (parent) {
this.set_parent(parent, abstract_block.id);
}
this.set_page(abstract_block); this.set_page(abstract_block);
}); });
this._blockCaches.set(abstract_block.id, abstract_block); this._blockCaches.set(abstract_block.id, abstract_block);
@@ -650,13 +673,6 @@ export type BlockInitOptions = NonNullable<
Parameters<typeof BlockClient.init>[1] Parameters<typeof BlockClient.init>[1]
>; >;
export type {
ArrayOperation,
ChangedStates,
Connectivity,
MapOperation,
TextOperation,
} from './adapter';
export type { export type {
BlockSearchItem, BlockSearchItem,
Decoration as BlockDecoration, Decoration as BlockDecoration,
@@ -665,4 +681,11 @@ export type {
export { BlockTypes, BucketBackend as BlockBackend } from './types'; export { BlockTypes, BucketBackend as BlockBackend } from './types';
export type { BlockTypeKeys } from './types'; export type { BlockTypeKeys } from './types';
export { isBlock } from './utils'; export { isBlock } from './utils';
export type {
ArrayOperation,
ChangedStates,
Connectivity,
MapOperation,
TextOperation,
} from './yjs/types';
export type { QueryIndexMetadata }; export type { QueryIndexMetadata };

View File

@@ -1,4 +1,4 @@
import { ContentOperation } from '../adapter'; import { ContentOperation } from '../yjs/types';
import { RefMetadata } from './metadata'; import { RefMetadata } from './metadata';
import { UUID } from './uuid'; import { UUID } from './uuid';
// base type of block // base type of block
@@ -60,8 +60,8 @@ export type BlockItem<C extends ContentOperation> = {
flavor: typeof BlockFlavors[BlockFlavorKeys]; flavor: typeof BlockFlavors[BlockFlavorKeys];
children: string[]; children: string[];
readonly created: number; // creation time, UTC timestamp readonly created: number; // creation time, UTC timestamp
readonly updated?: number; // update time, UTC timestamp readonly updated?: number | undefined; // update time, UTC timestamp
readonly creator?: string; // creator id readonly creator?: string | undefined; // creator id
content: C; // Essentially what is stored here is either Uint8Array (binary resource) or YDoc (structured resource) content: C; // Essentially what is stored here is either Uint8Array (binary resource) or YDoc (structured resource)
}; };

View File

@@ -81,7 +81,7 @@ class BlockScopedEventBus<T> extends BlockEventBus {
options?: ListenerOptions options?: ListenerOptions
) { ) {
if (options?.debounce) { if (options?.debounce) {
const { wait, maxWait } = options.debounce; const { wait, maxWait = 500 } = options.debounce;
const debounced = debounce(listener, wait, { maxWait }); const debounced = debounce(listener, wait, { maxWait });
this.on_listener(this._topic, name, e => { this.on_listener(this._topic, name, e => {
debounced((e as CustomEvent)?.detail); debounced((e as CustomEvent)?.detail);

View File

@@ -39,7 +39,9 @@ export function getLogger(namespace: string) {
if (JWT_DEV) { if (JWT_DEV) {
const logger = debug(namespace); const logger = debug(namespace);
logger.log = console.log.bind(console); logger.log = console.log.bind(console);
if (JWT_DEV === ('testing' as any)) logger.enabled = true; if (JWT_DEV === ('testing' as any)) {
logger.enabled = true;
}
return logger; return logger;
} else { } else {
return () => {}; return () => {};

View File

@@ -25,7 +25,7 @@ export class YjsRemoteBinaries {
} else { } else {
// TODO: Remote Load // TODO: Remote Load
try { try {
const file = await this._remoteStorage?.instance.getBuffData( const file = await this._remoteStorage?.instance?.getBuffData(
name name
); );
console.log(file); console.log(file);
@@ -44,11 +44,11 @@ export class YjsRemoteBinaries {
this._binaries.set(name, binary); this._binaries.set(name, binary);
if (this._remoteStorage) { if (this._remoteStorage) {
// TODO: Remote Save, if there is an object with the same name remotely, the upload is skipped, because the file name is the hash of the file content // TODO: Remote Save, if there is an object with the same name remotely, the upload is skipped, because the file name is the hash of the file content
const has_file = this._remoteStorage.instance.exist(name); const has_file = this._remoteStorage.instance?.exist(name);
if (!has_file) { if (!has_file) {
const upload_file = new File(binary.toArray(), name); const upload_file = new File(binary.toArray(), name);
await this._remoteStorage.instance await this._remoteStorage.instance
.upload(upload_file) ?.upload(upload_file)
.catch(err => { .catch(err => {
throw new Error(`${err} upload error`); throw new Error(`${err} upload error`);
}); });

View File

@@ -5,8 +5,8 @@ import {
transact, transact,
} from 'yjs'; } from 'yjs';
import { BlockItem, BlockTypes } from '../../types'; import { BlockItem, BlockTypes } from '../types';
import { BlockInstance, BlockListener, HistoryManager } from '../index'; import { BlockInstance, BlockListener, HistoryManager } from './types';
import { YjsHistoryManager } from './history'; import { YjsHistoryManager } from './history';
import { ChildrenListenerHandler, ContentListenerHandler } from './listener'; import { ChildrenListenerHandler, ContentListenerHandler } from './listener';
@@ -34,7 +34,7 @@ type YjsBlockInstanceProps = {
export class YjsBlockInstance implements BlockInstance<YjsContentOperation> { export class YjsBlockInstance implements BlockInstance<YjsContentOperation> {
private readonly _id: string; private readonly _id: string;
private readonly _block: YMap<unknown>; private readonly _block: YMap<unknown>;
private readonly _binary?: YArray<ArrayBuffer>; private readonly _binary: YArray<ArrayBuffer> | undefined;
private readonly _children: YArray<string>; private readonly _children: YArray<string>;
private readonly _setBlock: ( private readonly _setBlock: (
id: string, id: string,
@@ -48,8 +48,7 @@ export class YjsBlockInstance implements BlockInstance<YjsContentOperation> {
private readonly _childrenListeners: Map<string, BlockListener>; private readonly _childrenListeners: Map<string, BlockListener>;
private readonly _contentListeners: Map<string, BlockListener>; private readonly _contentListeners: Map<string, BlockListener>;
// eslint-disable-next-line @typescript-eslint/naming-convention private _childrenMap: Map<string, number>;
_childrenMap: Map<string, number>;
constructor(props: YjsBlockInstanceProps) { constructor(props: YjsBlockInstanceProps) {
this._id = props.id; this._id = props.id;
@@ -184,7 +183,9 @@ export class YjsBlockInstance implements BlockInstance<YjsContentOperation> {
} }
hasChildren(id: string): boolean { hasChildren(id: string): boolean {
if (this.children.includes(id)) return true; if (this.children.includes(id)) {
return true;
}
return this.getChildren().some(block => block.hasChildren(id)); return this.getChildren().some(block => block.hasChildren(id));
} }
@@ -263,7 +264,9 @@ export class YjsBlockInstance implements BlockInstance<YjsContentOperation> {
break; break;
} }
} }
if (id) failed.push(id); if (id) {
failed.push(id);
}
} }
this._childrenMap = getMapFromYArray(this._children); this._childrenMap = getMapFromYArray(this._children);

View File

@@ -32,7 +32,7 @@ export class GateKeeper {
return creator === this._userId || !!this._common.get(block_id); return creator === this._userId || !!this._common.get(block_id);
} }
checkDeleteLists(block_ids: string[]) { checkDeleteLists(block_ids: string[]): [string[], string[]] {
const success = []; const success = [];
const fail = []; const fail = [];
for (const block_id of block_ids) { for (const block_id of block_ids) {

View File

@@ -1,10 +1,11 @@
import { Map as YMap, UndoManager } from 'yjs'; import { Map as YMap, UndoManager } from 'yjs';
import { HistoryCallback, HistoryManager } from '../../adapter'; import { HistoryCallback, HistoryManager } from './types';
type StackItem = UndoManager['undoStack'][0]; type StackItem = UndoManager['undoStack'][0];
export class YjsHistoryManager implements HistoryManager { export class YjsHistoryManager implements HistoryManager {
// @ts-ignore
private readonly _blocks: YMap<any>; private readonly _blocks: YMap<any>;
private readonly _historyManager: UndoManager; private readonly _historyManager: UndoManager;
private readonly _pushListeners: Map<string, HistoryCallback<any>>; private readonly _pushListeners: Map<string, HistoryCallback<any>>;
@@ -56,7 +57,7 @@ export class YjsHistoryManager implements HistoryManager {
} }
break(): void { break(): void {
// this.#history_manager. // this._historyManager.
} }
undo<T = unknown>(): Map<string, T> | undefined { undo<T = unknown>(): Map<string, T> | undefined {

View File

@@ -18,15 +18,15 @@ import {
transact, transact,
} from 'yjs'; } from 'yjs';
import { BlockItem, BlockTypes } from '../types';
import { getLogger, sha3, sleep } from '../utils';
import { import {
AsyncDatabaseAdapter, AsyncDatabaseAdapter,
BlockListener, BlockListener,
ChangedStateKeys, ChangedStateKeys,
Connectivity, Connectivity,
HistoryManager, HistoryManager,
} from '../../adapter'; } from './types';
import { BlockItem, BlockTypes } from '../../types';
import { getLogger, sha3, sleep } from '../../utils';
import { YjsRemoteBinaries } from './binary'; import { YjsRemoteBinaries } from './binary';
import { YjsBlockInstance } from './block'; import { YjsBlockInstance } from './block';
@@ -40,6 +40,7 @@ import {
import { YjsProvider } from './provider'; import { YjsProvider } from './provider';
declare const JWT_DEV: boolean; declare const JWT_DEV: boolean;
// @ts-ignore
const logger = getLogger('BlockDB:yjs'); const logger = getLogger('BlockDB:yjs');
type ConnectivityListener = ( type ConnectivityListener = (
@@ -54,7 +55,7 @@ type YjsProviders = {
gatekeeper: GateKeeper; gatekeeper: GateKeeper;
connListener: { listeners?: ConnectivityListener }; connListener: { listeners?: ConnectivityListener };
userId: string; userId: string;
remoteToken?: string; // remote storage token remoteToken: string | undefined; // remote storage token
}; };
const _yjsDatabaseInstance = new Map<string, YjsProviders>(); const _yjsDatabaseInstance = new Map<string, YjsProviders>();
@@ -70,8 +71,8 @@ async function _initYjsDatabase(
workspace: string, workspace: string,
options: { options: {
userId: string; userId: string;
token?: string; token?: string | undefined;
provider?: Record<string, YjsProvider>; provider?: Record<string, YjsProvider> | undefined;
} }
): Promise<YjsProviders> { ): Promise<YjsProviders> {
if (_asyncInitLoading.has(workspace)) { if (_asyncInitLoading.has(workspace)) {
@@ -243,9 +244,10 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
typeof updated === 'number' && typeof updated === 'number' &&
updated + 1000 * 10 > Date.now() updated + 1000 * 10 > Date.now()
) { ) {
if (!editing_mapping[editing]) if (!editing_mapping[editing]) {
editing_mapping[editing] = []; editing_mapping[editing] = [];
editing_mapping[editing].push(userId); }
editing_mapping[editing]?.push(userId);
} }
} }
listener( listener(
@@ -338,7 +340,7 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
], ],
}); });
const [file] = (await fromEvent(handles)) as File[]; const [file] = (await fromEvent(handles)) as File[];
const binary = await file.arrayBuffer(); const binary = await file?.arrayBuffer();
// await this._provider.idb.clearData(); // await this._provider.idb.clearData();
const doc = new Doc({ autoLoad: true, shouldLoad: true }); const doc = new Doc({ autoLoad: true, shouldLoad: true });
let updated = 0; let updated = 0;
@@ -348,7 +350,9 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
updated += 1; updated += 1;
}); });
setInterval(() => { setInterval(() => {
if (updated > 0) updated -= 1; if (updated > 0) {
updated -= 1;
}
}, 500); }, 500);
const update_check = new Promise<void>(resolve => { const update_check = new Promise<void>(resolve => {
@@ -362,8 +366,10 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
}); });
// await new IndexedDBProvider(this._provider.idb.name, doc) // await new IndexedDBProvider(this._provider.idb.name, doc)
// .whenSynced; // .whenSynced;
applyUpdate(doc, new Uint8Array(binary)); if (binary) {
await update_check; applyUpdate(doc, new Uint8Array(binary));
await update_check;
}
console.log('load success'); console.log('load success');
}, },
parse: () => this._doc.toJSON(), parse: () => this._doc.toJSON(),
@@ -409,8 +415,8 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
async createBlock( async createBlock(
options: Pick<BlockItem<YjsContentOperation>, 'type' | 'flavor'> & { options: Pick<BlockItem<YjsContentOperation>, 'type' | 'flavor'> & {
uuid?: string; uuid: string | undefined;
binary?: ArrayBufferLike; binary: ArrayBufferLike | undefined;
} }
): Promise<YjsBlockInstance> { ): Promise<YjsBlockInstance> {
const uuid = options.uuid || `affine${nanoid(16)}`; const uuid = options.uuid || `affine${nanoid(16)}`;
@@ -481,7 +487,9 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
async getBlock(id: string): Promise<YjsBlockInstance | undefined> { async getBlock(id: string): Promise<YjsBlockInstance | undefined> {
const block_instance = this.get_block_sync(id); const block_instance = this.get_block_sync(id);
if (block_instance) return block_instance; if (block_instance) {
return block_instance;
}
const block = this._blocks.get(id); const block = this._blocks.get(id);
if (block && block.get('type') === BlockTypes.binary) { if (block && block.get('type') === BlockTypes.binary) {
const binary = await this._binaries.get( const binary = await this._binaries.get(
@@ -540,7 +548,9 @@ export class YjsAdapter implements AsyncDatabaseAdapter<YjsContentOperation> {
let uploaded: Promise<void> | undefined; let uploaded: Promise<void> | undefined;
if (!block.size) { if (!block.size) {
const content = item.content[INTO_INNER](); const content = item.content[INTO_INNER]();
if (!content) return reject(); if (!content) {
return reject();
}
const children = new YArray(); const children = new YArray();
children.push(item.children); children.push(item.children);

View File

@@ -1,7 +1,8 @@
import { produce } from 'immer'; import { produce } from 'immer';
import { debounce } from 'ts-debounce'; import { debounce } from 'ts-debounce';
import { YEvent } from 'yjs'; import { YEvent } from 'yjs';
import { BlockListener, ChangedStateKeys } from '../index';
import { BlockListener, ChangedStateKeys } from './types';
let listener_suspend = false; let listener_suspend = false;

View File

@@ -6,6 +6,7 @@ import {
Text as YText, Text as YText,
} from 'yjs'; } from 'yjs';
import { ChildrenListenerHandler, ContentListenerHandler } from './listener';
import { import {
ArrayOperation, ArrayOperation,
BaseTypes, BaseTypes,
@@ -16,21 +17,29 @@ import {
Operable, Operable,
TextOperation, TextOperation,
TextToken, TextToken,
} from '../index'; } from './types';
import { ChildrenListenerHandler, ContentListenerHandler } from './listener';
const INTO_INNER = Symbol('INTO_INNER'); const INTO_INNER = Symbol('INTO_INNER');
export const DO_NOT_USE_THIS_OR_YOU_WILL_BE_FIRED_SYMBOL_INTO_INNER: typeof INTO_INNER = export const DO_NOT_USE_THIS_OR_YOU_WILL_BE_FIRED_SYMBOL_INTO_INNER: typeof INTO_INNER =
INTO_INNER; INTO_INNER;
function auto_get(root: ContentOperation, key: string): unknown | undefined { function auto_get(
root: ContentOperation,
key: string | undefined
): unknown | undefined {
const array = root.asArray(); const array = root.asArray();
if (array && !Number.isNaN(Number(key))) return array.get(Number(key)); if (array && !Number.isNaN(Number(key))) {
return array.get(Number(key));
}
const map = root.asMap(); const map = root.asMap();
if (map) return map.get(key); if (map && key) {
return map.get(key);
}
const text = root.asText(); const text = root.asText();
if (text) return text.toString(); if (text) {
return text.toString();
}
console.error('auto_get unknown root', root, key); console.error('auto_get unknown root', root, key);
return undefined; return undefined;
} }
@@ -150,7 +159,9 @@ export class YjsContentOperation implements ContentOperation {
if (root instanceof YjsContentOperation) { if (root instanceof YjsContentOperation) {
if (path.length === 1) { if (path.length === 1) {
const [key] = path; const [key] = path;
if (key) return auto_set(root, key, data); if (key) {
return auto_set(root, key, data);
}
console.error('autoSet unknown path', root, path, data); console.error('autoSet unknown path', root, path, data);
return; return;
} }
@@ -191,6 +202,7 @@ export class YjsContentOperation implements ContentOperation {
} }
// eslint-disable-next-line @typescript-eslint/naming-convention // eslint-disable-next-line @typescript-eslint/naming-convention
// @ts-ignore
private toJSON() { private toJSON() {
return this._content.toJSON(); return this._content.toJSON();
} }
@@ -283,7 +295,9 @@ class YjsArrayOperation<T extends ContentTypes>
get(index: number): Operable<T> | undefined { get(index: number): Operable<T> | undefined {
const content = this._arrayContent.get(index); const content = this._arrayContent.get(index);
if (content) return this.to_operable(content); if (content) {
return this.to_operable(content);
}
return undefined; return undefined;
} }
@@ -368,7 +382,9 @@ class YjsMapOperation<T extends ContentTypes>
set(key: string, value: Operable<T>): void { set(key: string, value: Operable<T>): void {
if (value instanceof YjsContentOperation) { if (value instanceof YjsContentOperation) {
const content = value[INTO_INNER](); const content = value[INTO_INNER]();
if (content) this._mapContent.set(key, content as unknown as T); if (content) {
this._mapContent.set(key, content as unknown as T);
}
} else { } else {
this._mapContent.set(key, value as T); this._mapContent.set(key, value as T);
} }
@@ -376,7 +392,9 @@ class YjsMapOperation<T extends ContentTypes>
get(key: string): Operable<T> | undefined { get(key: string): Operable<T> | undefined {
const content = this._mapContent.get(key); const content = this._mapContent.get(key);
if (content) return this.to_operable(content); if (content) {
return this.to_operable(content);
}
return undefined; return undefined;
} }

View File

@@ -7,13 +7,13 @@ import {
WebsocketProvider, WebsocketProvider,
} from '@toeverything/datasource/jwt-rpc'; } from '@toeverything/datasource/jwt-rpc';
import { Connectivity } from '../../adapter'; import { BucketBackend } from '../types';
import { BucketBackend } from '../../types'; import { Connectivity } from './types';
type YjsDefaultInstances = { type YjsDefaultInstances = {
awareness: Awareness; awareness: Awareness;
doc: Doc; doc: Doc;
token?: string; token?: string | undefined;
workspace: string; workspace: string;
emitState: (connectivity: Connectivity) => void; emitState: (connectivity: Connectivity) => void;
}; };

View File

@@ -139,8 +139,8 @@ interface AsyncDatabaseAdapter<C extends ContentOperation> {
reload(): void; reload(): void;
createBlock( createBlock(
options: Pick<BlockItem<C>, 'type' | 'flavor'> & { options: Pick<BlockItem<C>, 'type' | 'flavor'> & {
binary?: ArrayBuffer; binary: ArrayBuffer | undefined;
uuid?: string; uuid: string | undefined;
} }
): Promise<BlockInstance<C>>; ): Promise<BlockInstance<C>>;
getBlock(id: string): Promise<BlockInstance<C> | undefined>; getBlock(id: string): Promise<BlockInstance<C> | undefined>;
@@ -184,8 +184,6 @@ export const getDataExporter = () => {
return { importData, exportData, hasExporter, installExporter }; return { importData, exportData, hasExporter, installExporter };
}; };
export { YjsAdapter } from './yjs';
export type { YjsContentOperation, YjsInitOptions } from './yjs';
export type { export type {
AsyncDatabaseAdapter, AsyncDatabaseAdapter,
BlockPosition, BlockPosition,

57
pnpm-lock.yaml generated
View File

@@ -184,7 +184,7 @@ importers:
yjs: ^13.5.41 yjs: ^13.5.41
dependencies: dependencies:
authing-js-sdk: 4.23.35 authing-js-sdk: 4.23.35
firebase-admin: 11.0.1_@firebase+app-types@0.7.0 firebase-admin: 11.0.1
lib0: 0.2.52 lib0: 0.2.52
lru-cache: 7.13.2 lru-cache: 7.13.2
nanoid: 4.0.0 nanoid: 4.0.0
@@ -566,6 +566,9 @@ importers:
dependencies: dependencies:
ffc-js-client-side-sdk: 1.1.5 ffc-js-client-side-sdk: 1.1.5
libs/datasource/jwst/pkg:
specifiers: {}
libs/datasource/jwt: libs/datasource/jwt:
specifiers: specifiers:
'@types/debug': ^4.1.7 '@types/debug': ^4.1.7
@@ -583,7 +586,7 @@ importers:
idb-keyval: ^6.2.0 idb-keyval: ^6.2.0
immer: ^9.0.15 immer: ^9.0.15
lib0: ^0.2.52 lib0: ^0.2.52
lru-cache: ^7.13.2 lru-cache: ^7.14.0
nanoid: ^4.0.0 nanoid: ^4.0.0
sha3: ^2.1.4 sha3: ^2.1.4
sift: ^16.0.0 sift: ^16.0.0
@@ -614,7 +617,7 @@ importers:
file-selector: 0.6.0 file-selector: 0.6.0
flexsearch: 0.7.21 flexsearch: 0.7.21
lib0: 0.2.52 lib0: 0.2.52
lru-cache: 7.13.2 lru-cache: 7.14.0
ts-debounce: 4.0.0 ts-debounce: 4.0.0
libs/datasource/jwt-rpc: libs/datasource/jwt-rpc:
@@ -3180,6 +3183,15 @@ packages:
- utf-8-validate - utf-8-validate
dev: true dev: true
/@firebase/auth-interop-types/0.1.6_@firebase+util@1.6.3:
resolution: {integrity: sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==}
peerDependencies:
'@firebase/app-types': 0.x
'@firebase/util': 1.x
dependencies:
'@firebase/util': 1.6.3
dev: false
/@firebase/auth-interop-types/0.1.6_pbfwexsq7uf6mrzcwnikj3g37m: /@firebase/auth-interop-types/0.1.6_pbfwexsq7uf6mrzcwnikj3g37m:
resolution: {integrity: sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==} resolution: {integrity: sha512-etIi92fW3CctsmR9e3sYM3Uqnoq861M0Id9mdOPF6PWIg38BXL5k4upCNBggGUpLIS0H1grMOvy/wn1xymwe2g==}
peerDependencies: peerDependencies:
@@ -3188,6 +3200,7 @@ packages:
dependencies: dependencies:
'@firebase/app-types': 0.7.0 '@firebase/app-types': 0.7.0
'@firebase/util': 1.6.3 '@firebase/util': 1.6.3
dev: true
/@firebase/auth-types/0.11.0_pbfwexsq7uf6mrzcwnikj3g37m: /@firebase/auth-types/0.11.0_pbfwexsq7uf6mrzcwnikj3g37m:
resolution: {integrity: sha512-q7Bt6cx+ySj9elQHTsKulwk3+qDezhzRBFC9zlQ1BjgMueUOnGMcvqmU0zuKlQ4RhLSH7MNAdBV2znVaoN3Vxw==} resolution: {integrity: sha512-q7Bt6cx+ySj9elQHTsKulwk3+qDezhzRBFC9zlQ1BjgMueUOnGMcvqmU0zuKlQ4RhLSH7MNAdBV2znVaoN3Vxw==}
@@ -3223,6 +3236,19 @@ packages:
'@firebase/util': 1.6.3 '@firebase/util': 1.6.3
tslib: 2.4.0 tslib: 2.4.0
/@firebase/database-compat/0.2.4:
resolution: {integrity: sha512-VtsGixO5mTjNMJn6PwxAJEAR70fj+3blCXIdQKel3q+eYGZAfdqxox1+tzZDnf9NWBJpaOgAHPk3JVDxEo9NFQ==}
dependencies:
'@firebase/component': 0.5.17
'@firebase/database': 0.13.4
'@firebase/database-types': 0.9.12
'@firebase/logger': 0.3.3
'@firebase/util': 1.6.3
tslib: 2.4.0
transitivePeerDependencies:
- '@firebase/app-types'
dev: false
/@firebase/database-compat/0.2.4_@firebase+app-types@0.7.0: /@firebase/database-compat/0.2.4_@firebase+app-types@0.7.0:
resolution: {integrity: sha512-VtsGixO5mTjNMJn6PwxAJEAR70fj+3blCXIdQKel3q+eYGZAfdqxox1+tzZDnf9NWBJpaOgAHPk3JVDxEo9NFQ==} resolution: {integrity: sha512-VtsGixO5mTjNMJn6PwxAJEAR70fj+3blCXIdQKel3q+eYGZAfdqxox1+tzZDnf9NWBJpaOgAHPk3JVDxEo9NFQ==}
dependencies: dependencies:
@@ -3234,6 +3260,7 @@ packages:
tslib: 2.4.0 tslib: 2.4.0
transitivePeerDependencies: transitivePeerDependencies:
- '@firebase/app-types' - '@firebase/app-types'
dev: true
/@firebase/database-types/0.9.10: /@firebase/database-types/0.9.10:
resolution: {integrity: sha512-2ji6nXRRsY+7hgU6zRhUtK0RmSjVWM71taI7Flgaw+BnopCo/lDF5HSwxp8z7LtiHlvQqeRA3Ozqx5VhlAbiKg==} resolution: {integrity: sha512-2ji6nXRRsY+7hgU6zRhUtK0RmSjVWM71taI7Flgaw+BnopCo/lDF5HSwxp8z7LtiHlvQqeRA3Ozqx5VhlAbiKg==}
@@ -3248,6 +3275,19 @@ packages:
'@firebase/app-types': 0.7.0 '@firebase/app-types': 0.7.0
'@firebase/util': 1.6.3 '@firebase/util': 1.6.3
/@firebase/database/0.13.4:
resolution: {integrity: sha512-NW7bOoiaC4sJCj6DY/m9xHoFNa0CK32YPMCh6FiMweLCDQbOZM8Ql/Kn6yyuxCb7K7ypz9eSbRlCWQJsJRQjhg==}
dependencies:
'@firebase/auth-interop-types': 0.1.6_@firebase+util@1.6.3
'@firebase/component': 0.5.17
'@firebase/logger': 0.3.3
'@firebase/util': 1.6.3
faye-websocket: 0.11.4
tslib: 2.4.0
transitivePeerDependencies:
- '@firebase/app-types'
dev: false
/@firebase/database/0.13.4_@firebase+app-types@0.7.0: /@firebase/database/0.13.4_@firebase+app-types@0.7.0:
resolution: {integrity: sha512-NW7bOoiaC4sJCj6DY/m9xHoFNa0CK32YPMCh6FiMweLCDQbOZM8Ql/Kn6yyuxCb7K7ypz9eSbRlCWQJsJRQjhg==} resolution: {integrity: sha512-NW7bOoiaC4sJCj6DY/m9xHoFNa0CK32YPMCh6FiMweLCDQbOZM8Ql/Kn6yyuxCb7K7ypz9eSbRlCWQJsJRQjhg==}
dependencies: dependencies:
@@ -3259,6 +3299,7 @@ packages:
tslib: 2.4.0 tslib: 2.4.0
transitivePeerDependencies: transitivePeerDependencies:
- '@firebase/app-types' - '@firebase/app-types'
dev: true
/@firebase/firestore-compat/0.1.23_53yvy43rwpg2c45kgeszsxtrca: /@firebase/firestore-compat/0.1.23_53yvy43rwpg2c45kgeszsxtrca:
resolution: {integrity: sha512-QfcuyMAavp//fQnjSfCEpnbWi7spIdKaXys1kOLu7395fLr+U6ykmto1HUMCSz8Yus9cEr/03Ujdi2SUl2GUAA==} resolution: {integrity: sha512-QfcuyMAavp//fQnjSfCEpnbWi7spIdKaXys1kOLu7395fLr+U6ykmto1HUMCSz8Yus9cEr/03Ujdi2SUl2GUAA==}
@@ -10332,12 +10373,12 @@ packages:
semver-regex: 2.0.0 semver-regex: 2.0.0
dev: true dev: true
/firebase-admin/11.0.1_@firebase+app-types@0.7.0: /firebase-admin/11.0.1:
resolution: {integrity: sha512-rL3wlZbi2Kb/KJgcmj1YHlD4ZhfmhfgRO2YJialxAllm0tj1IQea878hHuBLGmv4DpbW9t9nLvX9kddNR2Y65Q==} resolution: {integrity: sha512-rL3wlZbi2Kb/KJgcmj1YHlD4ZhfmhfgRO2YJialxAllm0tj1IQea878hHuBLGmv4DpbW9t9nLvX9kddNR2Y65Q==}
engines: {node: '>=14'} engines: {node: '>=14'}
dependencies: dependencies:
'@fastify/busboy': 1.1.0 '@fastify/busboy': 1.1.0
'@firebase/database-compat': 0.2.4_@firebase+app-types@0.7.0 '@firebase/database-compat': 0.2.4
'@firebase/database-types': 0.9.10 '@firebase/database-types': 0.9.10
'@types/node': 18.0.1 '@types/node': 18.0.1
jsonwebtoken: 8.5.1 jsonwebtoken: 8.5.1
@@ -13573,6 +13614,12 @@ packages:
/lru-cache/7.13.2: /lru-cache/7.13.2:
resolution: {integrity: sha512-VJL3nIpA79TodY/ctmZEfhASgqekbT574/c4j3jn4bKXbSCnTTCH/KltZyvL2GlV+tGSMtsWyem8DCX7qKTMBA==} resolution: {integrity: sha512-VJL3nIpA79TodY/ctmZEfhASgqekbT574/c4j3jn4bKXbSCnTTCH/KltZyvL2GlV+tGSMtsWyem8DCX7qKTMBA==}
engines: {node: '>=12'} engines: {node: '>=12'}
dev: false
/lru-cache/7.14.0:
resolution: {integrity: sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==}
engines: {node: '>=12'}
dev: true
/lru-memoizer/2.1.4: /lru-memoizer/2.1.4:
resolution: {integrity: sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==} resolution: {integrity: sha512-IXAq50s4qwrOBrXJklY+KhgZF+5y98PDaNo0gi/v2KQBFLyWr+JyFvijZXkGKjQj/h9c0OwoE+JZbwUXce76hQ==}