@guidelytech/react-native 0.8.0 → 0.10.0
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/README.md +11 -0
- package/dist/index.cjs +451 -109
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +14 -4
- package/dist/index.d.ts +14 -4
- package/dist/index.js +451 -109
- package/dist/index.js.map +1 -1
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -96,7 +96,7 @@ function FlagsProvider({ children, client: external, payload, ...options }) {
|
|
|
96
96
|
return /* @__PURE__ */ jsx(FlagsContext.Provider, { value, children });
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
// ../shared/dist/chunk-
|
|
99
|
+
// ../shared/dist/chunk-UW6NRYRY.js
|
|
100
100
|
function fnv1a(input) {
|
|
101
101
|
let h = 2166136261;
|
|
102
102
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -238,6 +238,8 @@ function resolveField(rule, ctx) {
|
|
|
238
238
|
return ctx.userAttributes.role;
|
|
239
239
|
case "user.plan":
|
|
240
240
|
return ctx.userAttributes.plan ?? ctx.accountAttributes.plan;
|
|
241
|
+
case "user.identified":
|
|
242
|
+
return !!ctx.userId;
|
|
241
243
|
case "url.path":
|
|
242
244
|
return ctx.path;
|
|
243
245
|
case "url.full":
|
|
@@ -400,6 +402,7 @@ function sanitizeProperties(props) {
|
|
|
400
402
|
return out;
|
|
401
403
|
}
|
|
402
404
|
var SOURCEMAP_MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
405
|
+
var MAX_COMPLAINT_ATTACHMENT_BYTES = 2 * 1024 * 1024;
|
|
403
406
|
var MEDIA_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
|
|
404
407
|
var MEDIA_VIDEO_MAX_BYTES = 200 * 1024 * 1024;
|
|
405
408
|
var MEDIA_FILE_MAX_BYTES = 50 * 1024 * 1024;
|
|
@@ -551,51 +554,108 @@ function isArticleListed(collections, article, member) {
|
|
|
551
554
|
}
|
|
552
555
|
var CAPTURE_SCREENSHOT_MAX_BYTES = 8 * 1024 * 1024;
|
|
553
556
|
|
|
554
|
-
// ../sdk-core/dist/chunk-
|
|
555
|
-
function readJson(store, key, fallback) {
|
|
556
|
-
const raw = store.get(key);
|
|
557
|
-
if (raw == null) return fallback;
|
|
558
|
-
try {
|
|
559
|
-
return JSON.parse(raw);
|
|
560
|
-
} catch {
|
|
561
|
-
return fallback;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
function writeJson(store, key, value) {
|
|
565
|
-
try {
|
|
566
|
-
store.set(key, JSON.stringify(value));
|
|
567
|
-
} catch {
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
var KEYS = {
|
|
571
|
-
anon: "guidely:anon",
|
|
572
|
-
seen: "guidely:seen",
|
|
573
|
-
config: (key) => `guidely:config:${key}`,
|
|
574
|
-
etag: (key) => `guidely:etag:${key}`,
|
|
575
|
-
state: (key) => `guidely:state:${key}`,
|
|
576
|
-
user: (key) => `guidely:user:${key}`,
|
|
577
|
-
queue: (key) => `guidely:queue:${key}`,
|
|
578
|
-
session: "guidely:session",
|
|
579
|
-
active: "guidely:active",
|
|
580
|
-
authoring: "guidely:authoring",
|
|
581
|
-
/** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
|
|
582
|
-
capture: "guidely:capture",
|
|
583
|
-
capturePending: "guidely:capture:pending",
|
|
584
|
-
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
585
|
-
conversations: (key) => `guidely:conversations:${key}`,
|
|
586
|
-
/** Announcement ids opened in the messenger's News tab. */
|
|
587
|
-
newsSeen: (key) => `guidely:news:${key}`
|
|
588
|
-
};
|
|
589
|
-
|
|
590
|
-
// ../sdk-core/dist/chunk-AKNE7D5M.js
|
|
557
|
+
// ../sdk-core/dist/chunk-C4WWSF67.js
|
|
591
558
|
var __defProp2 = Object.defineProperty;
|
|
592
559
|
var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
593
560
|
var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
561
|
+
var MAX_RETRY_AFTER_MS = 60 * 60 * 1e3;
|
|
562
|
+
var OFFLINE_WAIT_MS = 3e4;
|
|
563
|
+
var ServerHealth = class {
|
|
564
|
+
constructor(opts = {}) {
|
|
565
|
+
__publicField2(this, "failures", 0);
|
|
566
|
+
__publicField2(this, "openUntil", 0);
|
|
567
|
+
__publicField2(this, "lastOkAt", 0);
|
|
568
|
+
__publicField2(this, "listeners", /* @__PURE__ */ new Set());
|
|
569
|
+
__publicField2(this, "threshold");
|
|
570
|
+
__publicField2(this, "baseMs");
|
|
571
|
+
__publicField2(this, "maxMs");
|
|
572
|
+
__publicField2(this, "now");
|
|
573
|
+
__publicField2(this, "random");
|
|
574
|
+
this.threshold = opts.threshold ?? 3;
|
|
575
|
+
this.baseMs = opts.baseMs ?? 5e3;
|
|
576
|
+
this.maxMs = opts.maxMs ?? 5 * 60 * 1e3;
|
|
577
|
+
this.now = opts.now ?? Date.now;
|
|
578
|
+
this.random = opts.random ?? Math.random;
|
|
579
|
+
}
|
|
580
|
+
/** The server answered (2xx, 3xx, or a 4xx other than 429): it is up. */
|
|
581
|
+
success() {
|
|
582
|
+
const wasDown = this.failures > 0 || this.openUntil > 0;
|
|
583
|
+
this.failures = 0;
|
|
584
|
+
this.openUntil = 0;
|
|
585
|
+
this.lastOkAt = this.now();
|
|
586
|
+
if (wasDown) this.notify();
|
|
587
|
+
}
|
|
588
|
+
/** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */
|
|
589
|
+
failure(retryAfterMs) {
|
|
590
|
+
this.failures += 1;
|
|
591
|
+
const now = this.now();
|
|
592
|
+
if (retryAfterMs != null && retryAfterMs > 0) {
|
|
593
|
+
this.openUntil = Math.max(this.openUntil, now + Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
if (this.failures < this.threshold) return;
|
|
597
|
+
const cooldown = Math.min(this.maxMs, this.baseMs * 2 ** (this.failures - this.threshold));
|
|
598
|
+
this.openUntil = Math.max(this.openUntil, now + cooldown * (0.8 + this.random() * 0.4));
|
|
599
|
+
}
|
|
600
|
+
/** Milliseconds background traffic should wait before trying again (0: send now). */
|
|
601
|
+
waitMs() {
|
|
602
|
+
if (typeof navigator !== "undefined" && navigator.onLine === false) return OFFLINE_WAIT_MS;
|
|
603
|
+
return Math.max(0, this.openUntil - this.now());
|
|
604
|
+
}
|
|
605
|
+
canSend() {
|
|
606
|
+
return this.waitMs() === 0;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* The last request got an answer. Unload beacons can't report back, so what they carry is only
|
|
610
|
+
* forgotten while this holds; otherwise it stays queued and is sent again (the server drops duplicates).
|
|
611
|
+
*/
|
|
612
|
+
get reachable() {
|
|
613
|
+
return this.failures === 0 && this.lastOkAt > 0;
|
|
614
|
+
}
|
|
615
|
+
/** The browser came back online: end the cooldown and let queued traffic try straight away. */
|
|
616
|
+
online() {
|
|
617
|
+
this.openUntil = 0;
|
|
618
|
+
this.notify();
|
|
619
|
+
}
|
|
620
|
+
/** Called when the server answers again after failures, or the browser comes back online. */
|
|
621
|
+
onRecover(cb) {
|
|
622
|
+
this.listeners.add(cb);
|
|
623
|
+
return () => this.listeners.delete(cb);
|
|
624
|
+
}
|
|
625
|
+
notify() {
|
|
626
|
+
for (const cb of Array.from(this.listeners)) {
|
|
627
|
+
try {
|
|
628
|
+
cb();
|
|
629
|
+
} catch {
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
};
|
|
634
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
635
|
+
if (!value) return null;
|
|
636
|
+
const trimmed = value.trim();
|
|
637
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
|
|
638
|
+
const at = Date.parse(trimmed);
|
|
639
|
+
return Number.isNaN(at) ? null : Math.max(0, at - now);
|
|
640
|
+
}
|
|
641
|
+
function isServerFailure(status) {
|
|
642
|
+
return status === 429 || status >= 500;
|
|
643
|
+
}
|
|
644
|
+
var STREAM_RESPONSE_TIMEOUT_MS = 3e4;
|
|
645
|
+
var STREAM_IDLE_TIMEOUT_MS = 6e4;
|
|
594
646
|
var Api = class {
|
|
595
|
-
constructor(baseUrl, key, fetchImpl) {
|
|
647
|
+
constructor(baseUrl, key, fetchImpl, health) {
|
|
596
648
|
__publicField2(this, "baseUrl", baseUrl);
|
|
597
649
|
__publicField2(this, "key", key);
|
|
598
650
|
__publicField2(this, "fetchImpl", fetchImpl);
|
|
651
|
+
__publicField2(this, "health");
|
|
652
|
+
this.health = health ?? new ServerHealth();
|
|
653
|
+
}
|
|
654
|
+
/** Feeds an answer (or a failed request, `null`) into {@link health}. */
|
|
655
|
+
observe(res) {
|
|
656
|
+
if (!res) this.health.failure();
|
|
657
|
+
else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.("retry-after")));
|
|
658
|
+
else this.health.success();
|
|
599
659
|
}
|
|
600
660
|
url(path, params = {}) {
|
|
601
661
|
const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
@@ -615,15 +675,22 @@ var Api = class {
|
|
|
615
675
|
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
616
676
|
const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
|
|
617
677
|
try {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
678
|
+
let res;
|
|
679
|
+
try {
|
|
680
|
+
res = await f(this.url(path, options.params), {
|
|
681
|
+
method,
|
|
682
|
+
headers,
|
|
683
|
+
body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
|
|
684
|
+
keepalive: options.keepalive,
|
|
685
|
+
signal: ac?.signal,
|
|
686
|
+
credentials: "omit",
|
|
687
|
+
mode: "cors"
|
|
688
|
+
});
|
|
689
|
+
} catch (err) {
|
|
690
|
+
this.observe(null);
|
|
691
|
+
throw err;
|
|
692
|
+
}
|
|
693
|
+
this.observe(res);
|
|
627
694
|
let data = null;
|
|
628
695
|
if (res.status !== 304 && res.status !== 204) {
|
|
629
696
|
try {
|
|
@@ -649,30 +716,53 @@ var Api = class {
|
|
|
649
716
|
}
|
|
650
717
|
/**
|
|
651
718
|
* POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
|
|
652
|
-
* returned instead.
|
|
719
|
+
* returned instead. Scout replies can take a while, so there is no overall timeout: the response
|
|
720
|
+
* must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),
|
|
721
|
+
* otherwise the request is aborted and rejects.
|
|
653
722
|
*/
|
|
654
723
|
async stream(path, body, onEvent, headers = {}) {
|
|
655
724
|
const f = this.fetcher;
|
|
656
725
|
if (!f) throw new Error("fetch is not available");
|
|
657
|
-
const
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
let
|
|
726
|
+
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
727
|
+
let timer = null;
|
|
728
|
+
const arm = (ms) => {
|
|
729
|
+
if (!ac) return;
|
|
730
|
+
if (timer) clearTimeout(timer);
|
|
731
|
+
timer = setTimeout(() => ac.abort(), ms);
|
|
732
|
+
};
|
|
733
|
+
arm(STREAM_RESPONSE_TIMEOUT_MS);
|
|
734
|
+
try {
|
|
735
|
+
let res;
|
|
667
736
|
try {
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
737
|
+
res = await f(this.url(path), {
|
|
738
|
+
method: "POST",
|
|
739
|
+
headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
|
|
740
|
+
body: JSON.stringify(body),
|
|
741
|
+
signal: ac?.signal,
|
|
742
|
+
credentials: "omit",
|
|
743
|
+
mode: "cors"
|
|
744
|
+
});
|
|
745
|
+
} catch (err) {
|
|
746
|
+
this.observe(null);
|
|
747
|
+
throw err;
|
|
748
|
+
}
|
|
749
|
+
this.observe(res);
|
|
750
|
+
const type = res.headers?.get?.("content-type") ?? "";
|
|
751
|
+
if (!type.includes("text/event-stream") || !res.body) {
|
|
752
|
+
let data = null;
|
|
753
|
+
try {
|
|
754
|
+
data = await res.json();
|
|
755
|
+
} catch {
|
|
756
|
+
data = null;
|
|
757
|
+
}
|
|
758
|
+
return { status: res.status, ok: res.ok, data, etag: null };
|
|
671
759
|
}
|
|
672
|
-
|
|
760
|
+
arm(STREAM_IDLE_TIMEOUT_MS);
|
|
761
|
+
await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));
|
|
762
|
+
return { status: res.status, ok: res.ok, data: null, etag: null };
|
|
763
|
+
} finally {
|
|
764
|
+
if (timer) clearTimeout(timer);
|
|
673
765
|
}
|
|
674
|
-
await readEventStream(res.body, onEvent);
|
|
675
|
-
return { status: res.status, ok: res.ok, data: null, etag: null };
|
|
676
766
|
}
|
|
677
767
|
/** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
|
|
678
768
|
beacon(path, body) {
|
|
@@ -703,7 +793,7 @@ async function withRetry(send, attempts = 3, baseMs = 500) {
|
|
|
703
793
|
function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
|
|
704
794
|
return withRetry(() => api.post(path, body), attempts, baseMs);
|
|
705
795
|
}
|
|
706
|
-
async function readEventStream(body, onEvent) {
|
|
796
|
+
async function readEventStream(body, onEvent, onChunk) {
|
|
707
797
|
const reader = body.getReader();
|
|
708
798
|
const decoder = new TextDecoder();
|
|
709
799
|
let buffer = "";
|
|
@@ -726,6 +816,7 @@ async function readEventStream(body, onEvent) {
|
|
|
726
816
|
};
|
|
727
817
|
for (; ; ) {
|
|
728
818
|
const { done, value } = await reader.read();
|
|
819
|
+
if (value) onChunk?.();
|
|
729
820
|
if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
|
|
730
821
|
let sep;
|
|
731
822
|
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
@@ -736,8 +827,44 @@ async function readEventStream(body, onEvent) {
|
|
|
736
827
|
}
|
|
737
828
|
if (buffer.trim()) dispatch(buffer);
|
|
738
829
|
}
|
|
830
|
+
function readJson(store, key, fallback) {
|
|
831
|
+
const raw = store.get(key);
|
|
832
|
+
if (raw == null) return fallback;
|
|
833
|
+
try {
|
|
834
|
+
return JSON.parse(raw);
|
|
835
|
+
} catch {
|
|
836
|
+
return fallback;
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
function writeJson(store, key, value) {
|
|
840
|
+
try {
|
|
841
|
+
store.set(key, JSON.stringify(value));
|
|
842
|
+
} catch {
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
var KEYS = {
|
|
846
|
+
anon: "guidely:anon",
|
|
847
|
+
seen: "guidely:seen",
|
|
848
|
+
config: (key) => `guidely:config:${key}`,
|
|
849
|
+
etag: (key) => `guidely:etag:${key}`,
|
|
850
|
+
state: (key) => `guidely:state:${key}`,
|
|
851
|
+
user: (key) => `guidely:user:${key}`,
|
|
852
|
+
queue: (key) => `guidely:queue:${key}`,
|
|
853
|
+
/** Survey answers and guide/checklist progress not yet accepted by the server. */
|
|
854
|
+
outbox: (key) => `guidely:outbox:${key}`,
|
|
855
|
+
session: "guidely:session",
|
|
856
|
+
active: "guidely:active",
|
|
857
|
+
authoring: "guidely:authoring",
|
|
858
|
+
/** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
|
|
859
|
+
capture: "guidely:capture",
|
|
860
|
+
capturePending: "guidely:capture:pending",
|
|
861
|
+
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
862
|
+
conversations: (key) => `guidely:conversations:${key}`,
|
|
863
|
+
/** Announcement ids opened in the messenger's News tab. */
|
|
864
|
+
newsSeen: (key) => `guidely:news:${key}`
|
|
865
|
+
};
|
|
739
866
|
|
|
740
|
-
// ../sdk-core/dist/chunk-
|
|
867
|
+
// ../sdk-core/dist/chunk-GDJB466J.js
|
|
741
868
|
var EventQueue = class {
|
|
742
869
|
constructor(opts) {
|
|
743
870
|
__publicField2(this, "opts", opts);
|
|
@@ -748,17 +875,34 @@ var EventQueue = class {
|
|
|
748
875
|
__publicField2(this, "inflightIds", /* @__PURE__ */ new Set());
|
|
749
876
|
__publicField2(this, "attempt", 0);
|
|
750
877
|
__publicField2(this, "destroyed", false);
|
|
878
|
+
__publicField2(this, "offRecover", null);
|
|
751
879
|
__publicField2(this, "flushIntervalMs");
|
|
752
880
|
__publicField2(this, "maxQueue");
|
|
753
881
|
__publicField2(this, "batchSize");
|
|
754
882
|
__publicField2(this, "maxAttempts");
|
|
755
883
|
this.flushIntervalMs = opts.flushIntervalMs ?? 2e3;
|
|
756
|
-
this.maxQueue = opts.maxQueue ??
|
|
884
|
+
this.maxQueue = opts.maxQueue ?? 500;
|
|
757
885
|
this.batchSize = opts.batchSize ?? 50;
|
|
758
886
|
this.maxAttempts = opts.maxAttempts ?? 5;
|
|
759
887
|
const persisted = readJson(opts.store, opts.storageKey, []);
|
|
760
888
|
if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === "string").slice(-this.maxQueue);
|
|
761
889
|
if (this.events.length) this.schedule();
|
|
890
|
+
this.offRecover = opts.health?.onRecover(() => this.retryNow()) ?? null;
|
|
891
|
+
}
|
|
892
|
+
/** The API is back: drop any pending backoff and send what is queued. */
|
|
893
|
+
retryNow() {
|
|
894
|
+
if (this.destroyed) return;
|
|
895
|
+
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
896
|
+
this.retryTimer = null;
|
|
897
|
+
this.attempt = 0;
|
|
898
|
+
void this.flush();
|
|
899
|
+
}
|
|
900
|
+
retryIn(ms) {
|
|
901
|
+
if (this.retryTimer || this.destroyed) return;
|
|
902
|
+
this.retryTimer = setTimeout(() => {
|
|
903
|
+
this.retryTimer = null;
|
|
904
|
+
void this.flush();
|
|
905
|
+
}, ms);
|
|
762
906
|
}
|
|
763
907
|
get size() {
|
|
764
908
|
return this.events.length;
|
|
@@ -792,6 +936,8 @@ var EventQueue = class {
|
|
|
792
936
|
clearTimeout(this.timer);
|
|
793
937
|
this.timer = null;
|
|
794
938
|
}
|
|
939
|
+
const wait = this.opts.health?.waitMs() ?? 0;
|
|
940
|
+
if (wait > 0) return this.retryIn(wait);
|
|
795
941
|
this.inflight = true;
|
|
796
942
|
const batch = this.events.slice(0, this.batchSize);
|
|
797
943
|
this.inflightIds = new Set(batch.map((e) => e.eventId));
|
|
@@ -815,26 +961,20 @@ var EventQueue = class {
|
|
|
815
961
|
if (this.events.length) this.schedule();
|
|
816
962
|
return;
|
|
817
963
|
}
|
|
818
|
-
this.attempt
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
return;
|
|
822
|
-
}
|
|
823
|
-
this.retryTimer = setTimeout(() => {
|
|
824
|
-
this.retryTimer = null;
|
|
825
|
-
void this.flush();
|
|
826
|
-
}, backoffMs(this.attempt - 1));
|
|
964
|
+
this.attempt = Math.min(this.attempt + 1, this.maxAttempts);
|
|
965
|
+
const delay = this.attempt >= this.maxAttempts ? 6e4 : backoffMs(this.attempt - 1);
|
|
966
|
+
this.retryIn(Math.max(delay, this.opts.health?.waitMs() ?? 0));
|
|
827
967
|
}
|
|
828
968
|
/** Best-effort synchronous flush on unload using `sendBeacon`. */
|
|
829
969
|
flushSync() {
|
|
830
970
|
if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;
|
|
831
971
|
const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);
|
|
832
972
|
if (!batch.length) return;
|
|
833
|
-
if (this.opts.beacon(batch)) {
|
|
973
|
+
if (this.opts.beacon(batch) && (this.opts.health?.reachable ?? true)) {
|
|
834
974
|
const ids = new Set(batch.map((e) => e.eventId));
|
|
835
975
|
this.events = this.events.filter((e) => !ids.has(e.eventId));
|
|
836
|
-
this.persist();
|
|
837
976
|
}
|
|
977
|
+
this.persist();
|
|
838
978
|
}
|
|
839
979
|
clear() {
|
|
840
980
|
this.events = [];
|
|
@@ -842,6 +982,7 @@ var EventQueue = class {
|
|
|
842
982
|
}
|
|
843
983
|
destroy() {
|
|
844
984
|
this.destroyed = true;
|
|
985
|
+
this.offRecover?.();
|
|
845
986
|
if (this.timer) clearTimeout(this.timer);
|
|
846
987
|
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
847
988
|
this.timer = null;
|
|
@@ -849,6 +990,88 @@ var EventQueue = class {
|
|
|
849
990
|
this.persist();
|
|
850
991
|
}
|
|
851
992
|
};
|
|
993
|
+
var Outbox = class {
|
|
994
|
+
constructor(opts) {
|
|
995
|
+
__publicField2(this, "opts", opts);
|
|
996
|
+
__publicField2(this, "items");
|
|
997
|
+
__publicField2(this, "sending", false);
|
|
998
|
+
__publicField2(this, "attempt", 0);
|
|
999
|
+
__publicField2(this, "timer", null);
|
|
1000
|
+
__publicField2(this, "destroyed", false);
|
|
1001
|
+
__publicField2(this, "offRecover");
|
|
1002
|
+
__publicField2(this, "maxItems");
|
|
1003
|
+
__publicField2(this, "maxAgeMs");
|
|
1004
|
+
this.maxItems = opts.maxItems ?? 50;
|
|
1005
|
+
this.maxAgeMs = opts.maxAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
|
|
1006
|
+
const stored = readJson(opts.store, opts.storageKey, []);
|
|
1007
|
+
const cutoff = Date.now() - this.maxAgeMs;
|
|
1008
|
+
this.items = Array.isArray(stored) ? stored.filter((i) => i && typeof i.path === "string" && typeof i.key === "string" && i.createdAt > cutoff).slice(-this.maxItems) : [];
|
|
1009
|
+
this.offRecover = opts.health?.onRecover(() => {
|
|
1010
|
+
this.attempt = 0;
|
|
1011
|
+
this.retryIn(0);
|
|
1012
|
+
}) ?? null;
|
|
1013
|
+
if (this.items.length) this.retryIn(0);
|
|
1014
|
+
}
|
|
1015
|
+
get size() {
|
|
1016
|
+
return this.items.length;
|
|
1017
|
+
}
|
|
1018
|
+
/** Queues `POST path` and sends it now (or as soon as the API is reachable). */
|
|
1019
|
+
add(key, path, body) {
|
|
1020
|
+
if (this.destroyed) return;
|
|
1021
|
+
this.items = this.items.filter((i) => i.key !== key);
|
|
1022
|
+
this.items.push({ key, path, body, createdAt: Date.now() });
|
|
1023
|
+
if (this.items.length > this.maxItems) this.items.splice(0, this.items.length - this.maxItems);
|
|
1024
|
+
this.persist();
|
|
1025
|
+
void this.flush();
|
|
1026
|
+
}
|
|
1027
|
+
persist() {
|
|
1028
|
+
writeJson(this.opts.store, this.opts.storageKey, this.items);
|
|
1029
|
+
}
|
|
1030
|
+
retryIn(ms) {
|
|
1031
|
+
if (this.destroyed) return;
|
|
1032
|
+
if (this.timer) clearTimeout(this.timer);
|
|
1033
|
+
this.timer = setTimeout(() => {
|
|
1034
|
+
this.timer = null;
|
|
1035
|
+
void this.flush();
|
|
1036
|
+
}, ms);
|
|
1037
|
+
}
|
|
1038
|
+
/** Sends queued requests in order, one at a time. */
|
|
1039
|
+
async flush() {
|
|
1040
|
+
if (this.destroyed || this.sending || !this.items.length) return;
|
|
1041
|
+
const wait = this.opts.health?.waitMs() ?? 0;
|
|
1042
|
+
if (wait > 0) return this.retryIn(wait);
|
|
1043
|
+
this.sending = true;
|
|
1044
|
+
try {
|
|
1045
|
+
while (this.items.length && !this.destroyed) {
|
|
1046
|
+
const item = this.items[0];
|
|
1047
|
+
let retry = false;
|
|
1048
|
+
try {
|
|
1049
|
+
const res = await this.opts.api.post(item.path, item.body);
|
|
1050
|
+
retry = !res.ok && (res.status === 429 || res.status >= 500);
|
|
1051
|
+
} catch (err) {
|
|
1052
|
+
retry = true;
|
|
1053
|
+
this.opts.onError?.(err);
|
|
1054
|
+
}
|
|
1055
|
+
if (retry) {
|
|
1056
|
+
this.attempt += 1;
|
|
1057
|
+
this.retryIn(Math.max(backoffMs(this.attempt - 1, 1e3, 6e4), this.opts.health?.waitMs() ?? 0));
|
|
1058
|
+
return;
|
|
1059
|
+
}
|
|
1060
|
+
this.attempt = 0;
|
|
1061
|
+
this.items = this.items.filter((i) => i !== item);
|
|
1062
|
+
this.persist();
|
|
1063
|
+
}
|
|
1064
|
+
} finally {
|
|
1065
|
+
this.sending = false;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
destroy() {
|
|
1069
|
+
this.destroyed = true;
|
|
1070
|
+
this.offRecover?.();
|
|
1071
|
+
if (this.timer) clearTimeout(this.timer);
|
|
1072
|
+
this.timer = null;
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
852
1075
|
var Emitter = class {
|
|
853
1076
|
constructor(onError) {
|
|
854
1077
|
__publicField2(this, "onError", onError);
|
|
@@ -923,6 +1146,10 @@ function isWithinSchedule(guide, now = Date.now()) {
|
|
|
923
1146
|
if (s.endAt && now > Date.parse(s.endAt)) return false;
|
|
924
1147
|
return true;
|
|
925
1148
|
}
|
|
1149
|
+
function dismissedAfterMerge(local, server) {
|
|
1150
|
+
if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
|
|
1151
|
+
return local?.dismissedAt ?? server.dismissedAt ?? null;
|
|
1152
|
+
}
|
|
926
1153
|
|
|
927
1154
|
// src/core/client.ts
|
|
928
1155
|
import { createFlagsClient as createFlagsClient2 } from "@guidelytech/flags";
|
|
@@ -1400,12 +1627,29 @@ function createSseParser(onEvent) {
|
|
|
1400
1627
|
}
|
|
1401
1628
|
};
|
|
1402
1629
|
}
|
|
1630
|
+
var STREAM_RESPONSE_TIMEOUT_MS2 = 3e4;
|
|
1631
|
+
var STREAM_IDLE_TIMEOUT_MS2 = 6e4;
|
|
1403
1632
|
function xhrStreamPost() {
|
|
1404
1633
|
if (typeof XMLHttpRequest === "undefined") return null;
|
|
1405
1634
|
return (url, headers, body, onChunk) => new Promise((resolve, reject) => {
|
|
1406
1635
|
const xhr = new XMLHttpRequest();
|
|
1407
1636
|
let seen = 0;
|
|
1408
1637
|
let streamed = false;
|
|
1638
|
+
let timer = null;
|
|
1639
|
+
let settled = false;
|
|
1640
|
+
const settle = (fn) => {
|
|
1641
|
+
if (settled) return;
|
|
1642
|
+
settled = true;
|
|
1643
|
+
if (timer) clearTimeout(timer);
|
|
1644
|
+
fn();
|
|
1645
|
+
};
|
|
1646
|
+
const arm = (ms, what) => {
|
|
1647
|
+
if (timer) clearTimeout(timer);
|
|
1648
|
+
timer = setTimeout(() => {
|
|
1649
|
+
settle(() => reject(new Error(`Scout ${what} timed out`)));
|
|
1650
|
+
xhr.abort();
|
|
1651
|
+
}, ms);
|
|
1652
|
+
};
|
|
1409
1653
|
const pump = () => {
|
|
1410
1654
|
const type = xhr.getResponseHeader("content-type") || "";
|
|
1411
1655
|
if (!type.includes("text/event-stream")) return;
|
|
@@ -1418,8 +1662,15 @@ function xhrStreamPost() {
|
|
|
1418
1662
|
};
|
|
1419
1663
|
xhr.open("POST", url);
|
|
1420
1664
|
for (const [k, v] of Object.entries(headers)) xhr.setRequestHeader(k, v);
|
|
1421
|
-
xhr.
|
|
1422
|
-
|
|
1665
|
+
xhr.onreadystatechange = () => {
|
|
1666
|
+
if (!settled && xhr.readyState >= 2 && xhr.readyState < 4) arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
|
|
1667
|
+
};
|
|
1668
|
+
xhr.onprogress = () => {
|
|
1669
|
+
if (settled) return;
|
|
1670
|
+
arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
|
|
1671
|
+
pump();
|
|
1672
|
+
};
|
|
1673
|
+
xhr.onload = () => settle(() => {
|
|
1423
1674
|
pump();
|
|
1424
1675
|
let json = null;
|
|
1425
1676
|
if (!streamed) {
|
|
@@ -1430,27 +1681,74 @@ function xhrStreamPost() {
|
|
|
1430
1681
|
}
|
|
1431
1682
|
}
|
|
1432
1683
|
resolve({ status: xhr.status, json, streamed });
|
|
1433
|
-
};
|
|
1434
|
-
xhr.onerror = () => reject(new Error("Network request failed"));
|
|
1684
|
+
});
|
|
1685
|
+
xhr.onerror = () => settle(() => reject(new Error("Network request failed")));
|
|
1686
|
+
arm(STREAM_RESPONSE_TIMEOUT_MS2, "request");
|
|
1435
1687
|
xhr.send(JSON.stringify(body));
|
|
1436
1688
|
});
|
|
1437
1689
|
}
|
|
1690
|
+
function bodyReader(res) {
|
|
1691
|
+
const body = res.body;
|
|
1692
|
+
const Decoder = globalThis.TextDecoder;
|
|
1693
|
+
return body && typeof body.getReader === "function" && Decoder ? { reader: body.getReader(), decoder: new Decoder() } : null;
|
|
1694
|
+
}
|
|
1438
1695
|
function fetchStreamPost(fetchImpl) {
|
|
1439
1696
|
return async (url, headers, body, onChunk) => {
|
|
1440
|
-
const
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1697
|
+
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
1698
|
+
let timer = null;
|
|
1699
|
+
let timedOut = "";
|
|
1700
|
+
let fail = () => {
|
|
1701
|
+
};
|
|
1702
|
+
const expired = new Promise((_, reject) => fail = reject);
|
|
1703
|
+
expired.catch(() => {
|
|
1704
|
+
});
|
|
1705
|
+
const arm = (ms, what) => {
|
|
1706
|
+
if (timer) clearTimeout(timer);
|
|
1707
|
+
timer = setTimeout(() => {
|
|
1708
|
+
timedOut = what;
|
|
1709
|
+
ac?.abort();
|
|
1710
|
+
fail(new Error(`Scout ${what} timed out`));
|
|
1711
|
+
}, ms);
|
|
1712
|
+
};
|
|
1713
|
+
const race = (p) => Promise.race([p, expired]);
|
|
1714
|
+
arm(STREAM_RESPONSE_TIMEOUT_MS2, "request");
|
|
1448
1715
|
try {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1716
|
+
const res = await race(fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body), signal: ac?.signal }));
|
|
1717
|
+
arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
|
|
1718
|
+
const type = res.headers.get("content-type") || "";
|
|
1719
|
+
const sse = type.includes("text/event-stream");
|
|
1720
|
+
const streaming = sse ? bodyReader(res) : null;
|
|
1721
|
+
if (streaming) {
|
|
1722
|
+
const { reader, decoder } = streaming;
|
|
1723
|
+
for (; ; ) {
|
|
1724
|
+
const { done, value } = await race(reader.read());
|
|
1725
|
+
if (value) {
|
|
1726
|
+
arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
|
|
1727
|
+
onChunk(decoder.decode(value, { stream: true }));
|
|
1728
|
+
}
|
|
1729
|
+
if (done) break;
|
|
1730
|
+
}
|
|
1731
|
+
const rest = decoder.decode();
|
|
1732
|
+
if (rest) onChunk(rest);
|
|
1733
|
+
return { status: res.status, json: null, streamed: true };
|
|
1734
|
+
}
|
|
1735
|
+
const text = await race(res.text());
|
|
1736
|
+
if (sse) {
|
|
1737
|
+
onChunk(text);
|
|
1738
|
+
return { status: res.status, json: null, streamed: true };
|
|
1739
|
+
}
|
|
1740
|
+
let json = null;
|
|
1741
|
+
try {
|
|
1742
|
+
json = JSON.parse(text || "null");
|
|
1743
|
+
} catch {
|
|
1744
|
+
json = null;
|
|
1745
|
+
}
|
|
1746
|
+
return { status: res.status, json, streamed: false };
|
|
1747
|
+
} catch (err) {
|
|
1748
|
+
throw timedOut ? new Error(`Scout ${timedOut} timed out`) : err;
|
|
1749
|
+
} finally {
|
|
1750
|
+
if (timer) clearTimeout(timer);
|
|
1452
1751
|
}
|
|
1453
|
-
return { status: res.status, json, streamed: false };
|
|
1454
1752
|
};
|
|
1455
1753
|
}
|
|
1456
1754
|
var byNewest = (a, b) => Date.parse(b.lastMessageAt) - Date.parse(a.lastMessageAt);
|
|
@@ -1635,7 +1933,15 @@ var Messenger = class {
|
|
|
1635
1933
|
}
|
|
1636
1934
|
});
|
|
1637
1935
|
try {
|
|
1638
|
-
|
|
1936
|
+
let res;
|
|
1937
|
+
try {
|
|
1938
|
+
res = await this.deps.streamPost(this.url(path), headers, payload, (chunk) => parser.feed(chunk));
|
|
1939
|
+
} catch (err) {
|
|
1940
|
+
this.deps.health?.failure();
|
|
1941
|
+
throw err;
|
|
1942
|
+
}
|
|
1943
|
+
if (isServerFailure(res.status)) this.deps.health?.failure();
|
|
1944
|
+
else this.deps.health?.success();
|
|
1639
1945
|
parser.end();
|
|
1640
1946
|
if (res.status < 200 || res.status >= 300) throw new Error(`Request failed (${res.status})`);
|
|
1641
1947
|
if (!res.streamed && res.json) {
|
|
@@ -1822,9 +2128,20 @@ var PersistentStore = class {
|
|
|
1822
2128
|
|
|
1823
2129
|
// src/core/client.ts
|
|
1824
2130
|
var SDK_NAME = "@guidelytech/react-native";
|
|
1825
|
-
var SDK_VERSION = "0.
|
|
2131
|
+
var SDK_VERSION = "0.10.0";
|
|
1826
2132
|
var DEFAULT_API_URL = "https://guidelytech.com";
|
|
1827
2133
|
var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
|
|
2134
|
+
var CACHED_START_MS = 1500;
|
|
2135
|
+
function settlesWithin(promise, ms) {
|
|
2136
|
+
return new Promise((resolve) => {
|
|
2137
|
+
const timer = setTimeout(() => resolve(false), ms);
|
|
2138
|
+
const done = () => {
|
|
2139
|
+
clearTimeout(timer);
|
|
2140
|
+
resolve(true);
|
|
2141
|
+
};
|
|
2142
|
+
promise.then(done, done);
|
|
2143
|
+
});
|
|
2144
|
+
}
|
|
1828
2145
|
var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1829
2146
|
var emptyState = () => ({ guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] });
|
|
1830
2147
|
var newSession = () => ({ id: `s_${randomId(16)}`, startedAt: nowIso(), lastActiveAt: nowIso(), shownGuides: [], events: [] });
|
|
@@ -1837,6 +2154,8 @@ var GuidelyClient = class {
|
|
|
1837
2154
|
__publicField(this, "store");
|
|
1838
2155
|
__publicField(this, "emitter");
|
|
1839
2156
|
__publicField(this, "queue", null);
|
|
2157
|
+
/** Survey answers, guide completions/dismissals and checklist progress, kept until the server has them. */
|
|
2158
|
+
__publicField(this, "outbox", null);
|
|
1840
2159
|
__publicField(this, "config", null);
|
|
1841
2160
|
__publicField(this, "configFetchedAt", 0);
|
|
1842
2161
|
__publicField(this, "etag", null);
|
|
@@ -1899,7 +2218,7 @@ var GuidelyClient = class {
|
|
|
1899
2218
|
this.emitter = new Emitter((err) => this.reportError(err));
|
|
1900
2219
|
this.api = new Api(this.options.apiUrl, this.options.projectId, options.fetch);
|
|
1901
2220
|
this.store = new PersistentStore(options.storage ?? null, (err) => this.log("storage error", err));
|
|
1902
|
-
if (!options.storage) this.log("no `storage` passed: identity
|
|
2221
|
+
if (!options.storage) this.log("no `storage` passed: identity, guide history, queued events and survey answers last only for this app session, so whatever could not be sent during a Guidely outage is lost when the app restarts. Pass AsyncStorage to persist them.");
|
|
1903
2222
|
this.cleanups.push(this.targets.onTap((id) => this.handleTap(id)));
|
|
1904
2223
|
if (options.user) this.user = { ...options.user };
|
|
1905
2224
|
this.flags = this.createFlags(options.flags);
|
|
@@ -1950,7 +2269,7 @@ var GuidelyClient = class {
|
|
|
1950
2269
|
async bootstrap() {
|
|
1951
2270
|
const key = this.options.projectId;
|
|
1952
2271
|
try {
|
|
1953
|
-
await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.conversations(key)]);
|
|
2272
|
+
await this.store.hydrate([KEYS.anon, KEYS.seen, KEYS.session, KEYS.config(key), KEYS.etag(key), KEYS.state(key), KEYS.user(key), KEYS.queue(key), KEYS.outbox(key), KEYS.conversations(key)]);
|
|
1954
2273
|
if (this.destroyed) return;
|
|
1955
2274
|
this.anonymousId = this.store.get(KEYS.anon) || "";
|
|
1956
2275
|
if (!this.anonymousId) {
|
|
@@ -1970,22 +2289,27 @@ var GuidelyClient = class {
|
|
|
1970
2289
|
store: this.store,
|
|
1971
2290
|
storageKey: KEYS.queue(key),
|
|
1972
2291
|
enabled: () => this.analyticsEnabled(),
|
|
2292
|
+
health: this.api.health,
|
|
1973
2293
|
send: async (events) => {
|
|
1974
2294
|
const res = await this.api.post("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } });
|
|
1975
2295
|
return { ok: res.ok, retry: !(res.status >= 400 && res.status < 500 && res.status !== 429) };
|
|
1976
2296
|
},
|
|
1977
2297
|
onError: (err) => this.reportError(err)
|
|
1978
2298
|
});
|
|
2299
|
+
this.outbox = new Outbox({ api: this.api, store: this.store, storageKey: KEYS.outbox(key), health: this.api.health });
|
|
1979
2300
|
this.hydrated = true;
|
|
1980
2301
|
const cached = readJson(this.store, KEYS.config(key), null);
|
|
1981
2302
|
this.etag = this.store.get(KEYS.etag(key));
|
|
1982
2303
|
if (cached && Array.isArray(cached.guides)) this.config = forPlatform(cached, this.bridge.device.platform);
|
|
1983
2304
|
this.syncMessenger();
|
|
1984
2305
|
this.pending.splice(0).forEach((fn) => fn());
|
|
1985
|
-
|
|
2306
|
+
const before = this.config?.configVersion;
|
|
2307
|
+
const fresh = this.fetchConfig();
|
|
2308
|
+
if (!this.config) await fresh;
|
|
2309
|
+
else if (!await settlesWithin(fresh, CACHED_START_MS)) void fresh.then(() => this.applyConfigChange(before));
|
|
1986
2310
|
if (this.destroyed) return;
|
|
1987
2311
|
this.wireTimers();
|
|
1988
|
-
if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
|
|
2312
|
+
if (this.user?.id) await settlesWithin(this.identify(this.user.id, this.user.traits, this.user.account), CACHED_START_MS);
|
|
1989
2313
|
if (this.destroyed) return;
|
|
1990
2314
|
this.isReady = true;
|
|
1991
2315
|
if (this.config) this.emitter.emit("ready", this.config);
|
|
@@ -2025,7 +2349,9 @@ var GuidelyClient = class {
|
|
|
2025
2349
|
}
|
|
2026
2350
|
}
|
|
2027
2351
|
wireTimers() {
|
|
2028
|
-
const refresh = setInterval(() =>
|
|
2352
|
+
const refresh = setInterval(() => {
|
|
2353
|
+
if (this.api.health.canSend()) void this.refresh();
|
|
2354
|
+
}, this.options.configRefreshMs);
|
|
2029
2355
|
this.cleanups.push(() => clearInterval(refresh));
|
|
2030
2356
|
const poll = setInterval(() => {
|
|
2031
2357
|
if (!this.isReady || !this.config || this.backgroundedAt !== null) return;
|
|
@@ -2041,6 +2367,11 @@ var GuidelyClient = class {
|
|
|
2041
2367
|
if (this.destroyed) return;
|
|
2042
2368
|
const before = this.config?.configVersion;
|
|
2043
2369
|
await this.fetchConfig();
|
|
2370
|
+
this.applyConfigChange(before);
|
|
2371
|
+
}
|
|
2372
|
+
/** Re-renders after a config change (refresh, or a config that arrived after start). */
|
|
2373
|
+
applyConfigChange(before) {
|
|
2374
|
+
if (this.destroyed || !this.isReady) return;
|
|
2044
2375
|
if (this.config && this.config.configVersion !== before) {
|
|
2045
2376
|
this.notify();
|
|
2046
2377
|
this.evaluate("ready");
|
|
@@ -2054,6 +2385,7 @@ var GuidelyClient = class {
|
|
|
2054
2385
|
this.session.lastActiveAt = nowIso();
|
|
2055
2386
|
this.persistSession();
|
|
2056
2387
|
void this.queue?.flush();
|
|
2388
|
+
void this.outbox?.flush();
|
|
2057
2389
|
void this.store.flush();
|
|
2058
2390
|
void this.flags.flush();
|
|
2059
2391
|
this.messenger?.setForeground(false);
|
|
@@ -2067,7 +2399,7 @@ var GuidelyClient = class {
|
|
|
2067
2399
|
this.firedDelays.clear();
|
|
2068
2400
|
}
|
|
2069
2401
|
void this.flags.refresh();
|
|
2070
|
-
if (Date.now() - this.configFetchedAt > this.options.configRefreshMs) void this.refresh();
|
|
2402
|
+
if (Date.now() - this.configFetchedAt > this.options.configRefreshMs && this.api.health.canSend()) void this.refresh();
|
|
2071
2403
|
else if (this.isReady) this.evaluate("ready");
|
|
2072
2404
|
}
|
|
2073
2405
|
}
|
|
@@ -2093,6 +2425,7 @@ var GuidelyClient = class {
|
|
|
2093
2425
|
this.cleanups.splice(0).forEach((fn) => fn());
|
|
2094
2426
|
void this.queue?.flush();
|
|
2095
2427
|
this.queue?.destroy();
|
|
2428
|
+
this.outbox?.destroy();
|
|
2096
2429
|
this.flags.destroy();
|
|
2097
2430
|
this.emitter.clear();
|
|
2098
2431
|
this.uiListeners.clear();
|
|
@@ -2140,7 +2473,8 @@ var GuidelyClient = class {
|
|
|
2140
2473
|
checklists[id] = {
|
|
2141
2474
|
completedTaskIds: Array.from(/* @__PURE__ */ new Set([...local?.completedTaskIds ?? [], ...p.completedTaskIds ?? []])),
|
|
2142
2475
|
completedAt: local?.completedAt ?? p.completedAt ?? null,
|
|
2143
|
-
dismissedAt: local
|
|
2476
|
+
dismissedAt: dismissedAfterMerge(local, p),
|
|
2477
|
+
dismissResetAt: p.dismissResetAt ?? local?.dismissResetAt ?? null
|
|
2144
2478
|
};
|
|
2145
2479
|
}
|
|
2146
2480
|
this.state = {
|
|
@@ -2505,7 +2839,13 @@ var GuidelyClient = class {
|
|
|
2505
2839
|
if (reason === "completed") this.completeChecklistTasksBy("guide_completed", guide);
|
|
2506
2840
|
}
|
|
2507
2841
|
syncGuideState(guideId, update) {
|
|
2508
|
-
|
|
2842
|
+
const body = { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] };
|
|
2843
|
+
if (update.impression) void postWithRetry(this.api, "/guide-state", body);
|
|
2844
|
+
else this.sendDurably(`guide:${guideId}:${update.completed ? "completed" : "dismissed"}:${this.anonymousId}`, "/guide-state", body);
|
|
2845
|
+
}
|
|
2846
|
+
/** Queues a write in the persisted outbox (sent now, or once the API is reachable again). */
|
|
2847
|
+
sendDurably(key, path, body) {
|
|
2848
|
+
this.whenHydrated(() => this.outbox?.add(key, path, body));
|
|
2509
2849
|
}
|
|
2510
2850
|
show(guide, manual, stepId) {
|
|
2511
2851
|
try {
|
|
@@ -2713,7 +3053,7 @@ var GuidelyClient = class {
|
|
|
2713
3053
|
progress.dismissedAt = nowIso();
|
|
2714
3054
|
this.persistState();
|
|
2715
3055
|
this.enqueue("checklist_dismissed", { guideId: guide.id, guideVersion: guide.version });
|
|
2716
|
-
|
|
3056
|
+
this.sendDurably(`checklist:${guide.id}:dismissed:${this.anonymousId}`, `/checklists/${encodeURIComponent(guide.id)}/progress`, { anonymousId: this.anonymousId, userId: this.user?.id || null, guideVersion: guide.version, completedTaskIds: progress.completedTaskIds, dismissed: true });
|
|
2717
3057
|
this.checklists.delete(guide.id);
|
|
2718
3058
|
this.notify();
|
|
2719
3059
|
}
|
|
@@ -2726,7 +3066,7 @@ var GuidelyClient = class {
|
|
|
2726
3066
|
this.persistState();
|
|
2727
3067
|
this.recordEnd(guide, "completed");
|
|
2728
3068
|
this.enqueue("survey_submitted", { guideId: guide.id, guideVersion: guide.version, properties: { responseId } });
|
|
2729
|
-
|
|
3069
|
+
this.sendDurably(`survey:${responseId}`, `/surveys/${encodeURIComponent(guide.id)}/responses`, { anonymousId: this.anonymousId, userId: this.user?.id || null, accountId: this.user?.account?.id || null, guideVersion: guide.version, answers, responseId });
|
|
2730
3070
|
this.emitter.emit("survey:submit", { guide, answers, responseId });
|
|
2731
3071
|
this.emitter.emit("guide:complete", guide);
|
|
2732
3072
|
this.surveyView = { ...this.surveyView, submitted: true };
|
|
@@ -2783,7 +3123,7 @@ var GuidelyClient = class {
|
|
|
2783
3123
|
this.enqueue("checklist_completed", { guideId: guide.id, guideVersion: guide.version });
|
|
2784
3124
|
}
|
|
2785
3125
|
this.persistState();
|
|
2786
|
-
|
|
3126
|
+
this.sendDurably(`checklist:${guide.id}:progress:${this.anonymousId}`, `/checklists/${encodeURIComponent(guide.id)}/progress`, { anonymousId: this.anonymousId, userId: this.user?.id || null, guideVersion: guide.version, completedTaskIds: progress.completedTaskIds, completed: !!progress.completedAt });
|
|
2787
3127
|
this.emitter.emit("checklist:task", { guide, task, progress: this.getChecklistProgress(guide.id) });
|
|
2788
3128
|
if (complete) {
|
|
2789
3129
|
this.emitter.emit("checklist:complete", guide);
|
|
@@ -2920,7 +3260,8 @@ var GuidelyClient = class {
|
|
|
2920
3260
|
},
|
|
2921
3261
|
onMessage: (message, conversation) => this.emitter.emit("conversation:message", { message, conversation }),
|
|
2922
3262
|
onStartGuide: (key) => this.setTimer(() => this.startGuide(key), 0),
|
|
2923
|
-
onError: (err) => this.log("messenger", err instanceof Error ? err.message : err)
|
|
3263
|
+
onError: (err) => this.log("messenger", err instanceof Error ? err.message : err),
|
|
3264
|
+
health: this.api.health
|
|
2924
3265
|
});
|
|
2925
3266
|
this.resourceCenterOpen = false;
|
|
2926
3267
|
}
|
|
@@ -3025,6 +3366,7 @@ var GuidelyClient = class {
|
|
|
3025
3366
|
audienceIds: this.state.audienceIds,
|
|
3026
3367
|
session: this.session,
|
|
3027
3368
|
pendingEvents: this.queue?.size ?? 0,
|
|
3369
|
+
pendingWrites: this.outbox?.size ?? 0,
|
|
3028
3370
|
targets: this.targets.ids(),
|
|
3029
3371
|
flagsReady: this.flags.isReady()
|
|
3030
3372
|
};
|