@openmrs/esm-offline 3.1.15-pre.892 → 3.2.1-pre.1012

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-offline",
3
- "version": "3.1.15-pre.892",
3
+ "version": "3.2.1-pre.1012",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Helper utilities for OpenMRS",
6
6
  "browser": "dist/openmrs-esm-offline.js",
@@ -36,10 +36,10 @@
36
36
  "access": "public"
37
37
  },
38
38
  "devDependencies": {
39
- "@openmrs/esm-api": "^3.1.15-pre.892",
40
- "@openmrs/esm-globals": "^3.1.15-pre.892",
41
- "@openmrs/esm-state": "^3.1.15-pre.892",
42
- "@openmrs/esm-styleguide": "^3.1.15-pre.892",
39
+ "@openmrs/esm-api": "^3.2.1-pre.1012",
40
+ "@openmrs/esm-globals": "^3.2.1-pre.1012",
41
+ "@openmrs/esm-state": "^3.2.1-pre.1012",
42
+ "@openmrs/esm-styleguide": "^3.2.1-pre.1012",
43
43
  "@types/uuid": "^8.3.0",
44
44
  "rxjs": "^6.5.3"
45
45
  },
@@ -56,5 +56,5 @@
56
56
  "uuid": "^8.3.2",
57
57
  "workbox-window": "^6.1.5"
58
58
  },
59
- "gitHead": "bb0cbb2387f66d9b5b8470e87284b7c7e3d612ac"
59
+ "gitHead": "96acf2eb0cf6d8d83d7ce12280bbf445c34c5672"
60
60
  }
package/src/index.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./service-worker-http-headers";
3
3
  export * from "./service-worker-messaging";
4
4
  export * from "./service-worker-events";
5
5
  export * from "./events";
6
+ export * from "./mode";
6
7
  export * from "./patches";
7
8
  export * from "./sync";
8
9
  export * from "./uuid";
package/src/mode.ts ADDED
@@ -0,0 +1,79 @@
1
+ async function isSafariPrivateBrowsing() {
2
+ const storage = window.sessionStorage;
3
+
4
+ try {
5
+ storage.setItem("someKeyHere", "test");
6
+ storage.removeItem("someKeyHere");
7
+ } catch (e) {
8
+ if (e.code === DOMException.QUOTA_EXCEEDED_ERR && storage.length === 0) {
9
+ return true;
10
+ }
11
+ }
12
+
13
+ return false;
14
+ }
15
+
16
+ async function isEdgePrivateBrowsing() {
17
+ return !window.indexedDB;
18
+ }
19
+
20
+ async function isFirefoxPrivateBrowsing() {
21
+ return new Promise<boolean>((resolve) => {
22
+ const db = indexedDB.open("test");
23
+ db.onerror = () => resolve(true);
24
+ db.onsuccess = () => resolve(false);
25
+ });
26
+ }
27
+
28
+ async function isPrivateBrowsing() {
29
+ return (
30
+ (await isFirefoxPrivateBrowsing()) ||
31
+ (await isEdgePrivateBrowsing()) ||
32
+ (await isSafariPrivateBrowsing())
33
+ );
34
+ }
35
+
36
+ export type OfflineMode = "on" | "off" | "unavailable";
37
+
38
+ export interface OfflineModeResult {
39
+ current: OfflineMode;
40
+ notAvailable: boolean;
41
+ active: boolean;
42
+ }
43
+
44
+ const offlineModeStorageKey = "openmrs3:offline-mode";
45
+ let offlineMode: OfflineMode = "unavailable";
46
+
47
+ export function getCurrentOfflineMode(): OfflineModeResult {
48
+ return {
49
+ current: offlineMode,
50
+ notAvailable: offlineMode === "unavailable",
51
+ active: offlineMode === "on",
52
+ };
53
+ }
54
+
55
+ export function setCurrentOfflineMode(mode: OfflineMode) {
56
+ if (offlineMode !== "unavailable" && mode !== "unavailable") {
57
+ localStorage.setItem(
58
+ offlineModeStorageKey,
59
+ mode === "on" ? "active" : "disabled"
60
+ );
61
+ offlineMode = mode;
62
+ }
63
+ }
64
+
65
+ export async function activateOfflineCapability() {
66
+ const isPrivate = await isPrivateBrowsing();
67
+
68
+ if (!isPrivate) {
69
+ if (localStorage.getItem(offlineModeStorageKey) === "active") {
70
+ offlineMode = "on";
71
+ } else {
72
+ offlineMode = "off";
73
+ }
74
+ }
75
+
76
+ if (navigator.onLine && offlineMode === "on") {
77
+ //TODO trigger here --> update cycle
78
+ }
79
+ }
package/src/sync.ts CHANGED
@@ -2,11 +2,16 @@ import Dexie, { Table } from "dexie";
2
2
  import { getLoggedInUser } from "@openmrs/esm-api";
3
3
  import { createGlobalStore } from "@openmrs/esm-state";
4
4
 
5
- export interface SyncItem {
5
+ /**
6
+ * Defines an item queued up in the offline synchronization queue.
7
+ * A `SyncItem` contains both meta information about the item in the sync queue, as well as the
8
+ * actual data to be synchronized (i.e. the item's `content`).
9
+ */
10
+ export interface SyncItem<T = any> {
6
11
  id?: number;
7
12
  userId: string;
8
13
  type: string;
9
- content: any;
14
+ content: T;
10
15
  createdOn: Date;
11
16
  descriptor: QueueItemDescriptor;
12
17
  lastError?: {
@@ -15,6 +20,12 @@ export interface SyncItem {
15
20
  };
16
21
  }
17
22
 
23
+ /**
24
+ * Contains information about the sync item which has been provided externally by the caller
25
+ * who added the item to the queue.
26
+ * This information is all optional, but, when provided while enqueuing the item, can be used in other
27
+ * locations to better represent the sync item, e.g. in the UI.
28
+ */
18
29
  export interface QueueItemDescriptor {
19
30
  id?: string;
20
31
  dependencies?: Array<{
@@ -25,19 +36,69 @@ export interface QueueItemDescriptor {
25
36
  displayName?: string;
26
37
  }
27
38
 
39
+ /**
40
+ * A function which, when invoked, performs the actual client-server synchronization of the given
41
+ * `item` (which is the actual data to be synchronized).
42
+ * The function receives additional `options` which provide additional data that can be used
43
+ * for synchronizing.
44
+ */
28
45
  export type ProcessSyncItem<T> = (
29
46
  item: T,
30
47
  options: SyncProcessOptions<T>
31
48
  ) => Promise<any>;
32
49
 
33
- interface SyncResultBag {
34
- [type: string]: Record<string, any>;
50
+ /**
51
+ * Additional data which can be used for synchronizing data in a {@link ProcessSyncItem} function.
52
+ */
53
+ export interface SyncProcessOptions<T> {
54
+ abort: AbortController;
55
+ userId: string;
56
+ index: number;
57
+ items: Array<T>;
58
+ dependencies: Array<any>;
35
59
  }
36
60
 
61
+ /**
62
+ * Defines additional options which can optionally be provided when setting up a synchronization callback
63
+ * for a specific synchronization item type.
64
+ * These are not required, but, when set, allow further
65
+ */
66
+ interface SetupOfflineSyncOptions<T> {
67
+ /**
68
+ * Invoked when the user requests to edit a sync item.
69
+ * The typical behavior for such a callback is to launch a UI which allows editing the content
70
+ * encapsulated by the sync item.
71
+ * @param syncItem The sync item to be edited.
72
+ */
73
+ onBeginEditSyncItem?(syncItem: SyncItem<T>): void;
74
+ }
75
+
76
+ /**
77
+ * Represents a synchronization handler which has been globally registered by the
78
+ * {@link setupOfflineSync} function.
79
+ * These handlers are used for synchronizing queued data.
80
+ */
37
81
  interface SyncHandler {
38
- type: string;
39
- dependsOn: Array<string>;
40
- process: ProcessSyncItem<unknown>;
82
+ readonly type: string;
83
+ readonly dependsOn: ReadonlyArray<string>;
84
+ readonly process: ProcessSyncItem<unknown>;
85
+ readonly options: Readonly<SetupOfflineSyncOptions<any>>;
86
+ }
87
+
88
+ /**
89
+ * Represents the data inside the global offline synchronization store.
90
+ * Provides information about a currently ongoing synchronization.
91
+ */
92
+ export interface OfflineSynchronizationStore {
93
+ synchronization?: {
94
+ totalCount: number;
95
+ pendingCount: number;
96
+ abortController: AbortController;
97
+ };
98
+ }
99
+
100
+ interface SyncResultBag {
101
+ [type: string]: Record<string, any>;
41
102
  }
42
103
 
43
104
  class OfflineDb extends Dexie {
@@ -64,14 +125,6 @@ export function getOfflineDb() {
64
125
  return db;
65
126
  }
66
127
 
67
- export interface OfflineSynchronizationStore {
68
- synchronization?: {
69
- totalCount: number;
70
- pendingCount: number;
71
- abortController: AbortController;
72
- };
73
- }
74
-
75
128
  const syncStore = createGlobalStore<OfflineSynchronizationStore>(
76
129
  "offline-synchronization",
77
130
  {}
@@ -81,6 +134,9 @@ export function getOfflineSynchronizationStore() {
81
134
  return syncStore;
82
135
  }
83
136
 
137
+ /**
138
+ * Runs a full synchronization of **all** queued synchronization items.
139
+ */
84
140
  export async function runSynchronization() {
85
141
  if (syncStore.getState().synchronization) {
86
142
  return;
@@ -192,6 +248,13 @@ async function getUserId() {
192
248
  return user?.uuid || "*";
193
249
  }
194
250
 
251
+ /**
252
+ * Enqueues a new item in the sync queue for a specific user.
253
+ * @param userId The user with whom the sync item should be associated with.
254
+ * @param type The identifying type of the synchronization item.
255
+ * @param content The actual data to be synchronized.
256
+ * @param descriptor An optional descriptor providing additional metadata about the sync item.
257
+ */
195
258
  export async function queueSynchronizationItemFor<T>(
196
259
  userId: string,
197
260
  type: string,
@@ -220,6 +283,12 @@ export async function queueSynchronizationItemFor<T>(
220
283
  return id;
221
284
  }
222
285
 
286
+ /**
287
+ * Enqueues a new item in the sync queue and associates the item with the currently signed in user.
288
+ * @param type The identifying type of the synchronization item.
289
+ * @param content The actual data to be synchronized.
290
+ * @param descriptor An optional descriptor providing additional metadata about the sync item.
291
+ */
223
292
  export async function queueSynchronizationItem<T>(
224
293
  type: string,
225
294
  content: T,
@@ -229,6 +298,11 @@ export async function queueSynchronizationItem<T>(
229
298
  return await queueSynchronizationItemFor(userId, type, content, descriptor);
230
299
  }
231
300
 
301
+ /**
302
+ * Returns all currently queued up sync items of a given user.
303
+ * @param userId The ID of the user whose synchronization items should be returned.
304
+ * @param type The identifying type of the synchronization items to be returned..
305
+ */
232
306
  export async function getSynchronizationItemsFor<T>(
233
307
  userId: string,
234
308
  type: string
@@ -243,31 +317,85 @@ export async function getSynchronizationItemsFor<T>(
243
317
  return items;
244
318
  }
245
319
 
320
+ /**
321
+ * Returns all currently queued up sync items of the currently signed in user.
322
+ * @param type The identifying type of the synchronization items to be returned.
323
+ */
246
324
  export async function getSynchronizationItems<T>(type: string) {
247
325
  const userId = await getUserId();
248
326
  return await getSynchronizationItemsFor<T>(userId, type);
249
327
  }
250
328
 
251
- export async function deleteSynchronizationItem(id: number) {
252
- await db.syncQueue.delete(id);
329
+ /**
330
+ * Returns a queued sync item with the given ID or `undefined` if no such item exists.
331
+ * @param id The ID of the requested sync item.
332
+ */
333
+ export async function getSynchronizationItem<T = any>(
334
+ id: number
335
+ ): Promise<SyncItem<T> | undefined> {
336
+ return await db.syncQueue.get(id);
253
337
  }
254
338
 
255
- export interface SyncProcessOptions<T> {
256
- abort: AbortController;
257
- userId: string;
258
- index: number;
259
- items: Array<T>;
260
- dependencies: Array<any>;
339
+ /**
340
+ * Returns whether editing synchronization items of the given type is supported by the currently
341
+ * registered synchronization handlers.
342
+ * @param type The identifying type of the synchronization item which should be edited.
343
+ */
344
+ export function canBeginEditSynchronizationItemsOfType(type: string) {
345
+ // Editing an item can be requested as long as callback for this flow exists on the associated handler.
346
+ return !!handlers[type]?.options.onBeginEditSyncItem;
347
+ }
348
+
349
+ /**
350
+ * Triggers an edit flow for the given synchronization item.
351
+ * If this is not possible, throws an error.
352
+ * @param id The ID of the synchronization item to be edited.
353
+ */
354
+ export async function beginEditSynchronizationItem(id: number) {
355
+ const item = await getSynchronizationItem(id);
356
+ if (!item) {
357
+ throw new Error(`No sync item with the ID ${id} exists.`);
358
+ }
359
+
360
+ const editCallback = handlers[item.type]?.options.onBeginEditSyncItem;
361
+ if (!editCallback) {
362
+ throw new Error(
363
+ `A sync item with the ID ${id} exists, but the associated handler (if one exists) doesn't support editing the item. You can avoid this error by either verifying that sync items of this type can be edited via the "canEditSynchronizationItemsOfType(type: string)" function or alternatively ensure that the synchronizaton handler for sync items of type "${item.type}" supports editing items.`
364
+ );
365
+ }
366
+
367
+ editCallback(item);
368
+ }
369
+
370
+ /**
371
+ * Deletes a queued up sync item with the given ID.
372
+ * @param id The ID of the synchronization item to be deleted.
373
+ */
374
+ export async function deleteSynchronizationItem(id: number) {
375
+ await db.syncQueue.delete(id);
261
376
  }
262
377
 
378
+ /**
379
+ * Registers a new synchronization handler which is able to synchronize data of a specific type.
380
+ * @param type The identifying type of the synchronization items which can be handled by this handler.
381
+ * @param dependsOn An array of other sync item types which must be synchronized before this handler
382
+ * can synchronize its own data. Items of these types are effectively dependencies of the data
383
+ * synchronized by this handler.
384
+ * @param process A function which, when invoked, performs the actual client-server synchronization of the given
385
+ * `item` (which is the actual data to be synchronized).
386
+ * @param options Additional options which can optionally be provided when setting up a synchronization callback
387
+ * for a specific synchronization item type.
388
+ */
263
389
  export function setupOfflineSync<T>(
264
390
  type: string,
265
391
  dependsOn: Array<string>,
266
- process: ProcessSyncItem<T>
392
+ process: ProcessSyncItem<T>,
393
+ options: SetupOfflineSyncOptions<T> = {}
267
394
  ) {
268
395
  handlers[type] = {
269
396
  type,
270
397
  dependsOn,
271
398
  process,
399
+ options,
272
400
  };
273
401
  }