@overmap-ai/core 1.0.53-fix-outbox.1 → 1.0.53-fix-outbox.3

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.
@@ -10,6 +10,7 @@ import React__default, { useState, useEffect, useRef, memo, useMemo, useCallback
10
10
  import { jsx, jsxs, Fragment } from "react/jsx-runtime";
11
11
  import { unsafeShowToast, AlertDialogProvider, ToastProvider, DefaultTheme, Flex as Flex$1, IconButton, RiIcon, Text as Text$1, useSeverityColor, TextArea, Select, useToast, Badge, MultiSelect, Overlay, Button, Spinner, useViewportSize, ButtonGroup, IconColorUtility, Tooltip, Popover, useSize, ToggleButton, Separator, OvermapItem, ButtonList, divButtonProps, OvermapDropdownMenu, Checkbox as Checkbox$1, Input, useAlertDialog } from "@overmap-ai/blocks";
12
12
  import { DepGraph } from "dependency-graph";
13
+ import saveAs$1, { saveAs } from "file-saver";
13
14
  import { offline as offline$1 } from "@redux-offline/redux-offline";
14
15
  import offlineConfig from "@redux-offline/redux-offline/lib/defaults";
15
16
  import localforage from "localforage";
@@ -17,7 +18,6 @@ import createMigration from "redux-persist-migrate";
17
18
  import { createSlice, createSelector, combineReducers, configureStore, createNextState } from "@reduxjs/toolkit";
18
19
  import request from "superagent";
19
20
  import { shallowEqual as shallowEqual$1, useDispatch, useSelector } from "react-redux";
20
- import saveAs$1, { saveAs } from "file-saver";
21
21
  import { v4 } from "uuid";
22
22
  import ColorCls from "color";
23
23
  import jwtDecode from "jwt-decode";
@@ -36,6 +36,354 @@ import "react-pdf/dist/Page/TextLayer.css";
36
36
  import { ReactSketchCanvas } from "react-sketch-canvas";
37
37
  import set from "lodash.set";
38
38
  import cloneDeep from "lodash.clonedeep";
39
+ class OutboxCoordinator {
40
+ constructor() {
41
+ __publicField(this, "graph");
42
+ __publicField(this, "requestAttemptCounter");
43
+ this.graph = new DepGraph();
44
+ this.requestAttemptCounter = {};
45
+ }
46
+ /**
47
+ * Used when the app is loaded. Reconstructs the dependency graph based on an outbox from the redux-offline store.
48
+ */
49
+ static _fromOutbox(outbox) {
50
+ const ret = new OutboxCoordinator();
51
+ for (let i = 0; i < outbox.length; i++) {
52
+ const outboxItem = outbox[i];
53
+ if (!outboxItem) {
54
+ console.error("Outbox item was undefined");
55
+ continue;
56
+ }
57
+ ret.sneakRequest(outboxItem);
58
+ for (let j = 0; j < i; j++) {
59
+ const previousOutboxItem = outbox[j];
60
+ if (!previousOutboxItem) {
61
+ console.error("Previous outbox item was undefined");
62
+ continue;
63
+ }
64
+ if (previousOutboxItem.payload.uuid === outboxItem.payload.uuid) {
65
+ continue;
66
+ }
67
+ if (previousOutboxItem.payload.blocks.some((block) => outboxItem.payload.blockers.includes(block))) {
68
+ OutboxCoordinator._addDependency(
69
+ outboxItem.payload.uuid,
70
+ previousOutboxItem.payload.uuid,
71
+ ret.graph
72
+ );
73
+ }
74
+ }
75
+ }
76
+ return ret;
77
+ }
78
+ _addDependency(from, to) {
79
+ OutboxCoordinator._addDependency(from, to, this.graph);
80
+ }
81
+ static _addDependency(from, to, graph) {
82
+ if (from === to) {
83
+ throw new Error(`Tried to add dependency from node to itself: ${from}`);
84
+ }
85
+ const fromExists = graph.hasNode(from);
86
+ if (!fromExists) {
87
+ throw new Error(`Tried to add dependency from non-existent node: ${from} (to node: ${to})`);
88
+ }
89
+ const toExists = graph.hasNode(to);
90
+ if (!toExists) {
91
+ throw new Error(`Tried to add dependency to non-existent node: ${to} (from node: ${from})`);
92
+ }
93
+ graph.addDependency(from, to);
94
+ }
95
+ /**
96
+ * If there are one or more nodes in the graph, we find all nodes that match a dependency of the request and add a
97
+ * dependency from the new request node to that node.
98
+ */
99
+ addRequest(request2) {
100
+ this.graph.addNode(request2.payload.uuid, request2);
101
+ if (request2.payload.blockers.length === 0 || this.graph.size() === 1) {
102
+ return;
103
+ }
104
+ for (const node of this.graph.overallOrder()) {
105
+ if (node === request2.payload.uuid)
106
+ continue;
107
+ const details = this.graph.getNodeData(node);
108
+ if (request2.payload.blockers.some((blocker) => details.payload.blocks.includes(blocker))) {
109
+ this._addDependency(request2.payload.uuid, node);
110
+ }
111
+ }
112
+ }
113
+ /**
114
+ * Inserts a request at the beginning of the queue. This could be used for requests that were popped, then failed,
115
+ * and need to be re-enqueued at the front of the queue. Any requests that were previously blocked by this request
116
+ * will be blocked by this request again. No blockers will be added to this request because it is assumed that the
117
+ * request was already unblocked when it was popped.
118
+ * @param request The request to insert at the beginning of the queue.
119
+ */
120
+ insertRequest(request2) {
121
+ this.graph.addNode(request2.payload.uuid, request2);
122
+ for (const node of this.graph.overallOrder()) {
123
+ if (node === request2.payload.uuid)
124
+ continue;
125
+ const details = this.graph.getNodeData(node);
126
+ if (details.payload.blockers.some((blocker) => request2.payload.blocks.includes(blocker))) {
127
+ this._addDependency(node, request2.payload.uuid);
128
+ }
129
+ }
130
+ }
131
+ /**
132
+ * Sneaks a request into the dependency graph without creating any blockers. Useful for reconstructing the graph
133
+ * (from the offline store's outbox state) when the app is loaded.
134
+ */
135
+ sneakRequest(request2) {
136
+ this.graph.addNode(request2.payload.uuid, request2);
137
+ }
138
+ /**
139
+ * Returns the next node in line to be sent. This is the unblocked node with the fewest number of attempts. If there
140
+ * are multiple nodes with the same number of attempts, the first one (by insertion order) will be returned.
141
+ */
142
+ _getNextNode() {
143
+ const leafNodes = this.graph.overallOrder(true);
144
+ let minAttempts = Infinity;
145
+ let minAttemptsNode;
146
+ for (const node of leafNodes) {
147
+ const attempts = this.requestAttemptCounter[node] || 0;
148
+ if (attempts < minAttempts) {
149
+ minAttempts = attempts;
150
+ minAttemptsNode = node;
151
+ }
152
+ }
153
+ return minAttemptsNode;
154
+ }
155
+ /**
156
+ * Returns the next request in line to be sent without removing it.
157
+ */
158
+ peek() {
159
+ const nextNode = this._getNextNode();
160
+ if (!nextNode)
161
+ return void 0;
162
+ return this.graph.getNodeData(nextNode);
163
+ }
164
+ /**
165
+ * Removes a request from the graph. This should be called when a request is successfully sent.
166
+ * @param uuid The UUID of the request to remove.
167
+ */
168
+ remove(uuid) {
169
+ this.graph.removeNode(uuid);
170
+ delete this.requestAttemptCounter[uuid];
171
+ }
172
+ /**
173
+ * Returns the next request in line to be sent and removes it from the graph.
174
+ */
175
+ pop() {
176
+ const nextRequestDetails = this.peek();
177
+ if (nextRequestDetails) {
178
+ this.graph.removeNode(nextRequestDetails.payload.uuid);
179
+ }
180
+ return nextRequestDetails;
181
+ }
182
+ /**
183
+ * Gets the current queue for the outbox. Should be called to get a new value for the outbox slice every time a
184
+ * request is enqueued. It will be used to render the outbox items in a single lane.
185
+ */
186
+ getQueue() {
187
+ const ret = this.graph.overallOrder().map((nodeName) => this.graph.getNodeData(nodeName));
188
+ const nextNode = this._getNextNode();
189
+ if (nextNode) {
190
+ const nextRequestDetails = this.graph.getNodeData(nextNode);
191
+ const nextRequestIndex = ret.findIndex(
192
+ (request2) => request2.payload.uuid === nextRequestDetails.payload.uuid
193
+ );
194
+ if (nextRequestIndex !== -1) {
195
+ ret.splice(nextRequestIndex, 1);
196
+ ret.unshift(nextRequestDetails);
197
+ }
198
+ }
199
+ return ret;
200
+ }
201
+ /**
202
+ * Gets a list of requests that can currently be sent (requests with no unresolved dependencies). Used to process
203
+ * the next ready request in case the request at the front of the queue is failing.
204
+ */
205
+ getReady() {
206
+ let ret = this.graph.overallOrder(true).map((nodeName) => this.graph.getNodeData(nodeName));
207
+ ret = ret.sort((a, b) => {
208
+ return a.meta.offline.effect.timestamp.localeCompare(b.meta.offline.effect.timestamp);
209
+ });
210
+ ret = ret.sort((a, b) => {
211
+ const aAttempts = this.requestAttemptCounter[a.payload.uuid] || 0;
212
+ const bAttempts = this.requestAttemptCounter[b.payload.uuid] || 0;
213
+ return aAttempts - bAttempts;
214
+ });
215
+ return ret;
216
+ }
217
+ registerRetry(uuid) {
218
+ this.requestAttemptCounter[uuid] = (this.requestAttemptCounter[uuid] || 0) + 1;
219
+ }
220
+ }
221
+ const UNKNOWN_ERROR_MESSAGE = "An unknown error occurred";
222
+ const MAX_ERROR_MESSAGE_LENGTH = 500;
223
+ const _SPECIAL_KEYS = ["non_field_errors", "detail"];
224
+ function extractErrorMessage(errorRes, err) {
225
+ let ret;
226
+ if (errorRes == null ? void 0 : errorRes.body) {
227
+ if (typeof errorRes.body === "object") {
228
+ const responseBody = errorRes.body;
229
+ if (typeof responseBody.error === "string") {
230
+ ret = responseBody.error;
231
+ } else if (typeof responseBody.message === "string") {
232
+ ret = responseBody.message;
233
+ } else if (responseBody.body) {
234
+ try {
235
+ ret = Object.entries(responseBody.body).map(([key, value]) => {
236
+ if (typeof value === "string") {
237
+ if (_SPECIAL_KEYS.includes(key))
238
+ return value;
239
+ return `${key}: ${value}`;
240
+ }
241
+ if (Array.isArray(value)) {
242
+ if (_SPECIAL_KEYS.includes(key))
243
+ return value.join("\n");
244
+ return value.map((v) => `${key}: ${v}`).join("\n");
245
+ }
246
+ return `${key}: ${JSON.stringify(value)}`;
247
+ }).join("\n");
248
+ } catch (e) {
249
+ console.error("Failed to extract error message from response body", e);
250
+ }
251
+ }
252
+ } else if (typeof errorRes.body === "string") {
253
+ ret = errorRes.body;
254
+ }
255
+ } else if (errorRes == null ? void 0 : errorRes.text) {
256
+ ret = errorRes.text;
257
+ } else if (err instanceof Error) {
258
+ ret = err.message;
259
+ }
260
+ if (!ret || ret.length > MAX_ERROR_MESSAGE_LENGTH) {
261
+ return UNKNOWN_ERROR_MESSAGE;
262
+ }
263
+ return ret;
264
+ }
265
+ class APIError extends Error {
266
+ constructor(options) {
267
+ super(UNKNOWN_ERROR_MESSAGE);
268
+ // NOTE: Needs to conform to NetworkError in @redux-offline/redux-offline, which has `status` and `response`.
269
+ __publicField(this, "status");
270
+ __publicField(this, "response");
271
+ __publicField(this, "message");
272
+ __publicField(this, "options");
273
+ const { response, innerError } = options;
274
+ this.message = options.message ?? extractErrorMessage(response, innerError) ?? UNKNOWN_ERROR_MESSAGE;
275
+ this.status = (response == null ? void 0 : response.status) ?? 0;
276
+ this.response = response;
277
+ options.discard = options.discard ?? false;
278
+ this.options = options;
279
+ }
280
+ }
281
+ class BaseApiService {
282
+ constructor(sdk) {
283
+ __publicField(this, "client");
284
+ this.client = sdk;
285
+ }
286
+ }
287
+ function hex(buffer) {
288
+ const hashArray = new Uint8Array(buffer);
289
+ return hashArray.reduce((data, byte) => data + byte.toString(16).padStart(2, "0"), "");
290
+ }
291
+ const getFileS3Key = async (file, hash) => {
292
+ if (!hash) {
293
+ hash = await hashFile(file);
294
+ }
295
+ let fileType = file.type;
296
+ if (fileType.includes("/")) {
297
+ fileType = fileType.split("/")[1];
298
+ }
299
+ if (!fileType) {
300
+ throw new Error(`Could not extract file type from ${file.type}`);
301
+ }
302
+ return `${hash}.${fileType}`;
303
+ };
304
+ function hashFile(file) {
305
+ return new Promise((resolve, reject) => {
306
+ const reader = new FileReader();
307
+ reader.onload = () => {
308
+ const fileResult = reader.result;
309
+ if (!fileResult) {
310
+ reject();
311
+ return;
312
+ }
313
+ void crypto.subtle.digest("SHA-1", fileResult).then((hash) => {
314
+ const sha1result = hex(hash);
315
+ resolve(sha1result);
316
+ });
317
+ };
318
+ reader.readAsArrayBuffer(file);
319
+ });
320
+ }
321
+ function getFileIdentifier(file) {
322
+ if (!file.name || !file.type || !file.size) {
323
+ const message = "File has no name, type, and/or size";
324
+ console.error(`${message}`, file);
325
+ throw new Error(`${message}.`);
326
+ }
327
+ return `${file.name}&${file.type}${file.size}`;
328
+ }
329
+ function getRenamedFile(file, newName) {
330
+ return new File([file], newName, { type: file.type });
331
+ }
332
+ function downloadInMemoryFile(filename, text) {
333
+ const element = document.createElement("a");
334
+ element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text));
335
+ element.setAttribute("download", filename);
336
+ element.style.display = "none";
337
+ document.body.appendChild(element);
338
+ element.click();
339
+ document.body.removeChild(element);
340
+ }
341
+ const constructUploadedFilePayloads = async (files) => {
342
+ const filePayloads = {};
343
+ for (const file of files) {
344
+ const sha1 = await hashFile(file);
345
+ filePayloads[sha1] = {
346
+ sha1,
347
+ extension: file.name.split(".").pop() || "",
348
+ file_type: file.type,
349
+ size: file.size
350
+ };
351
+ }
352
+ return Object.values(filePayloads);
353
+ };
354
+ const fileToBlob = async (dataUrl) => {
355
+ return (await fetch(dataUrl)).blob();
356
+ };
357
+ const blobToBase64 = (blob) => {
358
+ return new Promise((resolve, _) => {
359
+ const reader = new FileReader();
360
+ reader.onloadend = () => {
361
+ var _a2;
362
+ resolve(((_a2 = reader.result) == null ? void 0 : _a2.toString()) || "");
363
+ };
364
+ reader.readAsDataURL(blob);
365
+ });
366
+ };
367
+ const useFileSrc = (props) => {
368
+ const { file, fileSha1, placeholder } = props;
369
+ const [src, setSrc] = useState(placeholder);
370
+ const { sdk } = useSDK();
371
+ useEffect(() => {
372
+ if (!fileSha1 || !file)
373
+ return;
374
+ sdk.files.fetchFileFromUrl(file, fileSha1).then((file2) => {
375
+ setSrc(URL.createObjectURL(file2));
376
+ }).catch((reason) => {
377
+ console.error(`Failed to fetch file ${file} (${fileSha1}):
378
+ `, reason);
379
+ });
380
+ }, [file, fileSha1, sdk.files]);
381
+ return src;
382
+ };
383
+ function downloadFile(file) {
384
+ const blob = new Blob([file]);
385
+ saveAs(blob, file.name);
386
+ }
39
387
  const global$1 = typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {};
40
388
  function defaultSetTimout() {
41
389
  throw new Error("setTimeout has not been defined");
@@ -512,108 +860,8 @@ function classNames$1(...args) {
512
860
  }
513
861
  }
514
862
  }
515
- }
516
- return classes.join(" ");
517
- }
518
- function hex(buffer) {
519
- const hashArray = new Uint8Array(buffer);
520
- return hashArray.reduce((data, byte) => data + byte.toString(16).padStart(2, "0"), "");
521
- }
522
- const getFileS3Key = async (file, hash) => {
523
- if (!hash) {
524
- hash = await hashFile(file);
525
- }
526
- let fileType = file.type;
527
- if (fileType.includes("/")) {
528
- fileType = fileType.split("/")[1];
529
- }
530
- if (!fileType) {
531
- throw new Error(`Could not extract file type from ${file.type}`);
532
- }
533
- return `${hash}.${fileType}`;
534
- };
535
- function hashFile(file) {
536
- return new Promise((resolve, reject) => {
537
- const reader = new FileReader();
538
- reader.onload = () => {
539
- const fileResult = reader.result;
540
- if (!fileResult) {
541
- reject();
542
- return;
543
- }
544
- void crypto.subtle.digest("SHA-1", fileResult).then((hash) => {
545
- const sha1result = hex(hash);
546
- resolve(sha1result);
547
- });
548
- };
549
- reader.readAsArrayBuffer(file);
550
- });
551
- }
552
- function getFileIdentifier(file) {
553
- if (!file.name || !file.type || !file.size) {
554
- const message = "File has no name, type, and/or size";
555
- console.error(`${message}`, file);
556
- throw new Error(`${message}.`);
557
- }
558
- return `${file.name}&${file.type}${file.size}`;
559
- }
560
- function getRenamedFile(file, newName) {
561
- return new File([file], newName, { type: file.type });
562
- }
563
- function downloadInMemoryFile(filename, text) {
564
- const element = document.createElement("a");
565
- element.setAttribute("href", "data:text/plain;charset=utf-8," + encodeURIComponent(text));
566
- element.setAttribute("download", filename);
567
- element.style.display = "none";
568
- document.body.appendChild(element);
569
- element.click();
570
- document.body.removeChild(element);
571
- }
572
- const constructUploadedFilePayloads = async (files) => {
573
- const filePayloads = {};
574
- for (const file of files) {
575
- const sha1 = await hashFile(file);
576
- filePayloads[sha1] = {
577
- sha1,
578
- extension: file.name.split(".").pop() || "",
579
- file_type: file.type,
580
- size: file.size
581
- };
582
- }
583
- return Object.values(filePayloads);
584
- };
585
- const fileToBlob = async (dataUrl) => {
586
- return (await fetch(dataUrl)).blob();
587
- };
588
- const blobToBase64 = (blob) => {
589
- return new Promise((resolve, _) => {
590
- const reader = new FileReader();
591
- reader.onloadend = () => {
592
- var _a2;
593
- resolve(((_a2 = reader.result) == null ? void 0 : _a2.toString()) || "");
594
- };
595
- reader.readAsDataURL(blob);
596
- });
597
- };
598
- const useFileSrc = (props) => {
599
- const { file, fileSha1, placeholder } = props;
600
- const [src, setSrc] = useState(placeholder);
601
- const { sdk } = useSDK();
602
- useEffect(() => {
603
- if (!fileSha1 || !file)
604
- return;
605
- sdk.files.fetchFileFromUrl(file, fileSha1).then((file2) => {
606
- setSrc(URL.createObjectURL(file2));
607
- }).catch((reason) => {
608
- console.error(`Failed to fetch file ${file} (${fileSha1}):
609
- `, reason);
610
- });
611
- }, [file, fileSha1, sdk.files]);
612
- return src;
613
- };
614
- function downloadFile(file) {
615
- const blob = new Blob([file]);
616
- saveAs(blob, file.name);
863
+ }
864
+ return classes.join(" ");
617
865
  }
618
866
  const logCache = {};
619
867
  function logOnlyOnce(logId, objId, level, ...args) {
@@ -4393,9 +4641,17 @@ const rootReducer = (state, action) => {
4393
4641
  return overmapReducer(mutatedState, action);
4394
4642
  };
4395
4643
  let __OUTBOX_COORDINATOR = null;
4396
- function _getOutboxCoordinator() {
4644
+ function getOutboxCoordinator() {
4397
4645
  if (!__OUTBOX_COORDINATOR) {
4398
- __OUTBOX_COORDINATOR = new OutboxCoordinator();
4646
+ const clientStore2 = getClientStore();
4647
+ if (!clientStore2) {
4648
+ console.error(
4649
+ "Cannot reload outbox because there is no client store yet. If there is an outbox, it will be broken."
4650
+ );
4651
+ return new OutboxCoordinator();
4652
+ }
4653
+ const outbox = clientStore2.getState().offline.outbox;
4654
+ __OUTBOX_COORDINATOR = OutboxCoordinator._fromOutbox(outbox);
4399
4655
  }
4400
4656
  return __OUTBOX_COORDINATOR;
4401
4657
  }
@@ -4410,12 +4666,12 @@ const persistCallback = (err) => {
4410
4666
  }
4411
4667
  };
4412
4668
  const enqueue = (_array, item, _context) => {
4413
- const coordinator = _getOutboxCoordinator();
4669
+ const coordinator = getOutboxCoordinator();
4414
4670
  coordinator.addRequest(item);
4415
4671
  return coordinator.getQueue();
4416
4672
  };
4417
4673
  const dequeue = (_array, item, _context) => {
4418
- const coordinator = _getOutboxCoordinator();
4674
+ const coordinator = getOutboxCoordinator();
4419
4675
  const meta = item.meta;
4420
4676
  const uuid = meta.offlineAction.payload.uuid;
4421
4677
  coordinator.remove(uuid);
@@ -4707,7 +4963,7 @@ function discard(reason, action, retries = 0) {
4707
4963
  const uuid = action.payload.uuid;
4708
4964
  function rollbackAndThrow() {
4709
4965
  clientStore2.dispatch(markAsDeleted(uuid));
4710
- _getOutboxCoordinator().remove(action.payload.uuid);
4966
+ getOutboxCoordinator().remove(action.payload.uuid);
4711
4967
  const rollbackAction = action.meta.offline.rollback;
4712
4968
  if (rollbackAction) {
4713
4969
  console.warn("Rolling back request due to SDK error:", action);
@@ -4738,21 +4994,17 @@ function discard(reason, action, retries = 0) {
4738
4994
  console.error(`Could not display toast for status ${status} because there is no toast handle.`);
4739
4995
  }
4740
4996
  }
4741
- _getOutboxCoordinator().remove(action.payload.uuid);
4997
+ getOutboxCoordinator().remove(action.payload.uuid);
4742
4998
  reason.options.discard = true;
4743
4999
  rollbackAndThrow();
4744
5000
  }
4745
5001
  }
4746
5002
  console.debug("Registering a retry for request:", action.payload.uuid);
4747
- _getOutboxCoordinator().registerRetry(action.payload.uuid);
5003
+ getOutboxCoordinator().registerRetry(action.payload.uuid);
4748
5004
  return false;
4749
5005
  }
4750
5006
  function peek(_array, _item, _context) {
4751
- const ret = _getOutboxCoordinator().peek();
4752
- if (!ret) {
4753
- throw new Error(`Peek returned ${ret}`);
4754
- }
4755
- return ret;
5007
+ return getOutboxCoordinator().peek();
4756
5008
  }
4757
5009
  function retry(_action, _retries) {
4758
5010
  getClientStore().dispatch(_setLatestRetryTime((/* @__PURE__ */ new Date()).getTime()));
@@ -4760,258 +5012,6 @@ function retry(_action, _retries) {
4760
5012
  }
4761
5013
  const useAppDispatch = () => useDispatch();
4762
5014
  const useAppSelector = useSelector;
4763
- class OutboxCoordinator {
4764
- constructor() {
4765
- __publicField(this, "graph");
4766
- __publicField(this, "requestAttemptCounter");
4767
- this.graph = new DepGraph();
4768
- this.requestAttemptCounter = {};
4769
- }
4770
- /**
4771
- * Used when the app is loaded. Reconstructs the dependency graph based on an outbox from the redux-offline store.
4772
- */
4773
- static fromOutbox(outbox, client) {
4774
- const ret = new OutboxCoordinator();
4775
- for (let i = 0; i < outbox.length; i++) {
4776
- const outboxItem = outbox[i];
4777
- if (!outboxItem) {
4778
- console.error("Outbox item was undefined");
4779
- continue;
4780
- }
4781
- ret.sneakRequest(outboxItem);
4782
- for (let j = 0; j < i; j++) {
4783
- const previousOutboxItem = outbox[j];
4784
- if (!previousOutboxItem) {
4785
- console.error("Previous outbox item was undefined");
4786
- continue;
4787
- }
4788
- if (previousOutboxItem.payload.uuid === outboxItem.payload.uuid) {
4789
- continue;
4790
- }
4791
- if (previousOutboxItem.payload.blocks.some((block) => outboxItem.payload.blockers.includes(block))) {
4792
- OutboxCoordinator._addDependency(
4793
- outboxItem.payload.uuid,
4794
- previousOutboxItem.payload.uuid,
4795
- ret.graph
4796
- );
4797
- }
4798
- }
4799
- }
4800
- const firstRequest = ret.peek();
4801
- if (firstRequest) {
4802
- void performRequest(ret.pop(), client);
4803
- }
4804
- return ret;
4805
- }
4806
- _addDependency(from, to) {
4807
- OutboxCoordinator._addDependency(from, to, this.graph);
4808
- }
4809
- static _addDependency(from, to, graph) {
4810
- if (from === to) {
4811
- throw new Error(`Tried to add dependency from node to itself: ${from}`);
4812
- }
4813
- const fromExists = graph.hasNode(from);
4814
- if (!fromExists) {
4815
- throw new Error(`Tried to add dependency from non-existent node: ${from} (to node: ${to})`);
4816
- }
4817
- const toExists = graph.hasNode(to);
4818
- if (!toExists) {
4819
- throw new Error(`Tried to add dependency to non-existent node: ${to} (from node: ${from})`);
4820
- }
4821
- graph.addDependency(from, to);
4822
- }
4823
- /**
4824
- * If there are one or more nodes in the graph, we find all nodes that match a dependency of the request and add a
4825
- * dependency from the new request node to that node.
4826
- */
4827
- addRequest(request2) {
4828
- this.graph.addNode(request2.payload.uuid, request2);
4829
- if (request2.payload.blockers.length === 0 || this.graph.size() === 1) {
4830
- return;
4831
- }
4832
- for (const node of this.graph.overallOrder()) {
4833
- if (node === request2.payload.uuid)
4834
- continue;
4835
- const details = this.graph.getNodeData(node);
4836
- if (request2.payload.blockers.some((blocker) => details.payload.blocks.includes(blocker))) {
4837
- this._addDependency(request2.payload.uuid, node);
4838
- }
4839
- }
4840
- }
4841
- /**
4842
- * Inserts a request at the beginning of the queue. This could be used for requests that were popped, then failed,
4843
- * and need to be re-enqueued at the front of the queue. Any requests that were previously blocked by this request
4844
- * will be blocked by this request again. No blockers will be added to this request because it is assumed that the
4845
- * request was already unblocked when it was popped.
4846
- * @param request The request to insert at the beginning of the queue.
4847
- */
4848
- insertRequest(request2) {
4849
- this.graph.addNode(request2.payload.uuid, request2);
4850
- for (const node of this.graph.overallOrder()) {
4851
- if (node === request2.payload.uuid)
4852
- continue;
4853
- const details = this.graph.getNodeData(node);
4854
- if (details.payload.blockers.some((blocker) => request2.payload.blocks.includes(blocker))) {
4855
- this._addDependency(node, request2.payload.uuid);
4856
- }
4857
- }
4858
- }
4859
- /**
4860
- * Sneaks a request into the dependency graph without creating any blockers. Useful for reconstructing the graph
4861
- * (from the offline store's outbox state) when the app is loaded.
4862
- */
4863
- sneakRequest(request2) {
4864
- this.graph.addNode(request2.payload.uuid, request2);
4865
- }
4866
- /**
4867
- * Returns the next node in line to be sent. This is the unblocked node with the fewest number of attempts. If there
4868
- * are multiple nodes with the same number of attempts, the first one (by insertion order) will be returned.
4869
- */
4870
- _getNextNode() {
4871
- const leafNodes = this.graph.overallOrder(true);
4872
- let minAttempts = Infinity;
4873
- let minAttemptsNode;
4874
- for (const node of leafNodes) {
4875
- const attempts = this.requestAttemptCounter[node] || 0;
4876
- if (attempts < minAttempts) {
4877
- minAttempts = attempts;
4878
- minAttemptsNode = node;
4879
- }
4880
- }
4881
- return minAttemptsNode;
4882
- }
4883
- /**
4884
- * Returns the next request in line to be sent without removing it.
4885
- */
4886
- peek() {
4887
- const nextNode = this._getNextNode();
4888
- if (!nextNode)
4889
- return void 0;
4890
- return this.graph.getNodeData(nextNode);
4891
- }
4892
- /**
4893
- * Removes a request from the graph. This should be called when a request is successfully sent.
4894
- * @param uuid The UUID of the request to remove.
4895
- */
4896
- remove(uuid) {
4897
- this.graph.removeNode(uuid);
4898
- delete this.requestAttemptCounter[uuid];
4899
- }
4900
- /**
4901
- * Returns the next request in line to be sent and removes it from the graph.
4902
- */
4903
- pop() {
4904
- const nextRequestDetails = this.peek();
4905
- if (nextRequestDetails) {
4906
- this.graph.removeNode(nextRequestDetails.payload.uuid);
4907
- }
4908
- return nextRequestDetails;
4909
- }
4910
- /**
4911
- * Gets the current queue for the outbox. Should be called to get a new value for the outbox slice every time a
4912
- * request is enqueued. It will be used to render the outbox items in a single lane.
4913
- */
4914
- getQueue() {
4915
- const ret = this.graph.overallOrder().map((nodeName) => this.graph.getNodeData(nodeName));
4916
- const nextNode = this._getNextNode();
4917
- if (nextNode) {
4918
- const nextRequestDetails = this.graph.getNodeData(nextNode);
4919
- const nextRequestIndex = ret.findIndex(
4920
- (request2) => request2.payload.uuid === nextRequestDetails.payload.uuid
4921
- );
4922
- if (nextRequestIndex !== -1) {
4923
- ret.splice(nextRequestIndex, 1);
4924
- ret.unshift(nextRequestDetails);
4925
- }
4926
- }
4927
- return ret;
4928
- }
4929
- /**
4930
- * Gets a list of requests that can currently be sent (requests with no unresolved dependencies). Used to process
4931
- * the next ready request in case the request at the front of the queue is failing.
4932
- */
4933
- getReady() {
4934
- let ret = this.graph.overallOrder(true).map((nodeName) => this.graph.getNodeData(nodeName));
4935
- ret = ret.sort((a, b) => {
4936
- return a.meta.offline.effect.timestamp.localeCompare(b.meta.offline.effect.timestamp);
4937
- });
4938
- ret = ret.sort((a, b) => {
4939
- const aAttempts = this.requestAttemptCounter[a.payload.uuid] || 0;
4940
- const bAttempts = this.requestAttemptCounter[b.payload.uuid] || 0;
4941
- return aAttempts - bAttempts;
4942
- });
4943
- return ret;
4944
- }
4945
- registerRetry(uuid) {
4946
- this.requestAttemptCounter[uuid] = (this.requestAttemptCounter[uuid] || 0) + 1;
4947
- }
4948
- }
4949
- const UNKNOWN_ERROR_MESSAGE = "An unknown error occurred";
4950
- const MAX_ERROR_MESSAGE_LENGTH = 500;
4951
- const _SPECIAL_KEYS = ["non_field_errors", "detail"];
4952
- function extractErrorMessage(errorRes, err) {
4953
- let ret;
4954
- if (errorRes == null ? void 0 : errorRes.body) {
4955
- if (typeof errorRes.body === "object") {
4956
- const responseBody = errorRes.body;
4957
- if (typeof responseBody.error === "string") {
4958
- ret = responseBody.error;
4959
- } else if (typeof responseBody.message === "string") {
4960
- ret = responseBody.message;
4961
- } else if (responseBody.body) {
4962
- try {
4963
- ret = Object.entries(responseBody.body).map(([key, value]) => {
4964
- if (typeof value === "string") {
4965
- if (_SPECIAL_KEYS.includes(key))
4966
- return value;
4967
- return `${key}: ${value}`;
4968
- }
4969
- if (Array.isArray(value)) {
4970
- if (_SPECIAL_KEYS.includes(key))
4971
- return value.join("\n");
4972
- return value.map((v) => `${key}: ${v}`).join("\n");
4973
- }
4974
- return `${key}: ${JSON.stringify(value)}`;
4975
- }).join("\n");
4976
- } catch (e) {
4977
- console.error("Failed to extract error message from response body", e);
4978
- }
4979
- }
4980
- } else if (typeof errorRes.body === "string") {
4981
- ret = errorRes.body;
4982
- }
4983
- } else if (errorRes == null ? void 0 : errorRes.text) {
4984
- ret = errorRes.text;
4985
- } else if (err instanceof Error) {
4986
- ret = err.message;
4987
- }
4988
- if (!ret || ret.length > MAX_ERROR_MESSAGE_LENGTH) {
4989
- return UNKNOWN_ERROR_MESSAGE;
4990
- }
4991
- return ret;
4992
- }
4993
- class APIError extends Error {
4994
- constructor(options) {
4995
- super(UNKNOWN_ERROR_MESSAGE);
4996
- // NOTE: Needs to conform to NetworkError in @redux-offline/redux-offline, which has `status` and `response`.
4997
- __publicField(this, "status");
4998
- __publicField(this, "response");
4999
- __publicField(this, "message");
5000
- __publicField(this, "options");
5001
- const { response, innerError } = options;
5002
- this.message = options.message ?? extractErrorMessage(response, innerError) ?? UNKNOWN_ERROR_MESSAGE;
5003
- this.status = (response == null ? void 0 : response.status) ?? 0;
5004
- this.response = response;
5005
- options.discard = options.discard ?? false;
5006
- this.options = options;
5007
- }
5008
- }
5009
- class BaseApiService {
5010
- constructor(sdk) {
5011
- __publicField(this, "client");
5012
- this.client = sdk;
5013
- }
5014
- }
5015
5015
  const EXPIRING_SOON_THRESHOLD = 1800;
5016
5016
  function parseTokens(response) {
5017
5017
  if (!response.access)
@@ -16931,6 +16931,7 @@ export {
16931
16931
  getLocalDateString,
16932
16932
  getLocalRelativeDateString,
16933
16933
  getMarkerCoordinates,
16934
+ getOutboxCoordinator,
16934
16935
  getRenamedFile,
16935
16936
  getStageColor,
16936
16937
  hashFile,