♻️(service-worker) add DocsDB to store docs in indexedDB
Export the storage of docs to a separate class `DocsDB` that will be responsible for storing / retrieving / synching docs from indexedDB.
This commit is contained in:
@@ -1,31 +1,10 @@
|
|||||||
import { DBSchema, openDB } from 'idb';
|
|
||||||
import { WorkboxPlugin } from 'workbox-core';
|
import { WorkboxPlugin } from 'workbox-core';
|
||||||
|
|
||||||
import { Doc, DocsResponse } from '@/features/docs';
|
import { Doc, DocsResponse } from '@/features/docs';
|
||||||
|
|
||||||
import { RequestData, RequestSerializer } from './RequestSerializer';
|
import { DBRequest, DocsDB } from './DocsDB';
|
||||||
|
import { RequestSerializer } from './RequestSerializer';
|
||||||
import { SyncManager } from './SyncManager';
|
import { SyncManager } from './SyncManager';
|
||||||
import { getApiCatchHandler } from './utils';
|
|
||||||
|
|
||||||
type DBRequest = {
|
|
||||||
requestData: RequestData;
|
|
||||||
key: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface DocsDB extends DBSchema {
|
|
||||||
'doc-list': {
|
|
||||||
key: string;
|
|
||||||
value: DocsResponse;
|
|
||||||
};
|
|
||||||
'doc-item': {
|
|
||||||
key: string;
|
|
||||||
value: Doc;
|
|
||||||
};
|
|
||||||
'doc-mutation': {
|
|
||||||
key: string;
|
|
||||||
value: DBRequest;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface OptionsReadonly {
|
interface OptionsReadonly {
|
||||||
tableName: 'doc-list' | 'doc-item';
|
tableName: 'doc-list' | 'doc-item';
|
||||||
@@ -33,35 +12,30 @@ interface OptionsReadonly {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface OptionsMutate {
|
interface OptionsMutate {
|
||||||
tableName: 'doc-mutation';
|
|
||||||
type: 'update' | 'delete' | 'create';
|
type: 'update' | 'delete' | 'create';
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options = (OptionsReadonly | OptionsMutate) & { syncManager: SyncManager };
|
type Options = (OptionsReadonly | OptionsMutate) & { syncManager: SyncManager };
|
||||||
|
|
||||||
export class ApiPlugin implements WorkboxPlugin {
|
export class ApiPlugin implements WorkboxPlugin {
|
||||||
private static readonly DBNAME = 'api-docs-db';
|
|
||||||
private readonly options: Options;
|
private readonly options: Options;
|
||||||
private isFetchDidFailed = false;
|
private isFetchDidFailed = false;
|
||||||
private initialRequest?: Request;
|
private initialRequest?: Request;
|
||||||
|
|
||||||
|
static getApiCatchHandler = () => {
|
||||||
|
return new Response(JSON.stringify({ error: 'Network is unavailable.' }), {
|
||||||
|
status: 502,
|
||||||
|
statusText: 'Network is unavailable.',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
constructor(options: Options) {
|
constructor(options: Options) {
|
||||||
this.options = options;
|
this.options = options;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the response in the IndexedDB.
|
|
||||||
*/
|
|
||||||
private async cacheResponse(
|
|
||||||
key: string,
|
|
||||||
body: DocsResponse | Doc | DBRequest,
|
|
||||||
tableName: Options['tableName'],
|
|
||||||
): Promise<void> {
|
|
||||||
const db = await ApiPlugin.DB();
|
|
||||||
|
|
||||||
await db.put(tableName, body, key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This method is called after the response is received.
|
* This method is called after the response is received.
|
||||||
* An error server response is not a failed fetch.
|
* An error server response is not a failed fetch.
|
||||||
@@ -75,17 +49,9 @@ export class ApiPlugin implements WorkboxPlugin {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const tableName = this.options.tableName;
|
||||||
const tableName = this.options.tableName;
|
const body = (await response.clone().json()) as DocsResponse | Doc;
|
||||||
const body = (await response.clone().json()) as DocsResponse | Doc;
|
await DocsDB.cacheResponse(request.url, body, tableName);
|
||||||
await this.cacheResponse(request.url, body, tableName);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
'SW-DEV: Failed to save response in IndexedDB',
|
|
||||||
error,
|
|
||||||
this.options,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
@@ -119,28 +85,22 @@ export class ApiPlugin implements WorkboxPlugin {
|
|||||||
*/
|
*/
|
||||||
handlerDidError: WorkboxPlugin['handlerDidError'] = async ({ request }) => {
|
handlerDidError: WorkboxPlugin['handlerDidError'] = async ({ request }) => {
|
||||||
if (!this.isFetchDidFailed) {
|
if (!this.isFetchDidFailed) {
|
||||||
return Promise.resolve(getApiCatchHandler());
|
return Promise.resolve(ApiPlugin.getApiCatchHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
switch (this.options.type) {
|
||||||
* Update the cache item to sync it later.
|
case 'update':
|
||||||
*/
|
return this.handlerDidErrorUpdate(request);
|
||||||
if (this.options.type === 'update') {
|
case 'list':
|
||||||
return this.handlerDidErrorUpdate(request);
|
case 'item':
|
||||||
|
return this.handlerDidErrorRead(this.options.tableName, request.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
return Promise.resolve(ApiPlugin.getApiCatchHandler());
|
||||||
* Get data from the cache.
|
|
||||||
*/
|
|
||||||
if (this.options.type === 'list' || this.options.type === 'item') {
|
|
||||||
return this.handlerDidErrorRead(this.options.tableName, request.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
return Promise.resolve(getApiCatchHandler());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
private handlerDidErrorUpdate = async (request: Request) => {
|
private handlerDidErrorUpdate = async (request: Request) => {
|
||||||
const db = await openDB<DocsDB>(ApiPlugin.DBNAME, 1);
|
const db = await DocsDB.open();
|
||||||
const storedResponse = await db.get('doc-item', request.url);
|
const storedResponse = await db.get('doc-item', request.url);
|
||||||
|
|
||||||
if (!storedResponse || !this.initialRequest) {
|
if (!storedResponse || !this.initialRequest) {
|
||||||
@@ -159,7 +119,7 @@ export class ApiPlugin implements WorkboxPlugin {
|
|||||||
key: `${Date.now()}`,
|
key: `${Date.now()}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
await this.cacheResponse(
|
await DocsDB.cacheResponse(
|
||||||
serializeRequest.key,
|
serializeRequest.key,
|
||||||
serializeRequest,
|
serializeRequest,
|
||||||
'doc-mutation',
|
'doc-mutation',
|
||||||
@@ -177,7 +137,7 @@ export class ApiPlugin implements WorkboxPlugin {
|
|||||||
...bodyMutate,
|
...bodyMutate,
|
||||||
};
|
};
|
||||||
|
|
||||||
await db.put('doc-item', newResponse, request.url);
|
await DocsDB.cacheResponse(request.url, newResponse, 'doc-item');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the cache list with the new data.
|
* Update the cache list with the new data.
|
||||||
@@ -205,9 +165,11 @@ export class ApiPlugin implements WorkboxPlugin {
|
|||||||
return result;
|
return result;
|
||||||
});
|
});
|
||||||
|
|
||||||
await db.put('doc-list', list, key);
|
await DocsDB.cacheResponse(key, list, 'doc-list');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* All is good for our client, we return the new response.
|
* All is good for our client, we return the new response.
|
||||||
*/
|
*/
|
||||||
@@ -224,11 +186,11 @@ export class ApiPlugin implements WorkboxPlugin {
|
|||||||
tableName: OptionsReadonly['tableName'],
|
tableName: OptionsReadonly['tableName'],
|
||||||
url: string,
|
url: string,
|
||||||
) => {
|
) => {
|
||||||
const db = await openDB<DocsDB>(ApiPlugin.DBNAME, 1);
|
const db = await DocsDB.open();
|
||||||
const storedResponse = await db.get(tableName, url);
|
const storedResponse = await db.get(tableName, url);
|
||||||
|
|
||||||
if (!storedResponse) {
|
if (!storedResponse) {
|
||||||
return Promise.resolve(getApiCatchHandler());
|
return Promise.resolve(ApiPlugin.getApiCatchHandler());
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(JSON.stringify(storedResponse), {
|
return new Response(JSON.stringify(storedResponse), {
|
||||||
@@ -239,49 +201,4 @@ export class ApiPlugin implements WorkboxPlugin {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
public static hasSyncToDo = async () => {
|
|
||||||
const db = await ApiPlugin.DB();
|
|
||||||
const requests = await db.getAll('doc-mutation');
|
|
||||||
|
|
||||||
return requests.length > 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync the queue with the server.
|
|
||||||
*/
|
|
||||||
public static sync = async () => {
|
|
||||||
const db = await ApiPlugin.DB();
|
|
||||||
const requests = await db.getAll('doc-mutation');
|
|
||||||
|
|
||||||
for (const request of requests) {
|
|
||||||
try {
|
|
||||||
await fetch(new RequestSerializer(request.requestData).toRequest());
|
|
||||||
await db.delete('doc-mutation', request.key);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('SW-DEV: Replay failed for request', request, error);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* IndexedDB instance.
|
|
||||||
* @returns Promise<IDBPDatabase<DocsDB>>
|
|
||||||
*/
|
|
||||||
private static DB = async () => {
|
|
||||||
return await openDB<DocsDB>(ApiPlugin.DBNAME, 1, {
|
|
||||||
upgrade(db) {
|
|
||||||
if (!db.objectStoreNames.contains('doc-list')) {
|
|
||||||
db.createObjectStore('doc-list');
|
|
||||||
}
|
|
||||||
if (!db.objectStoreNames.contains('doc-item')) {
|
|
||||||
db.createObjectStore('doc-item');
|
|
||||||
}
|
|
||||||
if (!db.objectStoreNames.contains('doc-mutation')) {
|
|
||||||
db.createObjectStore('doc-mutation');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|||||||
168
src/frontend/apps/impress/src/core/service-worker/DocsDB.ts
Normal file
168
src/frontend/apps/impress/src/core/service-worker/DocsDB.ts
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
import { DBSchema, IDBPDatabase, deleteDB, openDB } from 'idb';
|
||||||
|
|
||||||
|
import { Doc, DocsResponse } from '@/features/docs';
|
||||||
|
|
||||||
|
import { RequestData, RequestSerializer } from './RequestSerializer';
|
||||||
|
|
||||||
|
// eslint-disable-next-line import/order
|
||||||
|
import pkg from '@/../package.json';
|
||||||
|
|
||||||
|
export type DBRequest = {
|
||||||
|
requestData: RequestData;
|
||||||
|
key: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IDocsDB extends DBSchema {
|
||||||
|
'doc-list': {
|
||||||
|
key: string;
|
||||||
|
value: DocsResponse;
|
||||||
|
};
|
||||||
|
'doc-item': {
|
||||||
|
key: string;
|
||||||
|
value: Doc;
|
||||||
|
};
|
||||||
|
'doc-mutation': {
|
||||||
|
key: string;
|
||||||
|
value: DBRequest;
|
||||||
|
};
|
||||||
|
'doc-version': {
|
||||||
|
key: 'version';
|
||||||
|
value: number;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
type TableName = 'doc-list' | 'doc-item' | 'doc-mutation';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IndexDB version must be a integer
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getCurrentVersion = () => {
|
||||||
|
const [major, minor, patch] = pkg.version.split('.');
|
||||||
|
return parseFloat(`${major}${minor}${patch}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Static class for managing the Docs with IndexedDB.
|
||||||
|
*/
|
||||||
|
export class DocsDB {
|
||||||
|
private static readonly DBNAME = 'api-docs-db';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* IndexedDB instance.
|
||||||
|
* @returns Promise<IDBPDatabase<DocsDB>>
|
||||||
|
*/
|
||||||
|
public static open = async () => {
|
||||||
|
let db: IDBPDatabase<IDocsDB>;
|
||||||
|
|
||||||
|
try {
|
||||||
|
db = await openDB<IDocsDB>(DocsDB.DBNAME, getCurrentVersion(), {
|
||||||
|
upgrade(db) {
|
||||||
|
if (!db.objectStoreNames.contains('doc-list')) {
|
||||||
|
db.createObjectStore('doc-list');
|
||||||
|
}
|
||||||
|
if (!db.objectStoreNames.contains('doc-item')) {
|
||||||
|
db.createObjectStore('doc-item');
|
||||||
|
}
|
||||||
|
if (!db.objectStoreNames.contains('doc-mutation')) {
|
||||||
|
db.createObjectStore('doc-mutation');
|
||||||
|
}
|
||||||
|
if (!db.objectStoreNames.contains('doc-version')) {
|
||||||
|
db.createObjectStore('doc-version');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
/**
|
||||||
|
* If for any reason the current version is lower than the previous one,
|
||||||
|
* we need to delete the database and create a new one.
|
||||||
|
*/
|
||||||
|
console.error('SW: Failed to open IndexedDB', error);
|
||||||
|
await deleteDB(DocsDB.DBNAME);
|
||||||
|
|
||||||
|
db = await DocsDB.open();
|
||||||
|
}
|
||||||
|
|
||||||
|
return db;
|
||||||
|
};
|
||||||
|
|
||||||
|
public static deleteAll = async (tableName: TableName) => {
|
||||||
|
const db = await DocsDB.open();
|
||||||
|
const keys = await db.getAllKeys(tableName);
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
await db.delete(tableName, key);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
public static cleanupOutdatedVersion = async () => {
|
||||||
|
const db = await DocsDB.open();
|
||||||
|
const version = await db.get('doc-version', 'version');
|
||||||
|
const currentVersion = getCurrentVersion();
|
||||||
|
|
||||||
|
if (version != currentVersion) {
|
||||||
|
console.debug('SW: Cleaning up outdated caches', version, currentVersion);
|
||||||
|
|
||||||
|
await DocsDB.deleteAll('doc-item');
|
||||||
|
await DocsDB.deleteAll('doc-list');
|
||||||
|
await DocsDB.deleteAll('doc-mutation');
|
||||||
|
await db.put('doc-version', currentVersion, 'version');
|
||||||
|
}
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the response in the IndexedDB.
|
||||||
|
*/
|
||||||
|
public static async cacheResponse(
|
||||||
|
key: string,
|
||||||
|
body: DocsResponse | Doc | DBRequest,
|
||||||
|
tableName: TableName,
|
||||||
|
): Promise<void> {
|
||||||
|
const db = await DocsDB.open();
|
||||||
|
|
||||||
|
try {
|
||||||
|
await db.put(tableName, body, key);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
'SW: Failed to save response in IndexedDB',
|
||||||
|
error,
|
||||||
|
key,
|
||||||
|
body,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static hasSyncToDo = async () => {
|
||||||
|
const db = await DocsDB.open();
|
||||||
|
const requests = await db.getAll('doc-mutation');
|
||||||
|
db.close();
|
||||||
|
|
||||||
|
return requests.length > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync the queue with the server.
|
||||||
|
*/
|
||||||
|
public static sync = async () => {
|
||||||
|
const db = await DocsDB.open();
|
||||||
|
const requests = await db.getAll('doc-mutation');
|
||||||
|
|
||||||
|
for (const request of requests) {
|
||||||
|
try {
|
||||||
|
await fetch(new RequestSerializer(request.requestData).toRequest());
|
||||||
|
await db.delete('doc-mutation', request.key);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('SW: Replay failed for request', request, error);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db.close();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,11 +2,16 @@ import { registerRoute } from 'workbox-routing';
|
|||||||
import { NetworkOnly } from 'workbox-strategies';
|
import { NetworkOnly } from 'workbox-strategies';
|
||||||
|
|
||||||
import { ApiPlugin } from './ApiPlugin';
|
import { ApiPlugin } from './ApiPlugin';
|
||||||
|
import { DocsDB } from './DocsDB';
|
||||||
import { SyncManager } from './SyncManager';
|
import { SyncManager } from './SyncManager';
|
||||||
|
|
||||||
declare const self: ServiceWorkerGlobalScope;
|
declare const self: ServiceWorkerGlobalScope;
|
||||||
|
|
||||||
const syncManager = new SyncManager(ApiPlugin.sync, ApiPlugin.hasSyncToDo);
|
const syncManager = new SyncManager(DocsDB.sync, DocsDB.hasSyncToDo);
|
||||||
|
|
||||||
|
self.addEventListener('activate', function (event) {
|
||||||
|
event.waitUntil(DocsDB.cleanupOutdatedVersion());
|
||||||
|
});
|
||||||
|
|
||||||
export const isApiUrl = (href: string) => {
|
export const isApiUrl = (href: string) => {
|
||||||
const devDomain = 'http://localhost:8071';
|
const devDomain = 'http://localhost:8071';
|
||||||
@@ -53,7 +58,6 @@ registerRoute(
|
|||||||
new NetworkOnly({
|
new NetworkOnly({
|
||||||
plugins: [
|
plugins: [
|
||||||
new ApiPlugin({
|
new ApiPlugin({
|
||||||
tableName: 'doc-mutation',
|
|
||||||
type: 'update',
|
type: 'update',
|
||||||
syncManager,
|
syncManager,
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user