@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.cjs
CHANGED
|
@@ -156,7 +156,7 @@ function FlagsProvider({ children, client: external, payload, ...options }) {
|
|
|
156
156
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FlagsContext.Provider, { value, children });
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
-
// ../shared/dist/chunk-
|
|
159
|
+
// ../shared/dist/chunk-UW6NRYRY.js
|
|
160
160
|
function fnv1a(input) {
|
|
161
161
|
let h = 2166136261;
|
|
162
162
|
for (let i = 0; i < input.length; i++) {
|
|
@@ -298,6 +298,8 @@ function resolveField(rule, ctx) {
|
|
|
298
298
|
return ctx.userAttributes.role;
|
|
299
299
|
case "user.plan":
|
|
300
300
|
return ctx.userAttributes.plan ?? ctx.accountAttributes.plan;
|
|
301
|
+
case "user.identified":
|
|
302
|
+
return !!ctx.userId;
|
|
301
303
|
case "url.path":
|
|
302
304
|
return ctx.path;
|
|
303
305
|
case "url.full":
|
|
@@ -460,6 +462,7 @@ function sanitizeProperties(props) {
|
|
|
460
462
|
return out;
|
|
461
463
|
}
|
|
462
464
|
var SOURCEMAP_MAX_FILE_BYTES = 25 * 1024 * 1024;
|
|
465
|
+
var MAX_COMPLAINT_ATTACHMENT_BYTES = 2 * 1024 * 1024;
|
|
463
466
|
var MEDIA_IMAGE_MAX_BYTES = 10 * 1024 * 1024;
|
|
464
467
|
var MEDIA_VIDEO_MAX_BYTES = 200 * 1024 * 1024;
|
|
465
468
|
var MEDIA_FILE_MAX_BYTES = 50 * 1024 * 1024;
|
|
@@ -611,51 +614,108 @@ function isArticleListed(collections, article, member) {
|
|
|
611
614
|
}
|
|
612
615
|
var CAPTURE_SCREENSHOT_MAX_BYTES = 8 * 1024 * 1024;
|
|
613
616
|
|
|
614
|
-
// ../sdk-core/dist/chunk-
|
|
615
|
-
function readJson(store, key, fallback) {
|
|
616
|
-
const raw = store.get(key);
|
|
617
|
-
if (raw == null) return fallback;
|
|
618
|
-
try {
|
|
619
|
-
return JSON.parse(raw);
|
|
620
|
-
} catch {
|
|
621
|
-
return fallback;
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
function writeJson(store, key, value) {
|
|
625
|
-
try {
|
|
626
|
-
store.set(key, JSON.stringify(value));
|
|
627
|
-
} catch {
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
var KEYS = {
|
|
631
|
-
anon: "guidely:anon",
|
|
632
|
-
seen: "guidely:seen",
|
|
633
|
-
config: (key) => `guidely:config:${key}`,
|
|
634
|
-
etag: (key) => `guidely:etag:${key}`,
|
|
635
|
-
state: (key) => `guidely:state:${key}`,
|
|
636
|
-
user: (key) => `guidely:user:${key}`,
|
|
637
|
-
queue: (key) => `guidely:queue:${key}`,
|
|
638
|
-
session: "guidely:session",
|
|
639
|
-
active: "guidely:active",
|
|
640
|
-
authoring: "guidely:authoring",
|
|
641
|
-
/** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
|
|
642
|
-
capture: "guidely:capture",
|
|
643
|
-
capturePending: "guidely:capture:pending",
|
|
644
|
-
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
645
|
-
conversations: (key) => `guidely:conversations:${key}`,
|
|
646
|
-
/** Announcement ids opened in the messenger's News tab. */
|
|
647
|
-
newsSeen: (key) => `guidely:news:${key}`
|
|
648
|
-
};
|
|
649
|
-
|
|
650
|
-
// ../sdk-core/dist/chunk-AKNE7D5M.js
|
|
617
|
+
// ../sdk-core/dist/chunk-C4WWSF67.js
|
|
651
618
|
var __defProp2 = Object.defineProperty;
|
|
652
619
|
var __defNormalProp2 = (obj, key, value) => key in obj ? __defProp2(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
653
620
|
var __publicField2 = (obj, key, value) => __defNormalProp2(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
621
|
+
var MAX_RETRY_AFTER_MS = 60 * 60 * 1e3;
|
|
622
|
+
var OFFLINE_WAIT_MS = 3e4;
|
|
623
|
+
var ServerHealth = class {
|
|
624
|
+
constructor(opts = {}) {
|
|
625
|
+
__publicField2(this, "failures", 0);
|
|
626
|
+
__publicField2(this, "openUntil", 0);
|
|
627
|
+
__publicField2(this, "lastOkAt", 0);
|
|
628
|
+
__publicField2(this, "listeners", /* @__PURE__ */ new Set());
|
|
629
|
+
__publicField2(this, "threshold");
|
|
630
|
+
__publicField2(this, "baseMs");
|
|
631
|
+
__publicField2(this, "maxMs");
|
|
632
|
+
__publicField2(this, "now");
|
|
633
|
+
__publicField2(this, "random");
|
|
634
|
+
this.threshold = opts.threshold ?? 3;
|
|
635
|
+
this.baseMs = opts.baseMs ?? 5e3;
|
|
636
|
+
this.maxMs = opts.maxMs ?? 5 * 60 * 1e3;
|
|
637
|
+
this.now = opts.now ?? Date.now;
|
|
638
|
+
this.random = opts.random ?? Math.random;
|
|
639
|
+
}
|
|
640
|
+
/** The server answered (2xx, 3xx, or a 4xx other than 429): it is up. */
|
|
641
|
+
success() {
|
|
642
|
+
const wasDown = this.failures > 0 || this.openUntil > 0;
|
|
643
|
+
this.failures = 0;
|
|
644
|
+
this.openUntil = 0;
|
|
645
|
+
this.lastOkAt = this.now();
|
|
646
|
+
if (wasDown) this.notify();
|
|
647
|
+
}
|
|
648
|
+
/** The request failed or the server is overloaded. `retryAfterMs` comes from a `Retry-After` header. */
|
|
649
|
+
failure(retryAfterMs) {
|
|
650
|
+
this.failures += 1;
|
|
651
|
+
const now = this.now();
|
|
652
|
+
if (retryAfterMs != null && retryAfterMs > 0) {
|
|
653
|
+
this.openUntil = Math.max(this.openUntil, now + Math.min(retryAfterMs, MAX_RETRY_AFTER_MS));
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (this.failures < this.threshold) return;
|
|
657
|
+
const cooldown = Math.min(this.maxMs, this.baseMs * 2 ** (this.failures - this.threshold));
|
|
658
|
+
this.openUntil = Math.max(this.openUntil, now + cooldown * (0.8 + this.random() * 0.4));
|
|
659
|
+
}
|
|
660
|
+
/** Milliseconds background traffic should wait before trying again (0: send now). */
|
|
661
|
+
waitMs() {
|
|
662
|
+
if (typeof navigator !== "undefined" && navigator.onLine === false) return OFFLINE_WAIT_MS;
|
|
663
|
+
return Math.max(0, this.openUntil - this.now());
|
|
664
|
+
}
|
|
665
|
+
canSend() {
|
|
666
|
+
return this.waitMs() === 0;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* The last request got an answer. Unload beacons can't report back, so what they carry is only
|
|
670
|
+
* forgotten while this holds; otherwise it stays queued and is sent again (the server drops duplicates).
|
|
671
|
+
*/
|
|
672
|
+
get reachable() {
|
|
673
|
+
return this.failures === 0 && this.lastOkAt > 0;
|
|
674
|
+
}
|
|
675
|
+
/** The browser came back online: end the cooldown and let queued traffic try straight away. */
|
|
676
|
+
online() {
|
|
677
|
+
this.openUntil = 0;
|
|
678
|
+
this.notify();
|
|
679
|
+
}
|
|
680
|
+
/** Called when the server answers again after failures, or the browser comes back online. */
|
|
681
|
+
onRecover(cb) {
|
|
682
|
+
this.listeners.add(cb);
|
|
683
|
+
return () => this.listeners.delete(cb);
|
|
684
|
+
}
|
|
685
|
+
notify() {
|
|
686
|
+
for (const cb of Array.from(this.listeners)) {
|
|
687
|
+
try {
|
|
688
|
+
cb();
|
|
689
|
+
} catch {
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
function parseRetryAfter(value, now = Date.now()) {
|
|
695
|
+
if (!value) return null;
|
|
696
|
+
const trimmed = value.trim();
|
|
697
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed) * 1e3;
|
|
698
|
+
const at = Date.parse(trimmed);
|
|
699
|
+
return Number.isNaN(at) ? null : Math.max(0, at - now);
|
|
700
|
+
}
|
|
701
|
+
function isServerFailure(status) {
|
|
702
|
+
return status === 429 || status >= 500;
|
|
703
|
+
}
|
|
704
|
+
var STREAM_RESPONSE_TIMEOUT_MS = 3e4;
|
|
705
|
+
var STREAM_IDLE_TIMEOUT_MS = 6e4;
|
|
654
706
|
var Api = class {
|
|
655
|
-
constructor(baseUrl, key, fetchImpl) {
|
|
707
|
+
constructor(baseUrl, key, fetchImpl, health) {
|
|
656
708
|
__publicField2(this, "baseUrl", baseUrl);
|
|
657
709
|
__publicField2(this, "key", key);
|
|
658
710
|
__publicField2(this, "fetchImpl", fetchImpl);
|
|
711
|
+
__publicField2(this, "health");
|
|
712
|
+
this.health = health ?? new ServerHealth();
|
|
713
|
+
}
|
|
714
|
+
/** Feeds an answer (or a failed request, `null`) into {@link health}. */
|
|
715
|
+
observe(res) {
|
|
716
|
+
if (!res) this.health.failure();
|
|
717
|
+
else if (isServerFailure(res.status)) this.health.failure(parseRetryAfter(res.headers?.get?.("retry-after")));
|
|
718
|
+
else this.health.success();
|
|
659
719
|
}
|
|
660
720
|
url(path, params = {}) {
|
|
661
721
|
const query = Object.entries({ key: this.key, ...params }).filter((e) => e[1] != null).map(([k, v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`).join("&");
|
|
@@ -675,15 +735,22 @@ var Api = class {
|
|
|
675
735
|
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
676
736
|
const timer = ac ? setTimeout(() => ac.abort(), options.timeoutMs ?? 1e4) : null;
|
|
677
737
|
try {
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
738
|
+
let res;
|
|
739
|
+
try {
|
|
740
|
+
res = await f(this.url(path, options.params), {
|
|
741
|
+
method,
|
|
742
|
+
headers,
|
|
743
|
+
body: options.body === void 0 ? void 0 : form ? options.body : JSON.stringify(options.body),
|
|
744
|
+
keepalive: options.keepalive,
|
|
745
|
+
signal: ac?.signal,
|
|
746
|
+
credentials: "omit",
|
|
747
|
+
mode: "cors"
|
|
748
|
+
});
|
|
749
|
+
} catch (err) {
|
|
750
|
+
this.observe(null);
|
|
751
|
+
throw err;
|
|
752
|
+
}
|
|
753
|
+
this.observe(res);
|
|
687
754
|
let data = null;
|
|
688
755
|
if (res.status !== 304 && res.status !== 204) {
|
|
689
756
|
try {
|
|
@@ -709,30 +776,53 @@ var Api = class {
|
|
|
709
776
|
}
|
|
710
777
|
/**
|
|
711
778
|
* POST that may answer with server-sent events. Each event is passed to `onEvent`; a JSON answer is
|
|
712
|
-
* returned instead.
|
|
779
|
+
* returned instead. Scout replies can take a while, so there is no overall timeout: the response
|
|
780
|
+
* must start within 30 s and then never go quiet for more than 60 s (the server sends keepalives),
|
|
781
|
+
* otherwise the request is aborted and rejects.
|
|
713
782
|
*/
|
|
714
783
|
async stream(path, body, onEvent, headers = {}) {
|
|
715
784
|
const f = this.fetcher;
|
|
716
785
|
if (!f) throw new Error("fetch is not available");
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
let
|
|
786
|
+
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
787
|
+
let timer = null;
|
|
788
|
+
const arm = (ms) => {
|
|
789
|
+
if (!ac) return;
|
|
790
|
+
if (timer) clearTimeout(timer);
|
|
791
|
+
timer = setTimeout(() => ac.abort(), ms);
|
|
792
|
+
};
|
|
793
|
+
arm(STREAM_RESPONSE_TIMEOUT_MS);
|
|
794
|
+
try {
|
|
795
|
+
let res;
|
|
727
796
|
try {
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
797
|
+
res = await f(this.url(path), {
|
|
798
|
+
method: "POST",
|
|
799
|
+
headers: { ...headers, "X-Guidely-Key": this.key, Accept: "text/event-stream, application/json", "Content-Type": "application/json" },
|
|
800
|
+
body: JSON.stringify(body),
|
|
801
|
+
signal: ac?.signal,
|
|
802
|
+
credentials: "omit",
|
|
803
|
+
mode: "cors"
|
|
804
|
+
});
|
|
805
|
+
} catch (err) {
|
|
806
|
+
this.observe(null);
|
|
807
|
+
throw err;
|
|
808
|
+
}
|
|
809
|
+
this.observe(res);
|
|
810
|
+
const type = res.headers?.get?.("content-type") ?? "";
|
|
811
|
+
if (!type.includes("text/event-stream") || !res.body) {
|
|
812
|
+
let data = null;
|
|
813
|
+
try {
|
|
814
|
+
data = await res.json();
|
|
815
|
+
} catch {
|
|
816
|
+
data = null;
|
|
817
|
+
}
|
|
818
|
+
return { status: res.status, ok: res.ok, data, etag: null };
|
|
731
819
|
}
|
|
732
|
-
|
|
820
|
+
arm(STREAM_IDLE_TIMEOUT_MS);
|
|
821
|
+
await readEventStream(res.body, onEvent, () => arm(STREAM_IDLE_TIMEOUT_MS));
|
|
822
|
+
return { status: res.status, ok: res.ok, data: null, etag: null };
|
|
823
|
+
} finally {
|
|
824
|
+
if (timer) clearTimeout(timer);
|
|
733
825
|
}
|
|
734
|
-
await readEventStream(res.body, onEvent);
|
|
735
|
-
return { status: res.status, ok: res.ok, data: null, etag: null };
|
|
736
826
|
}
|
|
737
827
|
/** `navigator.sendBeacon` when available (page unload); returns false when it could not be queued. */
|
|
738
828
|
beacon(path, body) {
|
|
@@ -763,7 +853,7 @@ async function withRetry(send, attempts = 3, baseMs = 500) {
|
|
|
763
853
|
function postWithRetry(api, path, body, attempts = 3, baseMs = 500) {
|
|
764
854
|
return withRetry(() => api.post(path, body), attempts, baseMs);
|
|
765
855
|
}
|
|
766
|
-
async function readEventStream(body, onEvent) {
|
|
856
|
+
async function readEventStream(body, onEvent, onChunk) {
|
|
767
857
|
const reader = body.getReader();
|
|
768
858
|
const decoder = new TextDecoder();
|
|
769
859
|
let buffer = "";
|
|
@@ -786,6 +876,7 @@ async function readEventStream(body, onEvent) {
|
|
|
786
876
|
};
|
|
787
877
|
for (; ; ) {
|
|
788
878
|
const { done, value } = await reader.read();
|
|
879
|
+
if (value) onChunk?.();
|
|
789
880
|
if (value) buffer += decoder.decode(value, { stream: true }).replace(/\r\n?/g, "\n");
|
|
790
881
|
let sep;
|
|
791
882
|
while ((sep = buffer.indexOf("\n\n")) >= 0) {
|
|
@@ -796,8 +887,44 @@ async function readEventStream(body, onEvent) {
|
|
|
796
887
|
}
|
|
797
888
|
if (buffer.trim()) dispatch(buffer);
|
|
798
889
|
}
|
|
890
|
+
function readJson(store, key, fallback) {
|
|
891
|
+
const raw = store.get(key);
|
|
892
|
+
if (raw == null) return fallback;
|
|
893
|
+
try {
|
|
894
|
+
return JSON.parse(raw);
|
|
895
|
+
} catch {
|
|
896
|
+
return fallback;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
function writeJson(store, key, value) {
|
|
900
|
+
try {
|
|
901
|
+
store.set(key, JSON.stringify(value));
|
|
902
|
+
} catch {
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
var KEYS = {
|
|
906
|
+
anon: "guidely:anon",
|
|
907
|
+
seen: "guidely:seen",
|
|
908
|
+
config: (key) => `guidely:config:${key}`,
|
|
909
|
+
etag: (key) => `guidely:etag:${key}`,
|
|
910
|
+
state: (key) => `guidely:state:${key}`,
|
|
911
|
+
user: (key) => `guidely:user:${key}`,
|
|
912
|
+
queue: (key) => `guidely:queue:${key}`,
|
|
913
|
+
/** Survey answers and guide/checklist progress not yet accepted by the server. */
|
|
914
|
+
outbox: (key) => `guidely:outbox:${key}`,
|
|
915
|
+
session: "guidely:session",
|
|
916
|
+
active: "guidely:active",
|
|
917
|
+
authoring: "guidely:authoring",
|
|
918
|
+
/** How-to capture session marker ({ token, … }) and its not-yet-uploaded steps. */
|
|
919
|
+
capture: "guidely:capture",
|
|
920
|
+
capturePending: "guidely:capture:pending",
|
|
921
|
+
/** Messenger conversations this browser can read: [{ id, token }]. */
|
|
922
|
+
conversations: (key) => `guidely:conversations:${key}`,
|
|
923
|
+
/** Announcement ids opened in the messenger's News tab. */
|
|
924
|
+
newsSeen: (key) => `guidely:news:${key}`
|
|
925
|
+
};
|
|
799
926
|
|
|
800
|
-
// ../sdk-core/dist/chunk-
|
|
927
|
+
// ../sdk-core/dist/chunk-GDJB466J.js
|
|
801
928
|
var EventQueue = class {
|
|
802
929
|
constructor(opts) {
|
|
803
930
|
__publicField2(this, "opts", opts);
|
|
@@ -808,17 +935,34 @@ var EventQueue = class {
|
|
|
808
935
|
__publicField2(this, "inflightIds", /* @__PURE__ */ new Set());
|
|
809
936
|
__publicField2(this, "attempt", 0);
|
|
810
937
|
__publicField2(this, "destroyed", false);
|
|
938
|
+
__publicField2(this, "offRecover", null);
|
|
811
939
|
__publicField2(this, "flushIntervalMs");
|
|
812
940
|
__publicField2(this, "maxQueue");
|
|
813
941
|
__publicField2(this, "batchSize");
|
|
814
942
|
__publicField2(this, "maxAttempts");
|
|
815
943
|
this.flushIntervalMs = opts.flushIntervalMs ?? 2e3;
|
|
816
|
-
this.maxQueue = opts.maxQueue ??
|
|
944
|
+
this.maxQueue = opts.maxQueue ?? 500;
|
|
817
945
|
this.batchSize = opts.batchSize ?? 50;
|
|
818
946
|
this.maxAttempts = opts.maxAttempts ?? 5;
|
|
819
947
|
const persisted = readJson(opts.store, opts.storageKey, []);
|
|
820
948
|
if (Array.isArray(persisted)) this.events = persisted.filter((e) => e && typeof e.eventId === "string").slice(-this.maxQueue);
|
|
821
949
|
if (this.events.length) this.schedule();
|
|
950
|
+
this.offRecover = opts.health?.onRecover(() => this.retryNow()) ?? null;
|
|
951
|
+
}
|
|
952
|
+
/** The API is back: drop any pending backoff and send what is queued. */
|
|
953
|
+
retryNow() {
|
|
954
|
+
if (this.destroyed) return;
|
|
955
|
+
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
956
|
+
this.retryTimer = null;
|
|
957
|
+
this.attempt = 0;
|
|
958
|
+
void this.flush();
|
|
959
|
+
}
|
|
960
|
+
retryIn(ms) {
|
|
961
|
+
if (this.retryTimer || this.destroyed) return;
|
|
962
|
+
this.retryTimer = setTimeout(() => {
|
|
963
|
+
this.retryTimer = null;
|
|
964
|
+
void this.flush();
|
|
965
|
+
}, ms);
|
|
822
966
|
}
|
|
823
967
|
get size() {
|
|
824
968
|
return this.events.length;
|
|
@@ -852,6 +996,8 @@ var EventQueue = class {
|
|
|
852
996
|
clearTimeout(this.timer);
|
|
853
997
|
this.timer = null;
|
|
854
998
|
}
|
|
999
|
+
const wait = this.opts.health?.waitMs() ?? 0;
|
|
1000
|
+
if (wait > 0) return this.retryIn(wait);
|
|
855
1001
|
this.inflight = true;
|
|
856
1002
|
const batch = this.events.slice(0, this.batchSize);
|
|
857
1003
|
this.inflightIds = new Set(batch.map((e) => e.eventId));
|
|
@@ -875,26 +1021,20 @@ var EventQueue = class {
|
|
|
875
1021
|
if (this.events.length) this.schedule();
|
|
876
1022
|
return;
|
|
877
1023
|
}
|
|
878
|
-
this.attempt
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
return;
|
|
882
|
-
}
|
|
883
|
-
this.retryTimer = setTimeout(() => {
|
|
884
|
-
this.retryTimer = null;
|
|
885
|
-
void this.flush();
|
|
886
|
-
}, backoffMs(this.attempt - 1));
|
|
1024
|
+
this.attempt = Math.min(this.attempt + 1, this.maxAttempts);
|
|
1025
|
+
const delay = this.attempt >= this.maxAttempts ? 6e4 : backoffMs(this.attempt - 1);
|
|
1026
|
+
this.retryIn(Math.max(delay, this.opts.health?.waitMs() ?? 0));
|
|
887
1027
|
}
|
|
888
1028
|
/** Best-effort synchronous flush on unload using `sendBeacon`. */
|
|
889
1029
|
flushSync() {
|
|
890
1030
|
if (this.destroyed || !this.opts.enabled() || !this.opts.beacon) return;
|
|
891
1031
|
const batch = this.events.filter((e) => !this.inflightIds.has(e.eventId)).slice(0, this.batchSize);
|
|
892
1032
|
if (!batch.length) return;
|
|
893
|
-
if (this.opts.beacon(batch)) {
|
|
1033
|
+
if (this.opts.beacon(batch) && (this.opts.health?.reachable ?? true)) {
|
|
894
1034
|
const ids = new Set(batch.map((e) => e.eventId));
|
|
895
1035
|
this.events = this.events.filter((e) => !ids.has(e.eventId));
|
|
896
|
-
this.persist();
|
|
897
1036
|
}
|
|
1037
|
+
this.persist();
|
|
898
1038
|
}
|
|
899
1039
|
clear() {
|
|
900
1040
|
this.events = [];
|
|
@@ -902,6 +1042,7 @@ var EventQueue = class {
|
|
|
902
1042
|
}
|
|
903
1043
|
destroy() {
|
|
904
1044
|
this.destroyed = true;
|
|
1045
|
+
this.offRecover?.();
|
|
905
1046
|
if (this.timer) clearTimeout(this.timer);
|
|
906
1047
|
if (this.retryTimer) clearTimeout(this.retryTimer);
|
|
907
1048
|
this.timer = null;
|
|
@@ -909,6 +1050,88 @@ var EventQueue = class {
|
|
|
909
1050
|
this.persist();
|
|
910
1051
|
}
|
|
911
1052
|
};
|
|
1053
|
+
var Outbox = class {
|
|
1054
|
+
constructor(opts) {
|
|
1055
|
+
__publicField2(this, "opts", opts);
|
|
1056
|
+
__publicField2(this, "items");
|
|
1057
|
+
__publicField2(this, "sending", false);
|
|
1058
|
+
__publicField2(this, "attempt", 0);
|
|
1059
|
+
__publicField2(this, "timer", null);
|
|
1060
|
+
__publicField2(this, "destroyed", false);
|
|
1061
|
+
__publicField2(this, "offRecover");
|
|
1062
|
+
__publicField2(this, "maxItems");
|
|
1063
|
+
__publicField2(this, "maxAgeMs");
|
|
1064
|
+
this.maxItems = opts.maxItems ?? 50;
|
|
1065
|
+
this.maxAgeMs = opts.maxAgeMs ?? 7 * 24 * 60 * 60 * 1e3;
|
|
1066
|
+
const stored = readJson(opts.store, opts.storageKey, []);
|
|
1067
|
+
const cutoff = Date.now() - this.maxAgeMs;
|
|
1068
|
+
this.items = Array.isArray(stored) ? stored.filter((i) => i && typeof i.path === "string" && typeof i.key === "string" && i.createdAt > cutoff).slice(-this.maxItems) : [];
|
|
1069
|
+
this.offRecover = opts.health?.onRecover(() => {
|
|
1070
|
+
this.attempt = 0;
|
|
1071
|
+
this.retryIn(0);
|
|
1072
|
+
}) ?? null;
|
|
1073
|
+
if (this.items.length) this.retryIn(0);
|
|
1074
|
+
}
|
|
1075
|
+
get size() {
|
|
1076
|
+
return this.items.length;
|
|
1077
|
+
}
|
|
1078
|
+
/** Queues `POST path` and sends it now (or as soon as the API is reachable). */
|
|
1079
|
+
add(key, path, body) {
|
|
1080
|
+
if (this.destroyed) return;
|
|
1081
|
+
this.items = this.items.filter((i) => i.key !== key);
|
|
1082
|
+
this.items.push({ key, path, body, createdAt: Date.now() });
|
|
1083
|
+
if (this.items.length > this.maxItems) this.items.splice(0, this.items.length - this.maxItems);
|
|
1084
|
+
this.persist();
|
|
1085
|
+
void this.flush();
|
|
1086
|
+
}
|
|
1087
|
+
persist() {
|
|
1088
|
+
writeJson(this.opts.store, this.opts.storageKey, this.items);
|
|
1089
|
+
}
|
|
1090
|
+
retryIn(ms) {
|
|
1091
|
+
if (this.destroyed) return;
|
|
1092
|
+
if (this.timer) clearTimeout(this.timer);
|
|
1093
|
+
this.timer = setTimeout(() => {
|
|
1094
|
+
this.timer = null;
|
|
1095
|
+
void this.flush();
|
|
1096
|
+
}, ms);
|
|
1097
|
+
}
|
|
1098
|
+
/** Sends queued requests in order, one at a time. */
|
|
1099
|
+
async flush() {
|
|
1100
|
+
if (this.destroyed || this.sending || !this.items.length) return;
|
|
1101
|
+
const wait = this.opts.health?.waitMs() ?? 0;
|
|
1102
|
+
if (wait > 0) return this.retryIn(wait);
|
|
1103
|
+
this.sending = true;
|
|
1104
|
+
try {
|
|
1105
|
+
while (this.items.length && !this.destroyed) {
|
|
1106
|
+
const item = this.items[0];
|
|
1107
|
+
let retry = false;
|
|
1108
|
+
try {
|
|
1109
|
+
const res = await this.opts.api.post(item.path, item.body);
|
|
1110
|
+
retry = !res.ok && (res.status === 429 || res.status >= 500);
|
|
1111
|
+
} catch (err) {
|
|
1112
|
+
retry = true;
|
|
1113
|
+
this.opts.onError?.(err);
|
|
1114
|
+
}
|
|
1115
|
+
if (retry) {
|
|
1116
|
+
this.attempt += 1;
|
|
1117
|
+
this.retryIn(Math.max(backoffMs(this.attempt - 1, 1e3, 6e4), this.opts.health?.waitMs() ?? 0));
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
this.attempt = 0;
|
|
1121
|
+
this.items = this.items.filter((i) => i !== item);
|
|
1122
|
+
this.persist();
|
|
1123
|
+
}
|
|
1124
|
+
} finally {
|
|
1125
|
+
this.sending = false;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
destroy() {
|
|
1129
|
+
this.destroyed = true;
|
|
1130
|
+
this.offRecover?.();
|
|
1131
|
+
if (this.timer) clearTimeout(this.timer);
|
|
1132
|
+
this.timer = null;
|
|
1133
|
+
}
|
|
1134
|
+
};
|
|
912
1135
|
var Emitter = class {
|
|
913
1136
|
constructor(onError) {
|
|
914
1137
|
__publicField2(this, "onError", onError);
|
|
@@ -983,6 +1206,10 @@ function isWithinSchedule(guide, now = Date.now()) {
|
|
|
983
1206
|
if (s.endAt && now > Date.parse(s.endAt)) return false;
|
|
984
1207
|
return true;
|
|
985
1208
|
}
|
|
1209
|
+
function dismissedAfterMerge(local, server) {
|
|
1210
|
+
if (server.dismissResetAt && server.dismissResetAt !== local?.dismissResetAt) return server.dismissedAt ?? null;
|
|
1211
|
+
return local?.dismissedAt ?? server.dismissedAt ?? null;
|
|
1212
|
+
}
|
|
986
1213
|
|
|
987
1214
|
// src/core/client.ts
|
|
988
1215
|
var import_flags2 = require("@guidelytech/flags");
|
|
@@ -1460,12 +1687,29 @@ function createSseParser(onEvent) {
|
|
|
1460
1687
|
}
|
|
1461
1688
|
};
|
|
1462
1689
|
}
|
|
1690
|
+
var STREAM_RESPONSE_TIMEOUT_MS2 = 3e4;
|
|
1691
|
+
var STREAM_IDLE_TIMEOUT_MS2 = 6e4;
|
|
1463
1692
|
function xhrStreamPost() {
|
|
1464
1693
|
if (typeof XMLHttpRequest === "undefined") return null;
|
|
1465
1694
|
return (url, headers, body, onChunk) => new Promise((resolve, reject) => {
|
|
1466
1695
|
const xhr = new XMLHttpRequest();
|
|
1467
1696
|
let seen = 0;
|
|
1468
1697
|
let streamed = false;
|
|
1698
|
+
let timer = null;
|
|
1699
|
+
let settled = false;
|
|
1700
|
+
const settle = (fn) => {
|
|
1701
|
+
if (settled) return;
|
|
1702
|
+
settled = true;
|
|
1703
|
+
if (timer) clearTimeout(timer);
|
|
1704
|
+
fn();
|
|
1705
|
+
};
|
|
1706
|
+
const arm = (ms, what) => {
|
|
1707
|
+
if (timer) clearTimeout(timer);
|
|
1708
|
+
timer = setTimeout(() => {
|
|
1709
|
+
settle(() => reject(new Error(`Scout ${what} timed out`)));
|
|
1710
|
+
xhr.abort();
|
|
1711
|
+
}, ms);
|
|
1712
|
+
};
|
|
1469
1713
|
const pump = () => {
|
|
1470
1714
|
const type = xhr.getResponseHeader("content-type") || "";
|
|
1471
1715
|
if (!type.includes("text/event-stream")) return;
|
|
@@ -1478,8 +1722,15 @@ function xhrStreamPost() {
|
|
|
1478
1722
|
};
|
|
1479
1723
|
xhr.open("POST", url);
|
|
1480
1724
|
for (const [k, v] of Object.entries(headers)) xhr.setRequestHeader(k, v);
|
|
1481
|
-
xhr.
|
|
1482
|
-
|
|
1725
|
+
xhr.onreadystatechange = () => {
|
|
1726
|
+
if (!settled && xhr.readyState >= 2 && xhr.readyState < 4) arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
|
|
1727
|
+
};
|
|
1728
|
+
xhr.onprogress = () => {
|
|
1729
|
+
if (settled) return;
|
|
1730
|
+
arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
|
|
1731
|
+
pump();
|
|
1732
|
+
};
|
|
1733
|
+
xhr.onload = () => settle(() => {
|
|
1483
1734
|
pump();
|
|
1484
1735
|
let json = null;
|
|
1485
1736
|
if (!streamed) {
|
|
@@ -1490,27 +1741,74 @@ function xhrStreamPost() {
|
|
|
1490
1741
|
}
|
|
1491
1742
|
}
|
|
1492
1743
|
resolve({ status: xhr.status, json, streamed });
|
|
1493
|
-
};
|
|
1494
|
-
xhr.onerror = () => reject(new Error("Network request failed"));
|
|
1744
|
+
});
|
|
1745
|
+
xhr.onerror = () => settle(() => reject(new Error("Network request failed")));
|
|
1746
|
+
arm(STREAM_RESPONSE_TIMEOUT_MS2, "request");
|
|
1495
1747
|
xhr.send(JSON.stringify(body));
|
|
1496
1748
|
});
|
|
1497
1749
|
}
|
|
1750
|
+
function bodyReader(res) {
|
|
1751
|
+
const body = res.body;
|
|
1752
|
+
const Decoder = globalThis.TextDecoder;
|
|
1753
|
+
return body && typeof body.getReader === "function" && Decoder ? { reader: body.getReader(), decoder: new Decoder() } : null;
|
|
1754
|
+
}
|
|
1498
1755
|
function fetchStreamPost(fetchImpl) {
|
|
1499
1756
|
return async (url, headers, body, onChunk) => {
|
|
1500
|
-
const
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1757
|
+
const ac = typeof AbortController !== "undefined" ? new AbortController() : null;
|
|
1758
|
+
let timer = null;
|
|
1759
|
+
let timedOut = "";
|
|
1760
|
+
let fail = () => {
|
|
1761
|
+
};
|
|
1762
|
+
const expired = new Promise((_, reject) => fail = reject);
|
|
1763
|
+
expired.catch(() => {
|
|
1764
|
+
});
|
|
1765
|
+
const arm = (ms, what) => {
|
|
1766
|
+
if (timer) clearTimeout(timer);
|
|
1767
|
+
timer = setTimeout(() => {
|
|
1768
|
+
timedOut = what;
|
|
1769
|
+
ac?.abort();
|
|
1770
|
+
fail(new Error(`Scout ${what} timed out`));
|
|
1771
|
+
}, ms);
|
|
1772
|
+
};
|
|
1773
|
+
const race = (p) => Promise.race([p, expired]);
|
|
1774
|
+
arm(STREAM_RESPONSE_TIMEOUT_MS2, "request");
|
|
1508
1775
|
try {
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1776
|
+
const res = await race(fetchImpl(url, { method: "POST", headers, body: JSON.stringify(body), signal: ac?.signal }));
|
|
1777
|
+
arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
|
|
1778
|
+
const type = res.headers.get("content-type") || "";
|
|
1779
|
+
const sse = type.includes("text/event-stream");
|
|
1780
|
+
const streaming = sse ? bodyReader(res) : null;
|
|
1781
|
+
if (streaming) {
|
|
1782
|
+
const { reader, decoder } = streaming;
|
|
1783
|
+
for (; ; ) {
|
|
1784
|
+
const { done, value } = await race(reader.read());
|
|
1785
|
+
if (value) {
|
|
1786
|
+
arm(STREAM_IDLE_TIMEOUT_MS2, "reply");
|
|
1787
|
+
onChunk(decoder.decode(value, { stream: true }));
|
|
1788
|
+
}
|
|
1789
|
+
if (done) break;
|
|
1790
|
+
}
|
|
1791
|
+
const rest = decoder.decode();
|
|
1792
|
+
if (rest) onChunk(rest);
|
|
1793
|
+
return { status: res.status, json: null, streamed: true };
|
|
1794
|
+
}
|
|
1795
|
+
const text = await race(res.text());
|
|
1796
|
+
if (sse) {
|
|
1797
|
+
onChunk(text);
|
|
1798
|
+
return { status: res.status, json: null, streamed: true };
|
|
1799
|
+
}
|
|
1800
|
+
let json = null;
|
|
1801
|
+
try {
|
|
1802
|
+
json = JSON.parse(text || "null");
|
|
1803
|
+
} catch {
|
|
1804
|
+
json = null;
|
|
1805
|
+
}
|
|
1806
|
+
return { status: res.status, json, streamed: false };
|
|
1807
|
+
} catch (err) {
|
|
1808
|
+
throw timedOut ? new Error(`Scout ${timedOut} timed out`) : err;
|
|
1809
|
+
} finally {
|
|
1810
|
+
if (timer) clearTimeout(timer);
|
|
1512
1811
|
}
|
|
1513
|
-
return { status: res.status, json, streamed: false };
|
|
1514
1812
|
};
|
|
1515
1813
|
}
|
|
1516
1814
|
var byNewest = (a, b) => Date.parse(b.lastMessageAt) - Date.parse(a.lastMessageAt);
|
|
@@ -1695,7 +1993,15 @@ var Messenger = class {
|
|
|
1695
1993
|
}
|
|
1696
1994
|
});
|
|
1697
1995
|
try {
|
|
1698
|
-
|
|
1996
|
+
let res;
|
|
1997
|
+
try {
|
|
1998
|
+
res = await this.deps.streamPost(this.url(path), headers, payload, (chunk) => parser.feed(chunk));
|
|
1999
|
+
} catch (err) {
|
|
2000
|
+
this.deps.health?.failure();
|
|
2001
|
+
throw err;
|
|
2002
|
+
}
|
|
2003
|
+
if (isServerFailure(res.status)) this.deps.health?.failure();
|
|
2004
|
+
else this.deps.health?.success();
|
|
1699
2005
|
parser.end();
|
|
1700
2006
|
if (res.status < 200 || res.status >= 300) throw new Error(`Request failed (${res.status})`);
|
|
1701
2007
|
if (!res.streamed && res.json) {
|
|
@@ -1882,9 +2188,20 @@ var PersistentStore = class {
|
|
|
1882
2188
|
|
|
1883
2189
|
// src/core/client.ts
|
|
1884
2190
|
var SDK_NAME = "@guidelytech/react-native";
|
|
1885
|
-
var SDK_VERSION = "0.
|
|
2191
|
+
var SDK_VERSION = "0.10.0";
|
|
1886
2192
|
var DEFAULT_API_URL = "https://guidelytech.com";
|
|
1887
2193
|
var PAGE_SCOPED = /* @__PURE__ */ new Set(["page_load", "url_match", "element_click", "element_visible", "delay"]);
|
|
2194
|
+
var CACHED_START_MS = 1500;
|
|
2195
|
+
function settlesWithin(promise, ms) {
|
|
2196
|
+
return new Promise((resolve) => {
|
|
2197
|
+
const timer = setTimeout(() => resolve(false), ms);
|
|
2198
|
+
const done = () => {
|
|
2199
|
+
clearTimeout(timer);
|
|
2200
|
+
resolve(true);
|
|
2201
|
+
};
|
|
2202
|
+
promise.then(done, done);
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
1888
2205
|
var nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
|
|
1889
2206
|
var emptyState = () => ({ guides: {}, checklists: {}, surveysSubmitted: [], audienceIds: [] });
|
|
1890
2207
|
var newSession = () => ({ id: `s_${randomId(16)}`, startedAt: nowIso(), lastActiveAt: nowIso(), shownGuides: [], events: [] });
|
|
@@ -1897,6 +2214,8 @@ var GuidelyClient = class {
|
|
|
1897
2214
|
__publicField(this, "store");
|
|
1898
2215
|
__publicField(this, "emitter");
|
|
1899
2216
|
__publicField(this, "queue", null);
|
|
2217
|
+
/** Survey answers, guide completions/dismissals and checklist progress, kept until the server has them. */
|
|
2218
|
+
__publicField(this, "outbox", null);
|
|
1900
2219
|
__publicField(this, "config", null);
|
|
1901
2220
|
__publicField(this, "configFetchedAt", 0);
|
|
1902
2221
|
__publicField(this, "etag", null);
|
|
@@ -1959,7 +2278,7 @@ var GuidelyClient = class {
|
|
|
1959
2278
|
this.emitter = new Emitter((err) => this.reportError(err));
|
|
1960
2279
|
this.api = new Api(this.options.apiUrl, this.options.projectId, options.fetch);
|
|
1961
2280
|
this.store = new PersistentStore(options.storage ?? null, (err) => this.log("storage error", err));
|
|
1962
|
-
if (!options.storage) this.log("no `storage` passed: identity
|
|
2281
|
+
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.");
|
|
1963
2282
|
this.cleanups.push(this.targets.onTap((id) => this.handleTap(id)));
|
|
1964
2283
|
if (options.user) this.user = { ...options.user };
|
|
1965
2284
|
this.flags = this.createFlags(options.flags);
|
|
@@ -2010,7 +2329,7 @@ var GuidelyClient = class {
|
|
|
2010
2329
|
async bootstrap() {
|
|
2011
2330
|
const key = this.options.projectId;
|
|
2012
2331
|
try {
|
|
2013
|
-
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)]);
|
|
2332
|
+
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)]);
|
|
2014
2333
|
if (this.destroyed) return;
|
|
2015
2334
|
this.anonymousId = this.store.get(KEYS.anon) || "";
|
|
2016
2335
|
if (!this.anonymousId) {
|
|
@@ -2030,22 +2349,27 @@ var GuidelyClient = class {
|
|
|
2030
2349
|
store: this.store,
|
|
2031
2350
|
storageKey: KEYS.queue(key),
|
|
2032
2351
|
enabled: () => this.analyticsEnabled(),
|
|
2352
|
+
health: this.api.health,
|
|
2033
2353
|
send: async (events) => {
|
|
2034
2354
|
const res = await this.api.post("/events", { events, sentAt: nowIso(), sdk: { name: SDK_NAME, version: SDK_VERSION } });
|
|
2035
2355
|
return { ok: res.ok, retry: !(res.status >= 400 && res.status < 500 && res.status !== 429) };
|
|
2036
2356
|
},
|
|
2037
2357
|
onError: (err) => this.reportError(err)
|
|
2038
2358
|
});
|
|
2359
|
+
this.outbox = new Outbox({ api: this.api, store: this.store, storageKey: KEYS.outbox(key), health: this.api.health });
|
|
2039
2360
|
this.hydrated = true;
|
|
2040
2361
|
const cached = readJson(this.store, KEYS.config(key), null);
|
|
2041
2362
|
this.etag = this.store.get(KEYS.etag(key));
|
|
2042
2363
|
if (cached && Array.isArray(cached.guides)) this.config = forPlatform(cached, this.bridge.device.platform);
|
|
2043
2364
|
this.syncMessenger();
|
|
2044
2365
|
this.pending.splice(0).forEach((fn) => fn());
|
|
2045
|
-
|
|
2366
|
+
const before = this.config?.configVersion;
|
|
2367
|
+
const fresh = this.fetchConfig();
|
|
2368
|
+
if (!this.config) await fresh;
|
|
2369
|
+
else if (!await settlesWithin(fresh, CACHED_START_MS)) void fresh.then(() => this.applyConfigChange(before));
|
|
2046
2370
|
if (this.destroyed) return;
|
|
2047
2371
|
this.wireTimers();
|
|
2048
|
-
if (this.user?.id) await this.identify(this.user.id, this.user.traits, this.user.account);
|
|
2372
|
+
if (this.user?.id) await settlesWithin(this.identify(this.user.id, this.user.traits, this.user.account), CACHED_START_MS);
|
|
2049
2373
|
if (this.destroyed) return;
|
|
2050
2374
|
this.isReady = true;
|
|
2051
2375
|
if (this.config) this.emitter.emit("ready", this.config);
|
|
@@ -2085,7 +2409,9 @@ var GuidelyClient = class {
|
|
|
2085
2409
|
}
|
|
2086
2410
|
}
|
|
2087
2411
|
wireTimers() {
|
|
2088
|
-
const refresh = setInterval(() =>
|
|
2412
|
+
const refresh = setInterval(() => {
|
|
2413
|
+
if (this.api.health.canSend()) void this.refresh();
|
|
2414
|
+
}, this.options.configRefreshMs);
|
|
2089
2415
|
this.cleanups.push(() => clearInterval(refresh));
|
|
2090
2416
|
const poll = setInterval(() => {
|
|
2091
2417
|
if (!this.isReady || !this.config || this.backgroundedAt !== null) return;
|
|
@@ -2101,6 +2427,11 @@ var GuidelyClient = class {
|
|
|
2101
2427
|
if (this.destroyed) return;
|
|
2102
2428
|
const before = this.config?.configVersion;
|
|
2103
2429
|
await this.fetchConfig();
|
|
2430
|
+
this.applyConfigChange(before);
|
|
2431
|
+
}
|
|
2432
|
+
/** Re-renders after a config change (refresh, or a config that arrived after start). */
|
|
2433
|
+
applyConfigChange(before) {
|
|
2434
|
+
if (this.destroyed || !this.isReady) return;
|
|
2104
2435
|
if (this.config && this.config.configVersion !== before) {
|
|
2105
2436
|
this.notify();
|
|
2106
2437
|
this.evaluate("ready");
|
|
@@ -2114,6 +2445,7 @@ var GuidelyClient = class {
|
|
|
2114
2445
|
this.session.lastActiveAt = nowIso();
|
|
2115
2446
|
this.persistSession();
|
|
2116
2447
|
void this.queue?.flush();
|
|
2448
|
+
void this.outbox?.flush();
|
|
2117
2449
|
void this.store.flush();
|
|
2118
2450
|
void this.flags.flush();
|
|
2119
2451
|
this.messenger?.setForeground(false);
|
|
@@ -2127,7 +2459,7 @@ var GuidelyClient = class {
|
|
|
2127
2459
|
this.firedDelays.clear();
|
|
2128
2460
|
}
|
|
2129
2461
|
void this.flags.refresh();
|
|
2130
|
-
if (Date.now() - this.configFetchedAt > this.options.configRefreshMs) void this.refresh();
|
|
2462
|
+
if (Date.now() - this.configFetchedAt > this.options.configRefreshMs && this.api.health.canSend()) void this.refresh();
|
|
2131
2463
|
else if (this.isReady) this.evaluate("ready");
|
|
2132
2464
|
}
|
|
2133
2465
|
}
|
|
@@ -2153,6 +2485,7 @@ var GuidelyClient = class {
|
|
|
2153
2485
|
this.cleanups.splice(0).forEach((fn) => fn());
|
|
2154
2486
|
void this.queue?.flush();
|
|
2155
2487
|
this.queue?.destroy();
|
|
2488
|
+
this.outbox?.destroy();
|
|
2156
2489
|
this.flags.destroy();
|
|
2157
2490
|
this.emitter.clear();
|
|
2158
2491
|
this.uiListeners.clear();
|
|
@@ -2200,7 +2533,8 @@ var GuidelyClient = class {
|
|
|
2200
2533
|
checklists[id] = {
|
|
2201
2534
|
completedTaskIds: Array.from(/* @__PURE__ */ new Set([...local?.completedTaskIds ?? [], ...p.completedTaskIds ?? []])),
|
|
2202
2535
|
completedAt: local?.completedAt ?? p.completedAt ?? null,
|
|
2203
|
-
dismissedAt: local
|
|
2536
|
+
dismissedAt: dismissedAfterMerge(local, p),
|
|
2537
|
+
dismissResetAt: p.dismissResetAt ?? local?.dismissResetAt ?? null
|
|
2204
2538
|
};
|
|
2205
2539
|
}
|
|
2206
2540
|
this.state = {
|
|
@@ -2565,7 +2899,13 @@ var GuidelyClient = class {
|
|
|
2565
2899
|
if (reason === "completed") this.completeChecklistTasksBy("guide_completed", guide);
|
|
2566
2900
|
}
|
|
2567
2901
|
syncGuideState(guideId, update) {
|
|
2568
|
-
|
|
2902
|
+
const body = { anonymousId: this.anonymousId, userId: this.user?.id || null, updates: [{ guideId, ...update }] };
|
|
2903
|
+
if (update.impression) void postWithRetry(this.api, "/guide-state", body);
|
|
2904
|
+
else this.sendDurably(`guide:${guideId}:${update.completed ? "completed" : "dismissed"}:${this.anonymousId}`, "/guide-state", body);
|
|
2905
|
+
}
|
|
2906
|
+
/** Queues a write in the persisted outbox (sent now, or once the API is reachable again). */
|
|
2907
|
+
sendDurably(key, path, body) {
|
|
2908
|
+
this.whenHydrated(() => this.outbox?.add(key, path, body));
|
|
2569
2909
|
}
|
|
2570
2910
|
show(guide, manual, stepId) {
|
|
2571
2911
|
try {
|
|
@@ -2773,7 +3113,7 @@ var GuidelyClient = class {
|
|
|
2773
3113
|
progress.dismissedAt = nowIso();
|
|
2774
3114
|
this.persistState();
|
|
2775
3115
|
this.enqueue("checklist_dismissed", { guideId: guide.id, guideVersion: guide.version });
|
|
2776
|
-
|
|
3116
|
+
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 });
|
|
2777
3117
|
this.checklists.delete(guide.id);
|
|
2778
3118
|
this.notify();
|
|
2779
3119
|
}
|
|
@@ -2786,7 +3126,7 @@ var GuidelyClient = class {
|
|
|
2786
3126
|
this.persistState();
|
|
2787
3127
|
this.recordEnd(guide, "completed");
|
|
2788
3128
|
this.enqueue("survey_submitted", { guideId: guide.id, guideVersion: guide.version, properties: { responseId } });
|
|
2789
|
-
|
|
3129
|
+
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 });
|
|
2790
3130
|
this.emitter.emit("survey:submit", { guide, answers, responseId });
|
|
2791
3131
|
this.emitter.emit("guide:complete", guide);
|
|
2792
3132
|
this.surveyView = { ...this.surveyView, submitted: true };
|
|
@@ -2843,7 +3183,7 @@ var GuidelyClient = class {
|
|
|
2843
3183
|
this.enqueue("checklist_completed", { guideId: guide.id, guideVersion: guide.version });
|
|
2844
3184
|
}
|
|
2845
3185
|
this.persistState();
|
|
2846
|
-
|
|
3186
|
+
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 });
|
|
2847
3187
|
this.emitter.emit("checklist:task", { guide, task, progress: this.getChecklistProgress(guide.id) });
|
|
2848
3188
|
if (complete) {
|
|
2849
3189
|
this.emitter.emit("checklist:complete", guide);
|
|
@@ -2980,7 +3320,8 @@ var GuidelyClient = class {
|
|
|
2980
3320
|
},
|
|
2981
3321
|
onMessage: (message, conversation) => this.emitter.emit("conversation:message", { message, conversation }),
|
|
2982
3322
|
onStartGuide: (key) => this.setTimer(() => this.startGuide(key), 0),
|
|
2983
|
-
onError: (err) => this.log("messenger", err instanceof Error ? err.message : err)
|
|
3323
|
+
onError: (err) => this.log("messenger", err instanceof Error ? err.message : err),
|
|
3324
|
+
health: this.api.health
|
|
2984
3325
|
});
|
|
2985
3326
|
this.resourceCenterOpen = false;
|
|
2986
3327
|
}
|
|
@@ -3085,6 +3426,7 @@ var GuidelyClient = class {
|
|
|
3085
3426
|
audienceIds: this.state.audienceIds,
|
|
3086
3427
|
session: this.session,
|
|
3087
3428
|
pendingEvents: this.queue?.size ?? 0,
|
|
3429
|
+
pendingWrites: this.outbox?.size ?? 0,
|
|
3088
3430
|
targets: this.targets.ids(),
|
|
3089
3431
|
flagsReady: this.flags.isReady()
|
|
3090
3432
|
};
|