From f5948959f28483abc27200f853290f1ae2cff53c Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 20 Jun 2024 15:17:19 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8(service-worker)=20add=20offline=20doc?= =?UTF-8?q?s=20create?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add offline docs create to the service worker. We use the Network fisrt strategy, if the network is down, we will create the doc in the indexDB and serve it from there. When the connection is back, we will send the doc to the server. --- .../src/core/service-worker/ApiPlugin.ts | 106 +++++++++++++++++- .../core/service-worker/RequestSerializer.ts | 19 ++++ .../core/service-worker/service-worker-api.ts | 13 +++ 3 files changed, 137 insertions(+), 1 deletion(-) diff --git a/src/frontend/apps/impress/src/core/service-worker/ApiPlugin.ts b/src/frontend/apps/impress/src/core/service-worker/ApiPlugin.ts index 7776218d..f674c6f9 100644 --- a/src/frontend/apps/impress/src/core/service-worker/ApiPlugin.ts +++ b/src/frontend/apps/impress/src/core/service-worker/ApiPlugin.ts @@ -71,7 +71,7 @@ export class ApiPlugin implements WorkboxPlugin { * A body sent get "used", and can't be read anymore. */ requestWillFetch: WorkboxPlugin['requestWillFetch'] = async ({ request }) => { - if (this.options.type === 'update') { + if (this.options.type === 'update' || this.options.type === 'create') { this.initialRequest = request.clone(); } @@ -89,6 +89,8 @@ export class ApiPlugin implements WorkboxPlugin { } switch (this.options.type) { + case 'create': + return this.handlerDidErrorCreate(request); case 'update': return this.handlerDidErrorUpdate(request); case 'list': @@ -99,6 +101,108 @@ export class ApiPlugin implements WorkboxPlugin { return Promise.resolve(ApiPlugin.getApiCatchHandler()); }; + private handlerDidErrorCreate = async (request: Request) => { + if (!this.initialRequest) { + return new Response('Request not found', { status: 404 }); + } + + /** + * Queue the request in the cache 'doc-mutation' to sync it later. + */ + const requestData = ( + await RequestSerializer.fromRequest(this.initialRequest) + ).toObject(); + + if (!requestData.body) { + return new Response('Body found', { status: 404 }); + } + + const jsonObject = RequestSerializer.arrayBufferToJson>( + requestData.body, + ); + + // Add a new doc id to the create request + const uuid = self.crypto.randomUUID(); + const newRequestData = { + ...requestData, + body: RequestSerializer.objectToArrayBuffer({ + ...jsonObject, + id: uuid, + }), + }; + + const serializeRequest: DBRequest = { + requestData: newRequestData, + key: `${Date.now()}`, + }; + + await DocsDB.cacheResponse( + serializeRequest.key, + serializeRequest, + 'doc-mutation', + ); + + /** + * Create new item in the cache + */ + const bodyMutate = (await this.initialRequest + .clone() + .json()) as Partial; + + const newResponse = { + ...bodyMutate, + id: uuid, + content: '', + abilities: { + destroy: true, + versions_destroy: true, + versions_list: true, + versions_retrieve: true, + manage_accesses: true, + update: true, + partial_update: true, + retrieve: true, + }, + accesses: [ + { + id: 'dummy-id', + }, + ], + } as Doc; + + await DocsDB.cacheResponse( + `${request.url}${uuid}/`, + newResponse, + 'doc-item', + ); + + /** + * Add the new entry to the cache list. + */ + const db = await DocsDB.open(); + const [firstKey] = await db.getAllKeys('doc-list'); + if (firstKey) { + const list = await db.get('doc-list', firstKey); + if (!list) { + return; + } + list.results.unshift(newResponse); + await DocsDB.cacheResponse(firstKey, list, 'doc-list'); + } + db.close(); + + /** + * All is good for our client, we return the new response. + */ + return new Response(JSON.stringify(newResponse), { + status: 201, + statusText: 'OK', + headers: { + 'Content-Type': 'application/json', + }, + }); + }; + private handlerDidErrorUpdate = async (request: Request) => { const db = await DocsDB.open(); const storedResponse = await db.get('doc-item', request.url); diff --git a/src/frontend/apps/impress/src/core/service-worker/RequestSerializer.ts b/src/frontend/apps/impress/src/core/service-worker/RequestSerializer.ts index 128af228..3b347c2d 100644 --- a/src/frontend/apps/impress/src/core/service-worker/RequestSerializer.ts +++ b/src/frontend/apps/impress/src/core/service-worker/RequestSerializer.ts @@ -48,6 +48,25 @@ export class RequestSerializer { return new RequestSerializer(requestData); } + public static arrayBufferToString(buffer: ArrayBuffer) { + const decoder = new TextDecoder(); + return decoder.decode(buffer); + } + + public static arrayBufferToJson(buffer: ArrayBuffer) { + const jsonString = RequestSerializer.arrayBufferToString(buffer); + return JSON.parse(jsonString) as T; + } + + public static stringToArrayBuffer(str: string) { + const encoder = new TextEncoder(); + return encoder.encode(str).buffer; + } + + public static objectToArrayBuffer(ob: Record) { + return RequestSerializer.stringToArrayBuffer(JSON.stringify(ob)); + } + constructor(requestData: RequestData) { if (requestData.mode === 'navigate') { requestData.mode = 'same-origin'; diff --git a/src/frontend/apps/impress/src/core/service-worker/service-worker-api.ts b/src/frontend/apps/impress/src/core/service-worker/service-worker-api.ts index b08fdd26..280f76d1 100644 --- a/src/frontend/apps/impress/src/core/service-worker/service-worker-api.ts +++ b/src/frontend/apps/impress/src/core/service-worker/service-worker-api.ts @@ -65,3 +65,16 @@ registerRoute( }), 'PATCH', ); + +registerRoute( + ({ url }) => isApiUrl(url.href) && url.href.match(/.*\/documents\//), + new NetworkOnly({ + plugins: [ + new ApiPlugin({ + type: 'create', + syncManager, + }), + ], + }), + 'POST', +);