mirror of
https://github.com/pybricks/pybricks-code.git
synced 2026-09-14 10:35:11 +00:00
fileStorage: use dexie-react-hooks
This lets the dexie-react-hooks library do more of the work for us instead of having to make redux-sagas wrappers for everything.
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { createAction } from '../actions';
|
||||
import { FileMetadata } from '.';
|
||||
|
||||
/** File open modes. */
|
||||
export type FileOpenMode = 'r' | 'w';
|
||||
@@ -9,23 +10,6 @@ export type FileOpenMode = 'r' | 'w';
|
||||
/** Type to avoid mixing up file descriptor with number. */
|
||||
export type FD = number & { _fdBrand: undefined };
|
||||
|
||||
/** Type to avoid mixing UUID with regular string. */
|
||||
export type UUID = string & { _uuidBrand: undefined };
|
||||
|
||||
/**
|
||||
* Database metadata table data type.
|
||||
*
|
||||
* IMPORTANT: if this type is changed, we need to modify the database schema to match
|
||||
*/
|
||||
export type FileMetadata = Readonly<{
|
||||
/** A globally unique identifier that serves a a file handle. */
|
||||
uuid: UUID;
|
||||
/** The path of the file in storage. */
|
||||
path: string;
|
||||
/** The SHA256 hash of the file contents. */
|
||||
sha256: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Action that indicates that the storage backend is ready to use.
|
||||
* @param files List of all files currently in storage.
|
||||
@@ -46,37 +30,6 @@ export const fileStorageDidFailToInitialize = createAction((error: Error) => ({
|
||||
error,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Action that indicates that an item in the storage was created by us or in another tab.
|
||||
* @param file The file metadata.
|
||||
*/
|
||||
export const fileStorageDidAddItem = createAction((file: FileMetadata) => ({
|
||||
type: 'fileStorage.action.didAddItem',
|
||||
file,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Action that indicates that an item in the storage was changed by us or in another tab.
|
||||
* @param file The old file metadata.
|
||||
* @param file The file metadata.
|
||||
*/
|
||||
export const fileStorageDidChangeItem = createAction(
|
||||
(oldFile: FileMetadata, file: FileMetadata) => ({
|
||||
type: 'fileStorage.action.didChangeItem',
|
||||
oldFile,
|
||||
file,
|
||||
}),
|
||||
);
|
||||
|
||||
/**
|
||||
* Action that indicates that an item in the storage was removed by us or in another tab.
|
||||
* @param file The file metadata.
|
||||
*/
|
||||
export const fileStorageDidRemoveItem = createAction((file: FileMetadata) => ({
|
||||
type: 'fileStorage.action.didRemoveItem',
|
||||
file,
|
||||
}));
|
||||
|
||||
/**
|
||||
* Action that requests to open a file in storage.
|
||||
* @param path The file path.
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { createContext } from 'react';
|
||||
import { FileStorageDb } from '.';
|
||||
|
||||
export const db = new FileStorageDb('pybricks.fileStorage');
|
||||
|
||||
export const FileStorageContext = createContext(db);
|
||||
@@ -0,0 +1,27 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import { useLiveQuery } from 'dexie-react-hooks';
|
||||
import { useContext } from 'react';
|
||||
import { FileStorageContext } from './context';
|
||||
import { FileMetadata, UUID } from '.';
|
||||
|
||||
/**
|
||||
* Gets all file metadata for all files currently in storage.
|
||||
*
|
||||
* The returned array is sorted by the path.
|
||||
*/
|
||||
export function useFileStorageMetadata(): FileMetadata[] | undefined {
|
||||
const db = useContext(FileStorageContext);
|
||||
return useLiveQuery(() => db.metadata.orderBy('path').toArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the file path for a file UUID.
|
||||
*
|
||||
* If the file is renamed, the returned value will be automatically updated.
|
||||
*/
|
||||
export function useFileStoragePath(uuid: UUID): string | undefined {
|
||||
const db = useContext(FileStorageContext);
|
||||
return useLiveQuery(() => db.metadata.get(uuid, (x) => x?.path));
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import 'dexie-observable';
|
||||
import Dexie, { Table } from 'dexie';
|
||||
|
||||
/** Type to avoid mixing UUID with regular string. */
|
||||
export type UUID = string & { _uuidBrand: undefined };
|
||||
|
||||
/**
|
||||
* Database metadata table data type.
|
||||
*
|
||||
* IMPORTANT: if this type is changed, we need to modify the database schema to match
|
||||
*/
|
||||
export type FileMetadata = Readonly<{
|
||||
/** A globally unique identifier that serves a a file handle. */
|
||||
uuid: UUID;
|
||||
/** The path of the file in storage. */
|
||||
path: string;
|
||||
/** The SHA256 hash of the file contents. */
|
||||
sha256: string;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* Database contents table data type.
|
||||
*
|
||||
* IMPORTANT: if this type is changed, we need to modify the database schema to match
|
||||
*/
|
||||
type FileContents = {
|
||||
/** The path of the file in storage. */
|
||||
path: string;
|
||||
/** The contents of the file. */
|
||||
contents: string;
|
||||
};
|
||||
|
||||
export class FileStorageDb extends Dexie {
|
||||
metadata!: Table<FileMetadata, UUID>;
|
||||
// NB: This table starts with an underscore to hide it from Dexie observable.
|
||||
// In the future we may change this to use File Access API or some other
|
||||
// storage, so we don't want to rely on the file contents being included
|
||||
// with the metadata.
|
||||
_contents!: Table<FileContents, string>;
|
||||
|
||||
constructor(databaseName: string) {
|
||||
super(databaseName);
|
||||
this.version(1).stores({
|
||||
metadata: '$$uuid, &path, sha256',
|
||||
_contents: 'path, contents',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -8,13 +8,10 @@ import { AsyncSaga, uuid } from '../../test';
|
||||
import { createCountFunc } from '../utils/iter';
|
||||
import {
|
||||
FD,
|
||||
FileMetadata,
|
||||
FileOpenMode,
|
||||
fileStorageClose,
|
||||
fileStorageCopyFile,
|
||||
fileStorageDeleteFile,
|
||||
fileStorageDidAddItem,
|
||||
fileStorageDidChangeItem,
|
||||
fileStorageDidClose,
|
||||
fileStorageDidCopyFile,
|
||||
fileStorageDidDeleteFile,
|
||||
@@ -33,7 +30,6 @@ import {
|
||||
fileStorageDidOpen,
|
||||
fileStorageDidRead,
|
||||
fileStorageDidReadFile,
|
||||
fileStorageDidRemoveItem,
|
||||
fileStorageDidRenameFile,
|
||||
fileStorageDidWrite,
|
||||
fileStorageDidWriteFile,
|
||||
@@ -46,10 +42,7 @@ import {
|
||||
fileStorageWriteFile,
|
||||
} from './actions';
|
||||
import fileStorage from './sagas';
|
||||
|
||||
/** SHA256 hash of '' */
|
||||
const emptyFileSha256 =
|
||||
'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
|
||||
import { FileMetadata, FileStorageDb } from '.';
|
||||
|
||||
beforeEach(() => {
|
||||
// deterministic UUID generator for repeatable tests
|
||||
@@ -61,7 +54,7 @@ afterEach(async () => {
|
||||
jest.restoreAllMocks();
|
||||
|
||||
await new Promise((resolve, reject) => {
|
||||
const request = indexedDB.deleteDatabase('pybricks.fileStorage');
|
||||
const request = indexedDB.deleteDatabase('test');
|
||||
request.addEventListener('success', resolve);
|
||||
request.addEventListener('error', reject);
|
||||
});
|
||||
@@ -87,8 +80,6 @@ async function setUpTestFile(saga: AsyncSaga): Promise<[FileMetadata, string]> {
|
||||
sha256: testFileContentsSha256,
|
||||
};
|
||||
|
||||
const emptyFile: FileMetadata = { ...testFile, sha256: emptyFileSha256 };
|
||||
|
||||
saga.put(fileStorageOpen(testFilePath, 'w', true));
|
||||
|
||||
const didOpen = await saga.take();
|
||||
@@ -97,14 +88,9 @@ async function setUpTestFile(saga: AsyncSaga): Promise<[FileMetadata, string]> {
|
||||
fail(didOpen);
|
||||
}
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidAddItem(emptyFile));
|
||||
|
||||
saga.put(fileStorageWrite(didOpen.fd, testFileContents));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidWrite(didOpen.fd));
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
fileStorageDidChangeItem(emptyFile, testFile),
|
||||
);
|
||||
|
||||
saga.put(fileStorageClose(didOpen.fd));
|
||||
|
||||
@@ -124,7 +110,9 @@ describe('initialize', () => {
|
||||
localStorage.setItem(oldProgramKey, oldProgramContents);
|
||||
expect(localStorage.getItem(oldProgramKey)).toBe(oldProgramContents);
|
||||
|
||||
const saga = new AsyncSaga(fileStorage);
|
||||
const saga = new AsyncSaga(fileStorage, {
|
||||
fileStorage: new FileStorageDb('test'),
|
||||
});
|
||||
|
||||
// initialization should remove the localStorage entry and add add it to
|
||||
// new storage backend
|
||||
@@ -145,7 +133,9 @@ describe('initialize', () => {
|
||||
throw testError;
|
||||
});
|
||||
|
||||
const saga = new AsyncSaga(fileStorage);
|
||||
const saga = new AsyncSaga(fileStorage, {
|
||||
fileStorage: new FileStorageDb('test'),
|
||||
});
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
fileStorageDidFailToInitialize(testError),
|
||||
@@ -159,7 +149,7 @@ describe('open', () => {
|
||||
let saga: AsyncSaga;
|
||||
|
||||
beforeEach(async () => {
|
||||
saga = new AsyncSaga(fileStorage);
|
||||
saga = new AsyncSaga(fileStorage, { fileStorage: new FileStorageDb('test') });
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidInitialize([]));
|
||||
});
|
||||
@@ -182,14 +172,6 @@ describe('open', () => {
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
fileStorageDidOpen('test.file', 0 as FD),
|
||||
);
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
fileStorageDidAddItem({
|
||||
uuid: uuid(0),
|
||||
path: 'test.file',
|
||||
sha256: emptyFileSha256,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
describe('should fail to open if file is already open for writing', () => {
|
||||
@@ -268,14 +250,6 @@ describe('open', () => {
|
||||
fileStorageDidOpen('test.file', 0 as FD),
|
||||
);
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
fileStorageDidAddItem({
|
||||
uuid: uuid(0),
|
||||
path: 'test.file',
|
||||
sha256: emptyFileSha256,
|
||||
}),
|
||||
);
|
||||
|
||||
saga.put(fileStorageClose(0 as FD));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidClose(0 as FD));
|
||||
@@ -294,7 +268,7 @@ describe('read', () => {
|
||||
let saga: AsyncSaga;
|
||||
|
||||
beforeEach(async () => {
|
||||
saga = new AsyncSaga(fileStorage);
|
||||
saga = new AsyncSaga(fileStorage, { fileStorage: new FileStorageDb('test') });
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidInitialize([]));
|
||||
});
|
||||
|
||||
@@ -350,7 +324,7 @@ describe('write', () => {
|
||||
let saga: AsyncSaga;
|
||||
|
||||
beforeEach(async () => {
|
||||
saga = new AsyncSaga(fileStorage);
|
||||
saga = new AsyncSaga(fileStorage, { fileStorage: new FileStorageDb('test') });
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidInitialize([]));
|
||||
});
|
||||
|
||||
@@ -421,7 +395,7 @@ describe('readFile', () => {
|
||||
let saga: AsyncSaga;
|
||||
|
||||
beforeEach(async () => {
|
||||
saga = new AsyncSaga(fileStorage);
|
||||
saga = new AsyncSaga(fileStorage, { fileStorage: new FileStorageDb('test') });
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidInitialize([]));
|
||||
|
||||
saga.put(fileStorageReadFile('test.file'));
|
||||
@@ -488,7 +462,7 @@ describe('writeFile', () => {
|
||||
const contents = 'test write file contents';
|
||||
|
||||
beforeEach(async () => {
|
||||
saga = new AsyncSaga(fileStorage);
|
||||
saga = new AsyncSaga(fileStorage, { fileStorage: new FileStorageDb('test') });
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidInitialize([]));
|
||||
|
||||
saga.put(fileStorageWriteFile('test.file', contents));
|
||||
@@ -553,13 +527,12 @@ describe('writeFile', () => {
|
||||
|
||||
describe('copyFile', () => {
|
||||
let saga: AsyncSaga;
|
||||
let testFile: FileMetadata;
|
||||
|
||||
beforeEach(async () => {
|
||||
saga = new AsyncSaga(fileStorage);
|
||||
saga = new AsyncSaga(fileStorage, { fileStorage: new FileStorageDb('test') });
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidInitialize([]));
|
||||
[testFile] = await setUpTestFile(saga);
|
||||
await setUpTestFile(saga);
|
||||
});
|
||||
|
||||
it('should fail if file does not exist', async () => {
|
||||
@@ -612,10 +585,6 @@ describe('copyFile', () => {
|
||||
saga.put(fileStorageCopyFile('test.file', 'new.file'));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidCopyFile('test.file'));
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
fileStorageDidAddItem({ ...testFile, uuid: uuid(1), path: 'new.file' }),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -625,13 +594,12 @@ describe('copyFile', () => {
|
||||
|
||||
describe('deleteFile', () => {
|
||||
let saga: AsyncSaga;
|
||||
let testFile: FileMetadata;
|
||||
|
||||
beforeEach(async () => {
|
||||
saga = new AsyncSaga(fileStorage);
|
||||
saga = new AsyncSaga(fileStorage, { fileStorage: new FileStorageDb('test') });
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidInitialize([]));
|
||||
[testFile] = await setUpTestFile(saga);
|
||||
await setUpTestFile(saga);
|
||||
});
|
||||
|
||||
it('should fail if file does not exist', async () => {
|
||||
@@ -667,8 +635,6 @@ describe('deleteFile', () => {
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
fileStorageDidDeleteFile('test.file'),
|
||||
);
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidRemoveItem(testFile));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -682,7 +648,7 @@ describe('renameFile', () => {
|
||||
const newPath = 'new.file';
|
||||
|
||||
beforeEach(async () => {
|
||||
saga = new AsyncSaga(fileStorage);
|
||||
saga = new AsyncSaga(fileStorage, { fileStorage: new FileStorageDb('test') });
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidInitialize([]));
|
||||
[testFile] = await setUpTestFile(saga);
|
||||
@@ -724,13 +690,6 @@ describe('renameFile', () => {
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
fileStorageDidOpen(newPath, 1 as FD),
|
||||
);
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
fileStorageDidAddItem({
|
||||
uuid: uuid(1),
|
||||
path: newPath,
|
||||
sha256: emptyFileSha256,
|
||||
}),
|
||||
);
|
||||
|
||||
saga.put(fileStorageRenameFile('test.file', newPath));
|
||||
|
||||
@@ -760,15 +719,9 @@ describe('renameFile', () => {
|
||||
it('should change file', async () => {
|
||||
saga.put(fileStorageRenameFile(testFile.path, newPath));
|
||||
|
||||
const newMetadata: FileMetadata = { ...testFile, path: newPath };
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
fileStorageDidRenameFile(testFile.path),
|
||||
);
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(
|
||||
fileStorageDidChangeItem(testFile, newMetadata),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
@@ -782,7 +735,7 @@ describe('dump all files', () => {
|
||||
let testFileContents: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
saga = new AsyncSaga(fileStorage);
|
||||
saga = new AsyncSaga(fileStorage, { fileStorage: new FileStorageDb('test') });
|
||||
|
||||
await expect(saga.take()).resolves.toEqual(fileStorageDidInitialize([]));
|
||||
[testFile, testFileContents] = await setUpTestFile(saga);
|
||||
|
||||
+11
-124
@@ -1,29 +1,24 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2022 The Pybricks Authors
|
||||
|
||||
import Dexie, { Table } from 'dexie';
|
||||
import {
|
||||
ICreateChange,
|
||||
IDatabaseChange,
|
||||
IDeleteChange,
|
||||
IUpdateChange,
|
||||
} from 'dexie-observable/api';
|
||||
import 'dexie-observable';
|
||||
import { eventChannel } from 'redux-saga';
|
||||
import { call, fork, put, race, take, takeEvery } from 'typed-redux-saga/macro';
|
||||
call,
|
||||
fork,
|
||||
getContext,
|
||||
put,
|
||||
race,
|
||||
take,
|
||||
takeEvery,
|
||||
} from 'typed-redux-saga/macro';
|
||||
import { defined, ensureError } from '../utils';
|
||||
import { sha256Digest } from '../utils/crypto';
|
||||
import { createCountFunc } from '../utils/iter';
|
||||
import {
|
||||
FD,
|
||||
FileMetadata,
|
||||
FileOpenMode,
|
||||
UUID,
|
||||
fileStorageClose,
|
||||
fileStorageCopyFile,
|
||||
fileStorageDeleteFile,
|
||||
fileStorageDidAddItem,
|
||||
fileStorageDidChangeItem,
|
||||
fileStorageDidClose,
|
||||
fileStorageDidCopyFile,
|
||||
fileStorageDidDeleteFile,
|
||||
@@ -42,7 +37,6 @@ import {
|
||||
fileStorageDidOpen,
|
||||
fileStorageDidRead,
|
||||
fileStorageDidReadFile,
|
||||
fileStorageDidRemoveItem,
|
||||
fileStorageDidRenameFile,
|
||||
fileStorageDidWrite,
|
||||
fileStorageDidWriteFile,
|
||||
@@ -54,88 +48,13 @@ import {
|
||||
fileStorageWrite,
|
||||
fileStorageWriteFile,
|
||||
} from './actions';
|
||||
import { FileMetadata, FileStorageDb, UUID } from '.';
|
||||
|
||||
// HACK: we have to redefine DatabaseChangeType since it is a const enum
|
||||
// https://ncjamieson.com/dont-export-const-enums
|
||||
const enum DatabaseChangeType {
|
||||
Create = 1,
|
||||
Update = 2,
|
||||
Delete = 3,
|
||||
}
|
||||
|
||||
/** Type discriminator for {@link ICreateChange} */
|
||||
function isCreateChange(change: IDatabaseChange): change is ICreateChange {
|
||||
return change.type === Number(DatabaseChangeType.Create);
|
||||
}
|
||||
|
||||
/** Type discriminator for {@link IUpdateChange} */
|
||||
function isUpdateChange(change: IDatabaseChange): change is IUpdateChange {
|
||||
return change.type === Number(DatabaseChangeType.Update);
|
||||
}
|
||||
|
||||
/** Type discriminator for {@link IDeleteChange} */
|
||||
function isDeleteChange(change: IDatabaseChange): change is IDeleteChange {
|
||||
return change.type === Number(DatabaseChangeType.Delete);
|
||||
}
|
||||
|
||||
/** Type discriminator for {@link ICreateChange} of {@link FileMetadata} */
|
||||
function isFileMetadataCreateChange(
|
||||
change: ICreateChange,
|
||||
): change is Omit<ICreateChange, 'key' | 'obj'> & { key: string; obj: FileMetadata } {
|
||||
return change.table === 'metadata';
|
||||
}
|
||||
|
||||
/** Type discriminator for {@link IUpdateChange} of {@link FileMetadata} */
|
||||
function isFileMetadataUpdateChange(change: IUpdateChange): change is Omit<
|
||||
IUpdateChange,
|
||||
'key' | 'obj' | 'oldObj'
|
||||
> & {
|
||||
key: string;
|
||||
obj: FileMetadata;
|
||||
oldObj: FileMetadata;
|
||||
} {
|
||||
return change.table === 'metadata';
|
||||
}
|
||||
|
||||
/** Type discriminator for {@link IDeleteChange} of {@link FileMetadata} */
|
||||
function isFileMetaDataDeleteChange(change: IDeleteChange): change is Omit<
|
||||
IDeleteChange,
|
||||
'key' | 'oldObj'
|
||||
> & {
|
||||
key: string;
|
||||
oldObj: FileMetadata;
|
||||
} {
|
||||
return change.table === 'metadata';
|
||||
}
|
||||
|
||||
/** Database contents table data type. */
|
||||
type FileContents = {
|
||||
/** The path of the file in storage. */
|
||||
path: string;
|
||||
/** The contents of the file. */
|
||||
contents: string;
|
||||
};
|
||||
export type FileStorageSageContext = { fileStorage: FileStorageDb };
|
||||
|
||||
/** Map for keeping track of open file descriptors. */
|
||||
type OpenFdMap = Map<FD, { mode: FileOpenMode; uuid: UUID }>;
|
||||
|
||||
class FileStorageDb extends Dexie {
|
||||
metadata!: Table<FileMetadata, UUID>;
|
||||
// NB: This table starts with an underscore to hide it from Dexie observable.
|
||||
// In the future we may change this to use File Access API or some other
|
||||
// storage, so we don't want to rely on the file contents being included
|
||||
// with the metadata.
|
||||
_contents!: Table<FileContents, string>;
|
||||
|
||||
constructor() {
|
||||
super('pybricks.fileStorage');
|
||||
this.version(1).stores({
|
||||
metadata: '$$uuid, &path, sha256',
|
||||
_contents: 'path, contents',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a namespaced lock name for the given path.
|
||||
*/
|
||||
@@ -143,28 +62,6 @@ function lockNameForPath(path: string): string {
|
||||
return `pybricks.fileStorage:${path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts localForage change events to redux actions.
|
||||
* @param changes The list of changes from the 'changed' event.
|
||||
*/
|
||||
function* handleFileStorageDidChange(changes: IDatabaseChange[]): Generator {
|
||||
for (const change of changes) {
|
||||
if (isCreateChange(change)) {
|
||||
if (isFileMetadataCreateChange(change)) {
|
||||
yield* put(fileStorageDidAddItem(change.obj));
|
||||
}
|
||||
} else if (isUpdateChange(change)) {
|
||||
if (isFileMetadataUpdateChange(change)) {
|
||||
yield* put(fileStorageDidChangeItem(change.oldObj, change.obj));
|
||||
}
|
||||
} else if (isDeleteChange(change)) {
|
||||
if (isFileMetaDataDeleteChange(change)) {
|
||||
yield* put(fileStorageDidRemoveItem(change.oldObj));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles requests to open a file.
|
||||
* @param db The database instance.
|
||||
@@ -706,7 +603,7 @@ function* initialize(): Generator {
|
||||
const defer = new Array<(...args: unknown[]) => unknown>();
|
||||
|
||||
try {
|
||||
const db = new FileStorageDb();
|
||||
const db = yield* getContext<FileStorageDb>('fileStorage');
|
||||
|
||||
// migrate from old storage
|
||||
|
||||
@@ -735,21 +632,11 @@ function* initialize(): Generator {
|
||||
yield* call(() => db.open());
|
||||
defer.push(() => db.close());
|
||||
|
||||
// wire storage observable to redux-sagas
|
||||
|
||||
const changesChan = eventChannel<IDatabaseChange[]>((emit) => {
|
||||
db.on('changes').subscribe(emit);
|
||||
return () => db.on('changes').unsubscribe(emit);
|
||||
});
|
||||
|
||||
defer.push(() => changesChan.close());
|
||||
|
||||
// subscribe to events
|
||||
|
||||
const nextFd = createCountFunc() as () => FD;
|
||||
const openFds: OpenFdMap = new Map();
|
||||
|
||||
yield* takeEvery(changesChan, handleFileStorageDidChange);
|
||||
yield* takeEvery(fileStorageOpen, handleOpen, db, nextFd, openFds);
|
||||
yield* takeEvery(fileStorageClose, handleClose, openFds);
|
||||
yield* takeEvery(fileStorageRead, handleRead, db, openFds);
|
||||
|
||||
Reference in New Issue
Block a user