@knocklabs/client 0.21.1 → 0.21.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.
- package/CHANGELOG.md +20 -0
- package/dist/cjs/api.js +1 -1
- package/dist/cjs/api.js.map +1 -1
- package/dist/cjs/clients/feed/feed.js +1 -1
- package/dist/cjs/clients/feed/feed.js.map +1 -1
- package/dist/cjs/clients/guide/client.js +1 -1
- package/dist/cjs/clients/guide/client.js.map +1 -1
- package/dist/cjs/clients/guide/helpers.js +1 -1
- package/dist/cjs/clients/guide/helpers.js.map +1 -1
- package/dist/cjs/clients/guide/types.js +2 -0
- package/dist/cjs/clients/guide/types.js.map +1 -0
- package/dist/cjs/helpers.js +1 -1
- package/dist/cjs/helpers.js.map +1 -1
- package/dist/cjs/index.js +1 -1
- package/dist/cjs/knock.js +2 -2
- package/dist/cjs/knock.js.map +1 -1
- package/dist/cjs/pageVisibility.js +2 -0
- package/dist/cjs/pageVisibility.js.map +1 -0
- package/dist/esm/api.mjs +27 -12
- package/dist/esm/api.mjs.map +1 -1
- package/dist/esm/clients/feed/feed.mjs +60 -87
- package/dist/esm/clients/feed/feed.mjs.map +1 -1
- package/dist/esm/clients/guide/client.mjs +346 -268
- package/dist/esm/clients/guide/client.mjs.map +1 -1
- package/dist/esm/clients/guide/helpers.mjs +50 -57
- package/dist/esm/clients/guide/helpers.mjs.map +1 -1
- package/dist/esm/clients/guide/types.mjs +13 -0
- package/dist/esm/clients/guide/types.mjs.map +1 -0
- package/dist/esm/helpers.mjs +19 -4
- package/dist/esm/helpers.mjs.map +1 -1
- package/dist/esm/index.mjs +14 -12
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/knock.mjs +31 -29
- package/dist/esm/knock.mjs.map +1 -1
- package/dist/esm/pageVisibility.mjs +31 -0
- package/dist/esm/pageVisibility.mjs.map +1 -0
- package/dist/types/api.d.ts +4 -0
- package/dist/types/api.d.ts.map +1 -1
- package/dist/types/clients/feed/feed.d.ts +1 -11
- package/dist/types/clients/feed/feed.d.ts.map +1 -1
- package/dist/types/clients/feed/interfaces.d.ts +0 -4
- package/dist/types/clients/feed/interfaces.d.ts.map +1 -1
- package/dist/types/clients/feed/utils.d.ts +0 -2
- package/dist/types/clients/feed/utils.d.ts.map +1 -1
- package/dist/types/clients/guide/client.d.ts +4 -1
- package/dist/types/clients/guide/client.d.ts.map +1 -1
- package/dist/types/clients/guide/helpers.d.ts +1 -7
- package/dist/types/clients/guide/helpers.d.ts.map +1 -1
- package/dist/types/clients/guide/index.d.ts +2 -1
- package/dist/types/clients/guide/index.d.ts.map +1 -1
- package/dist/types/clients/guide/types.d.ts +24 -0
- package/dist/types/clients/guide/types.d.ts.map +1 -1
- package/dist/types/helpers.d.ts +19 -0
- package/dist/types/helpers.d.ts.map +1 -1
- package/dist/types/interfaces.d.ts +2 -0
- package/dist/types/interfaces.d.ts.map +1 -1
- package/dist/types/knock.d.ts +1 -0
- package/dist/types/knock.d.ts.map +1 -1
- package/dist/types/pageVisibility.d.ts +22 -0
- package/dist/types/pageVisibility.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/api.ts +30 -0
- package/src/clients/feed/feed.ts +0 -73
- package/src/clients/feed/interfaces.ts +0 -7
- package/src/clients/guide/client.ts +182 -35
- package/src/clients/guide/helpers.ts +4 -12
- package/src/clients/guide/index.ts +3 -0
- package/src/clients/guide/types.ts +37 -0
- package/src/helpers.ts +39 -0
- package/src/interfaces.ts +2 -0
- package/src/knock.ts +4 -3
- package/src/pageVisibility.ts +70 -0
package/dist/types/knock.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"knock.d.ts","sourceRoot":"","sources":["../../src/knock.ts"],"names":[],"mappings":"AAEA,OAAO,SAAS,MAAM,OAAO,CAAC;AAC9B,OAAO,UAAU,MAAM,gBAAgB,CAAC;AACxC,OAAO,aAAa,MAAM,oBAAoB,CAAC;AAC/C,OAAO,aAAa,MAAM,oBAAoB,CAAC;AAC/C,OAAO,YAAY,MAAM,mBAAmB,CAAC;AAC7C,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAChD,OAAO,WAAW,MAAM,iBAAiB,CAAC;AAC1C,OAAO,UAAU,MAAM,iBAAiB,CAAC;AACzC,OAAO,EACL,mBAAmB,EACnB,YAAY,EACZ,QAAQ,EACR,MAAM,EACN,0BAA0B,EAE3B,MAAM,cAAc,CAAC;AAItB,cAAM,KAAK;
|
|
1
|
+
{"version":3,"file":"knock.d.ts","sourceRoot":"","sources":["../../src/knock.ts"],"names":[],"mappings":"AAEA,OAAO,SAAS,MAAM,OAAO,CAAC;AAC9B,OAAO,UAAU,MAAM,gBAAgB,CAAC;AACxC,OAAO,aAAa,MAAM,oBAAoB,CAAC;AAC/C,OAAO,aAAa,MAAM,oBAAoB,CAAC;AAC/C,OAAO,YAAY,MAAM,mBAAmB,CAAC;AAC7C,OAAO,WAAW,MAAM,uBAAuB,CAAC;AAChD,OAAO,WAAW,MAAM,iBAAiB,CAAC;AAC1C,OAAO,UAAU,MAAM,iBAAiB,CAAC;AACzC,OAAO,EACL,mBAAmB,EACnB,YAAY,EACZ,QAAQ,EACR,MAAM,EACN,0BAA0B,EAE3B,MAAM,cAAc,CAAC;AAItB,cAAM,KAAK;IAkBP,QAAQ,CAAC,MAAM,EAAE,MAAM;IAjBlB,IAAI,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,SAAS,CAA0B;IACpC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IAC3B,SAAgB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChC,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAAU;IAClD,OAAO,CAAC,oBAAoB,CAA8C;IAC1E,QAAQ,CAAC,KAAK,aAAwB;IACtC,QAAQ,CAAC,OAAO,eAA0B;IAC1C,QAAQ,CAAC,WAAW,cAAyB;IAC7C,QAAQ,CAAC,KAAK,cAAyB;IACvC,QAAQ,CAAC,OAAO,gBAA2B;IAC3C,QAAQ,CAAC,IAAI,aAAwB;IACrC,QAAQ,CAAC,QAAQ,gBAA2B;gBAGjC,MAAM,EAAE,MAAM,EACvB,OAAO,GAAE,YAAiB;IAiB5B,MAAM;IASN;;;;;;;OAOG;IACH,YAAY,CACV,0BAA0B,EAAE,MAAM,EAClC,SAAS,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,EAC9B,OAAO,CAAC,EAAE,mBAAmB,GAC5B,KAAK;IACR,YAAY,CACV,0BAA0B,EAAE,0BAA0B,EACtD,SAAS,CAAC,EAAE,KAAK,CAAC,WAAW,CAAC,EAC9B,OAAO,CAAC,EAAE,mBAAmB,GAC5B,IAAI;IAwEP,sBAAsB;IAUtB,eAAe,CAAC,cAAc,UAAQ;IAKtC,QAAQ;IAOR,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,UAAQ;IAMlC;;OAEG;IACH,OAAO,CAAC,eAAe;YAUT,gCAAgC;IA+B9C;;;;;OAKG;IACH,OAAO,CAAC,SAAS;CAclB;AAED,eAAe,KAAK,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Socket } from 'phoenix';
|
|
2
|
+
/**
|
|
3
|
+
* Disconnects the socket after a delay when the page becomes hidden,
|
|
4
|
+
* and reconnects when it becomes visible again. This avoids holding
|
|
5
|
+
* open connections for background tabs that aren't being viewed.
|
|
6
|
+
*
|
|
7
|
+
* The delay prevents unnecessary disconnects during brief tab switches.
|
|
8
|
+
* Phoenix channels automatically rejoin after reconnecting.
|
|
9
|
+
*/
|
|
10
|
+
export declare class PageVisibilityManager {
|
|
11
|
+
private socket;
|
|
12
|
+
private disconnectDelayMs;
|
|
13
|
+
private disconnectTimer;
|
|
14
|
+
private wasConnected;
|
|
15
|
+
constructor(socket: Socket, disconnectDelayMs?: number);
|
|
16
|
+
private onVisibilityChange;
|
|
17
|
+
private scheduleDisconnect;
|
|
18
|
+
private reconnect;
|
|
19
|
+
private clearTimer;
|
|
20
|
+
teardown(): void;
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=pageVisibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pageVisibility.d.ts","sourceRoot":"","sources":["../../src/pageVisibility.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAItC;;;;;;;GAOG;AACH,qBAAa,qBAAqB;IAK9B,OAAO,CAAC,MAAM;IACd,OAAO,CAAC,iBAAiB;IAL3B,OAAO,CAAC,eAAe,CAA8C;IACrE,OAAO,CAAC,YAAY,CAAS;gBAGnB,MAAM,EAAE,MAAM,EACd,iBAAiB,GAAE,MAAoC;IAOjE,OAAO,CAAC,kBAAkB,CAMxB;IAEF,OAAO,CAAC,kBAAkB;IAa1B,OAAO,CAAC,SAAS;IASjB,OAAO,CAAC,UAAU;IAOlB,QAAQ;CAOT"}
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -2,11 +2,16 @@ import axios, { AxiosError, AxiosInstance, AxiosRequestConfig } from "axios";
|
|
|
2
2
|
import axiosRetry from "axios-retry";
|
|
3
3
|
import { Socket } from "phoenix";
|
|
4
4
|
|
|
5
|
+
import { exponentialBackoffFullJitter } from "./helpers";
|
|
6
|
+
import { PageVisibilityManager } from "./pageVisibility";
|
|
7
|
+
|
|
5
8
|
type ApiClientOptions = {
|
|
6
9
|
host: string;
|
|
7
10
|
apiKey: string;
|
|
8
11
|
userToken: string | undefined;
|
|
9
12
|
branch?: string;
|
|
13
|
+
/** Automatically disconnect the socket when the page is hidden and reconnect when visible. Defaults to `true`. */
|
|
14
|
+
disconnectOnPageHidden?: boolean;
|
|
10
15
|
};
|
|
11
16
|
|
|
12
17
|
export interface ApiResponse {
|
|
@@ -26,6 +31,7 @@ class ApiClient {
|
|
|
26
31
|
private axiosClient: AxiosInstance;
|
|
27
32
|
|
|
28
33
|
public socket: Socket | undefined;
|
|
34
|
+
private pageVisibility: PageVisibilityManager | undefined;
|
|
29
35
|
|
|
30
36
|
constructor(options: ApiClientOptions) {
|
|
31
37
|
this.host = options.host;
|
|
@@ -53,7 +59,23 @@ class ApiClient {
|
|
|
53
59
|
api_key: this.apiKey,
|
|
54
60
|
branch_slug: this.branch,
|
|
55
61
|
},
|
|
62
|
+
reconnectAfterMs: (tries: number) => {
|
|
63
|
+
return exponentialBackoffFullJitter(tries, {
|
|
64
|
+
baseDelayMs: 1000,
|
|
65
|
+
maxDelayMs: 30_000,
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
rejoinAfterMs: (tries: number) => {
|
|
69
|
+
return exponentialBackoffFullJitter(tries, {
|
|
70
|
+
baseDelayMs: 1000,
|
|
71
|
+
maxDelayMs: 60_000,
|
|
72
|
+
});
|
|
73
|
+
},
|
|
56
74
|
});
|
|
75
|
+
|
|
76
|
+
if (options.disconnectOnPageHidden !== false) {
|
|
77
|
+
this.pageVisibility = new PageVisibilityManager(this.socket);
|
|
78
|
+
}
|
|
57
79
|
}
|
|
58
80
|
|
|
59
81
|
axiosRetry(this.axiosClient, {
|
|
@@ -87,6 +109,14 @@ class ApiClient {
|
|
|
87
109
|
}
|
|
88
110
|
}
|
|
89
111
|
|
|
112
|
+
teardown() {
|
|
113
|
+
this.pageVisibility?.teardown();
|
|
114
|
+
|
|
115
|
+
if (this.socket?.isConnected()) {
|
|
116
|
+
this.socket.disconnect();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
90
120
|
private canRetryRequest(error: AxiosError) {
|
|
91
121
|
// Retry Network Errors.
|
|
92
122
|
if (axiosRetry.isNetworkError(error)) {
|
package/src/clients/feed/feed.ts
CHANGED
|
@@ -41,8 +41,6 @@ const feedClientDefaults: Pick<FeedClientOptions, "archived" | "mode"> = {
|
|
|
41
41
|
mode: "compact",
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
-
const DEFAULT_DISCONNECT_DELAY = 2000;
|
|
45
|
-
|
|
46
44
|
const CLIENT_REF_ID_PREFIX = "client_";
|
|
47
45
|
|
|
48
46
|
class Feed {
|
|
@@ -53,10 +51,7 @@ class Feed {
|
|
|
53
51
|
private userFeedId: string;
|
|
54
52
|
private broadcaster: EventEmitter;
|
|
55
53
|
private broadcastChannel!: BroadcastChannel | null;
|
|
56
|
-
private disconnectTimer: ReturnType<typeof setTimeout> | null = null;
|
|
57
54
|
private hasSubscribedToRealTimeUpdates: boolean = false;
|
|
58
|
-
private visibilityChangeHandler: () => void = () => {};
|
|
59
|
-
private visibilityChangeListenerConnected: boolean = false;
|
|
60
55
|
|
|
61
56
|
// The raw store instance, used for binding in React and other environments
|
|
62
57
|
public store: FeedStore;
|
|
@@ -117,13 +112,6 @@ class Feed {
|
|
|
117
112
|
|
|
118
113
|
this.socketManager?.leave(this);
|
|
119
114
|
|
|
120
|
-
this.tearDownVisibilityListeners();
|
|
121
|
-
|
|
122
|
-
if (this.disconnectTimer) {
|
|
123
|
-
clearTimeout(this.disconnectTimer);
|
|
124
|
-
this.disconnectTimer = null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
115
|
if (this.broadcastChannel) {
|
|
128
116
|
this.broadcastChannel.close();
|
|
129
117
|
}
|
|
@@ -553,8 +541,6 @@ class Feed {
|
|
|
553
541
|
__loadingType: undefined,
|
|
554
542
|
__fetchSource: undefined,
|
|
555
543
|
__experimentalCrossBrowserUpdates: undefined,
|
|
556
|
-
auto_manage_socket_connection: undefined,
|
|
557
|
-
auto_manage_socket_connection_delay: undefined,
|
|
558
544
|
};
|
|
559
545
|
|
|
560
546
|
const result = await this.knock.client().makeRequest({
|
|
@@ -815,10 +801,6 @@ class Feed {
|
|
|
815
801
|
// In server environments we might not have a socket connection
|
|
816
802
|
if (!this.socketManager) return;
|
|
817
803
|
|
|
818
|
-
if (this.defaultOptions.auto_manage_socket_connection) {
|
|
819
|
-
this.setUpVisibilityListeners();
|
|
820
|
-
}
|
|
821
|
-
|
|
822
804
|
// If we're initializing but they have previously opted to listen to real-time updates
|
|
823
805
|
// then we will automatically reconnect on their behalf
|
|
824
806
|
if (this.hasSubscribedToRealTimeUpdates && this.knock.isAuthenticated()) {
|
|
@@ -838,33 +820,6 @@ class Feed {
|
|
|
838
820
|
}
|
|
839
821
|
}
|
|
840
822
|
|
|
841
|
-
/**
|
|
842
|
-
* Listen for changes to document visibility and automatically disconnect
|
|
843
|
-
* or reconnect the socket after a delay
|
|
844
|
-
*/
|
|
845
|
-
private setUpVisibilityListeners() {
|
|
846
|
-
if (
|
|
847
|
-
typeof document === "undefined" ||
|
|
848
|
-
this.visibilityChangeListenerConnected
|
|
849
|
-
) {
|
|
850
|
-
return;
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
this.visibilityChangeHandler = this.handleVisibilityChange.bind(this);
|
|
854
|
-
this.visibilityChangeListenerConnected = true;
|
|
855
|
-
document.addEventListener("visibilitychange", this.visibilityChangeHandler);
|
|
856
|
-
}
|
|
857
|
-
|
|
858
|
-
private tearDownVisibilityListeners() {
|
|
859
|
-
if (typeof document === "undefined") return;
|
|
860
|
-
|
|
861
|
-
document.removeEventListener(
|
|
862
|
-
"visibilitychange",
|
|
863
|
-
this.visibilityChangeHandler,
|
|
864
|
-
);
|
|
865
|
-
this.visibilityChangeListenerConnected = false;
|
|
866
|
-
}
|
|
867
|
-
|
|
868
823
|
private emitEvent(
|
|
869
824
|
type:
|
|
870
825
|
| MessageEngagementStatus
|
|
@@ -882,34 +837,6 @@ class Feed {
|
|
|
882
837
|
// Internal events only need `items:`
|
|
883
838
|
this.broadcastOverChannel(`items:${type}`, { items });
|
|
884
839
|
}
|
|
885
|
-
|
|
886
|
-
private handleVisibilityChange() {
|
|
887
|
-
const disconnectDelay =
|
|
888
|
-
this.defaultOptions.auto_manage_socket_connection_delay ??
|
|
889
|
-
DEFAULT_DISCONNECT_DELAY;
|
|
890
|
-
|
|
891
|
-
const client = this.knock.client();
|
|
892
|
-
|
|
893
|
-
if (document.visibilityState === "hidden") {
|
|
894
|
-
// When the tab is hidden, clean up the socket connection after a delay
|
|
895
|
-
this.disconnectTimer = setTimeout(() => {
|
|
896
|
-
client.socket?.disconnect();
|
|
897
|
-
this.disconnectTimer = null;
|
|
898
|
-
}, disconnectDelay);
|
|
899
|
-
} else if (document.visibilityState === "visible") {
|
|
900
|
-
// When the tab is visible, clear the disconnect timer if active to cancel disconnecting
|
|
901
|
-
// This handles cases where the tab is only briefly hidden to avoid unnecessary disconnects
|
|
902
|
-
if (this.disconnectTimer) {
|
|
903
|
-
clearTimeout(this.disconnectTimer);
|
|
904
|
-
this.disconnectTimer = null;
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
// If the socket is not connected, try to reconnect
|
|
908
|
-
if (!client.socket?.isConnected()) {
|
|
909
|
-
client.socket?.connect();
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
840
|
}
|
|
914
841
|
|
|
915
842
|
export default Feed;
|
|
@@ -34,11 +34,6 @@ export interface FeedClientOptions {
|
|
|
34
34
|
trigger_data?: TriggerData;
|
|
35
35
|
// Optionally enable cross browser feed updates for this feed
|
|
36
36
|
__experimentalCrossBrowserUpdates?: boolean;
|
|
37
|
-
// Optionally automatically manage socket connections on changes to tab visibility (defaults to `false`)
|
|
38
|
-
auto_manage_socket_connection?: boolean;
|
|
39
|
-
// Optionally set the delay amount in milliseconds when automatically disconnecting sockets from inactive tabs (defaults to `2000`)
|
|
40
|
-
// Requires `auto_manage_socket_connection` to be `true`
|
|
41
|
-
auto_manage_socket_connection_delay?: number;
|
|
42
37
|
// Optionally scope notifications to a given date range
|
|
43
38
|
inserted_at_date_range?: {
|
|
44
39
|
// Optionally set the start date with a string in ISO 8601 format
|
|
@@ -88,8 +83,6 @@ export type FetchFeedOptionsForRequest = Omit<
|
|
|
88
83
|
__loadingType: undefined;
|
|
89
84
|
__fetchSource: undefined;
|
|
90
85
|
__experimentalCrossBrowserUpdates: undefined;
|
|
91
|
-
auto_manage_socket_connection: undefined;
|
|
92
|
-
auto_manage_socket_connection_delay: undefined;
|
|
93
86
|
};
|
|
94
87
|
|
|
95
88
|
export interface ContentBlockBase {
|
|
@@ -7,7 +7,6 @@ import Knock from "../../knock";
|
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
9
|
DEFAULT_GROUP_KEY,
|
|
10
|
-
SelectionResult,
|
|
11
10
|
byKey,
|
|
12
11
|
checkStateIfThrottled,
|
|
13
12
|
findDefaultGroup,
|
|
@@ -46,6 +45,8 @@ import {
|
|
|
46
45
|
SelectFilterParams,
|
|
47
46
|
SelectGuideOpts,
|
|
48
47
|
SelectGuidesOpts,
|
|
48
|
+
SelectQueryLimit,
|
|
49
|
+
SelectionResult,
|
|
49
50
|
StepMessageState,
|
|
50
51
|
StoreState,
|
|
51
52
|
TargetParams,
|
|
@@ -150,7 +151,16 @@ const safeJsonParseDebugParams = (value: string): DebugState => {
|
|
|
150
151
|
}
|
|
151
152
|
};
|
|
152
153
|
|
|
153
|
-
|
|
154
|
+
type SelectQueryMetadata = {
|
|
155
|
+
limit: SelectQueryLimit;
|
|
156
|
+
opts: SelectGuideOpts;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const select = (
|
|
160
|
+
state: StoreState,
|
|
161
|
+
filters: SelectFilterParams,
|
|
162
|
+
metadata: SelectQueryMetadata,
|
|
163
|
+
) => {
|
|
154
164
|
// A map of selected guides as values, with its order index as keys.
|
|
155
165
|
const result = new SelectionResult();
|
|
156
166
|
|
|
@@ -175,7 +185,8 @@ const select = (state: StoreState, filters: SelectFilterParams = {}) => {
|
|
|
175
185
|
result.set(index, guide);
|
|
176
186
|
}
|
|
177
187
|
|
|
178
|
-
result.metadata = { guideGroup: defaultGroup };
|
|
188
|
+
result.metadata = { guideGroup: defaultGroup, filters, ...metadata };
|
|
189
|
+
|
|
179
190
|
return result;
|
|
180
191
|
};
|
|
181
192
|
|
|
@@ -200,6 +211,12 @@ const predicate = (
|
|
|
200
211
|
// If in debug mode with a forced guide key, bypass other filtering and always
|
|
201
212
|
// return true for that guide only. This should always run AFTER checking the
|
|
202
213
|
// filters but BEFORE checking archived status and location rules.
|
|
214
|
+
if (
|
|
215
|
+
debug.focusedGuideKeys &&
|
|
216
|
+
Object.keys(debug.focusedGuideKeys).length > 0
|
|
217
|
+
) {
|
|
218
|
+
return !!debug.focusedGuideKeys[guide.key];
|
|
219
|
+
}
|
|
203
220
|
if (debug.forcedGuideKey) {
|
|
204
221
|
return debug.forcedGuideKey === guide.key;
|
|
205
222
|
}
|
|
@@ -552,7 +569,11 @@ export class KnockGuideClient {
|
|
|
552
569
|
// Clear debug state from store
|
|
553
570
|
this.store.setState((state) => ({
|
|
554
571
|
...state,
|
|
555
|
-
debug: {
|
|
572
|
+
debug: {
|
|
573
|
+
forcedGuideKey: null,
|
|
574
|
+
previewSessionId: null,
|
|
575
|
+
focusedGuideKeys: {},
|
|
576
|
+
},
|
|
556
577
|
previewGuides: {}, // Clear preview guides when exiting debug mode
|
|
557
578
|
}));
|
|
558
579
|
|
|
@@ -577,7 +598,13 @@ export class KnockGuideClient {
|
|
|
577
598
|
|
|
578
599
|
this.store.setState((state) => ({
|
|
579
600
|
...state,
|
|
580
|
-
debug: {
|
|
601
|
+
debug: {
|
|
602
|
+
skipEngagementTracking: true,
|
|
603
|
+
ignoreDisplayInterval: true,
|
|
604
|
+
focusedGuideKeys: {},
|
|
605
|
+
...debugOpts,
|
|
606
|
+
debugging: true,
|
|
607
|
+
},
|
|
581
608
|
}));
|
|
582
609
|
|
|
583
610
|
if (shouldRefetch) {
|
|
@@ -617,14 +644,35 @@ export class KnockGuideClient {
|
|
|
617
644
|
`[Guide] .selectGuides (filters: ${formatFilters(filters)}; state: ${formatState(state)})`,
|
|
618
645
|
);
|
|
619
646
|
|
|
620
|
-
|
|
621
|
-
|
|
647
|
+
// 1. First, call selectGuide() using the same filters to ensure we have a
|
|
648
|
+
// group stage open and respect throttling. This isn't the real query, but
|
|
649
|
+
// rather it's a shortcut ahead of handling the actual query result below.
|
|
650
|
+
const selectedGuide = this.selectGuide(state, filters, {
|
|
651
|
+
...opts,
|
|
652
|
+
// Don't record this result, not the actual query result we need.
|
|
653
|
+
recordSelectQuery: false,
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
// 2. Now make the actual select query with the provided filters and opts,
|
|
657
|
+
// and record the result (as needed). By default, we only record the result
|
|
658
|
+
// while in debugging.
|
|
659
|
+
const { recordSelectQuery = !!state.debug?.debugging } = opts;
|
|
660
|
+
const metadata: SelectQueryMetadata = {
|
|
661
|
+
limit: "all",
|
|
662
|
+
opts: { ...opts, recordSelectQuery },
|
|
663
|
+
};
|
|
664
|
+
const result = select(state, filters, metadata);
|
|
665
|
+
this.maybeRecordSelectResult(result);
|
|
666
|
+
|
|
667
|
+
// 3. Stop if there is not at least one guide to return.
|
|
668
|
+
if (!selectedGuide && !opts.includeThrottled) {
|
|
622
669
|
return [];
|
|
623
670
|
}
|
|
624
671
|
|
|
625
672
|
// There should be at least one guide to return here now.
|
|
626
|
-
const guides = [...
|
|
673
|
+
const guides = [...result.values()];
|
|
627
674
|
|
|
675
|
+
// 4. If throttled, filter out any throttled guides.
|
|
628
676
|
if (!opts.includeThrottled && checkStateIfThrottled(state)) {
|
|
629
677
|
const unthrottledGuides = guides.filter(
|
|
630
678
|
(g) => g.bypass_global_group_limit,
|
|
@@ -657,32 +705,6 @@ export class KnockGuideClient {
|
|
|
657
705
|
return undefined;
|
|
658
706
|
}
|
|
659
707
|
|
|
660
|
-
const result = select(state, filters);
|
|
661
|
-
|
|
662
|
-
if (result.size === 0) {
|
|
663
|
-
this.knock.log("[Guide] Selection found zero result");
|
|
664
|
-
return undefined;
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
const [index, guide] = [...result][0]!;
|
|
668
|
-
this.knock.log(
|
|
669
|
-
`[Guide] Selection found: \`${guide.key}\` (total: ${result.size})`,
|
|
670
|
-
);
|
|
671
|
-
|
|
672
|
-
// If a guide ignores the group limit, then return immediately to render
|
|
673
|
-
// always.
|
|
674
|
-
if (guide.bypass_global_group_limit) {
|
|
675
|
-
this.knock.log(`[Guide] Returning the unthrottled guide: ${guide.key}`);
|
|
676
|
-
return guide;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
// Check if inside the throttle window (i.e. throttled) and if so stop and
|
|
680
|
-
// return undefined unless explicitly given the option to include throttled.
|
|
681
|
-
if (!opts.includeThrottled && checkStateIfThrottled(state)) {
|
|
682
|
-
this.knock.log(`[Guide] Throttling the selected guide: ${guide.key}`);
|
|
683
|
-
return undefined;
|
|
684
|
-
}
|
|
685
|
-
|
|
686
708
|
// Starting here to the end of this method represents the core logic of how
|
|
687
709
|
// "group stage" works. It provides a mechanism for 1) figuring out which
|
|
688
710
|
// guide components are about to render on a page, 2) determining which
|
|
@@ -716,6 +738,39 @@ export class KnockGuideClient {
|
|
|
716
738
|
this.stage = this.openGroupStage(); // Assign here to make tsc happy
|
|
717
739
|
}
|
|
718
740
|
|
|
741
|
+
// Must come AFTER we ensure a group stage exists above, so we can record
|
|
742
|
+
// select queries. By default, we only record the result while in debugging.
|
|
743
|
+
const { recordSelectQuery = !!state.debug?.debugging } = opts;
|
|
744
|
+
const metadata: SelectQueryMetadata = {
|
|
745
|
+
limit: "one",
|
|
746
|
+
opts: { ...opts, recordSelectQuery },
|
|
747
|
+
};
|
|
748
|
+
const result = select(state, filters, metadata);
|
|
749
|
+
this.maybeRecordSelectResult(result);
|
|
750
|
+
|
|
751
|
+
if (result.size === 0) {
|
|
752
|
+
this.knock.log("[Guide] Selection found zero result");
|
|
753
|
+
return undefined;
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const [index, guide] = [...result][0]!;
|
|
757
|
+
this.knock.log(
|
|
758
|
+
`[Guide] Selection found: \`${guide.key}\` (total: ${result.size})`,
|
|
759
|
+
);
|
|
760
|
+
|
|
761
|
+
// If a guide ignores the group limit, then return immediately to render
|
|
762
|
+
// always.
|
|
763
|
+
if (guide.bypass_global_group_limit) {
|
|
764
|
+
this.knock.log(`[Guide] Returning the unthrottled guide: ${guide.key}`);
|
|
765
|
+
return guide;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// If focused while in debug mode, then we want to ignore the guide order
|
|
769
|
+
// and throttle settings and force render this guide.
|
|
770
|
+
const focusedInDebug = state.debug?.focusedGuideKeys?.[guide.key];
|
|
771
|
+
|
|
772
|
+
const throttled = !opts.includeThrottled && checkStateIfThrottled(state);
|
|
773
|
+
|
|
719
774
|
switch (this.stage.status) {
|
|
720
775
|
case "open": {
|
|
721
776
|
this.knock.log(`[Guide] Adding to the group stage: ${guide.key}`);
|
|
@@ -725,8 +780,23 @@ export class KnockGuideClient {
|
|
|
725
780
|
|
|
726
781
|
case "patch": {
|
|
727
782
|
this.knock.log(`[Guide] Patching the group stage: ${guide.key}`);
|
|
783
|
+
// Refresh the ordered queue in the group stage while continuing to
|
|
784
|
+
// render the currently resolved guide while in patch window, so that
|
|
785
|
+
// we can re-resolve when the group stage closes.
|
|
728
786
|
this.stage.ordered[index] = guide.key;
|
|
729
787
|
|
|
788
|
+
if (focusedInDebug) {
|
|
789
|
+
this.knock.log(
|
|
790
|
+
`[Guide] Focused to return \`${guide.key}\` (stage: ${formatGroupStage(this.stage)})`,
|
|
791
|
+
);
|
|
792
|
+
return guide;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (throttled) {
|
|
796
|
+
this.knock.log(`[Guide] Throttling the selected guide: ${guide.key}`);
|
|
797
|
+
return undefined;
|
|
798
|
+
}
|
|
799
|
+
|
|
730
800
|
const ret = this.stage.resolved === guide.key ? guide : undefined;
|
|
731
801
|
this.knock.log(
|
|
732
802
|
`[Guide] Returning \`${ret?.key}\` (stage: ${formatGroupStage(this.stage)})`,
|
|
@@ -735,6 +805,18 @@ export class KnockGuideClient {
|
|
|
735
805
|
}
|
|
736
806
|
|
|
737
807
|
case "closed": {
|
|
808
|
+
if (focusedInDebug) {
|
|
809
|
+
this.knock.log(
|
|
810
|
+
`[Guide] Focused to return \`${guide.key}\` (stage: ${formatGroupStage(this.stage)})`,
|
|
811
|
+
);
|
|
812
|
+
return guide;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
if (throttled) {
|
|
816
|
+
this.knock.log(`[Guide] Throttling the selected guide: ${guide.key}`);
|
|
817
|
+
return undefined;
|
|
818
|
+
}
|
|
819
|
+
|
|
738
820
|
const ret = this.stage.resolved === guide.key ? guide : undefined;
|
|
739
821
|
this.knock.log(
|
|
740
822
|
`[Guide] Returning \`${ret?.key}\` (stage: ${formatGroupStage(this.stage)})`,
|
|
@@ -744,6 +826,42 @@ export class KnockGuideClient {
|
|
|
744
826
|
}
|
|
745
827
|
}
|
|
746
828
|
|
|
829
|
+
// Record select query results by accumulating them by 1) key or type first,
|
|
830
|
+
// and then 2) "one" or "all".
|
|
831
|
+
private maybeRecordSelectResult(result: SelectionResult) {
|
|
832
|
+
if (!result.metadata) return;
|
|
833
|
+
|
|
834
|
+
const { opts, filters, limit } = result.metadata;
|
|
835
|
+
if (!opts.recordSelectQuery) return;
|
|
836
|
+
if (!filters.key && !filters.type) return;
|
|
837
|
+
if (!this.stage || this.stage.status === "closed") return;
|
|
838
|
+
|
|
839
|
+
// Deep merge to accumulate the results.
|
|
840
|
+
const queriedByKey = this.stage.results.key || {};
|
|
841
|
+
if (filters.key) {
|
|
842
|
+
queriedByKey[filters.key] = {
|
|
843
|
+
...(queriedByKey[filters.key] || {}),
|
|
844
|
+
...{ [limit]: result },
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
const queriedByType = this.stage.results.type || {};
|
|
848
|
+
if (filters.type) {
|
|
849
|
+
queriedByType[filters.type] = {
|
|
850
|
+
...(queriedByType[filters.type] || {}),
|
|
851
|
+
...{ [limit]: result },
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
this.stage = {
|
|
856
|
+
...this.stage,
|
|
857
|
+
results: { key: queriedByKey, type: queriedByType },
|
|
858
|
+
};
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
getStage() {
|
|
862
|
+
return this.stage;
|
|
863
|
+
}
|
|
864
|
+
|
|
747
865
|
private openGroupStage() {
|
|
748
866
|
this.knock.log("[Guide] Opening a new group stage");
|
|
749
867
|
|
|
@@ -759,6 +877,7 @@ export class KnockGuideClient {
|
|
|
759
877
|
this.stage = {
|
|
760
878
|
status: "open",
|
|
761
879
|
ordered: [],
|
|
880
|
+
results: {},
|
|
762
881
|
timeoutId,
|
|
763
882
|
};
|
|
764
883
|
|
|
@@ -885,6 +1004,13 @@ export class KnockGuideClient {
|
|
|
885
1004
|
});
|
|
886
1005
|
if (!updatedStep) return;
|
|
887
1006
|
|
|
1007
|
+
if (this.shouldSkipEngagementApi()) {
|
|
1008
|
+
this.knock.log(
|
|
1009
|
+
"[Guide] Skipping engagement API call for markAsSeen (debug mode)",
|
|
1010
|
+
);
|
|
1011
|
+
return updatedStep;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
888
1014
|
const params = {
|
|
889
1015
|
...this.buildEngagementEventBaseParams(guide, updatedStep),
|
|
890
1016
|
content: updatedStep.content,
|
|
@@ -915,6 +1041,13 @@ export class KnockGuideClient {
|
|
|
915
1041
|
});
|
|
916
1042
|
if (!updatedStep) return;
|
|
917
1043
|
|
|
1044
|
+
if (this.shouldSkipEngagementApi()) {
|
|
1045
|
+
this.knock.log(
|
|
1046
|
+
"[Guide] Skipping engagement API call for markAsInteracted (debug mode)",
|
|
1047
|
+
);
|
|
1048
|
+
return updatedStep;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
918
1051
|
const params = {
|
|
919
1052
|
...this.buildEngagementEventBaseParams(guide, updatedStep),
|
|
920
1053
|
metadata,
|
|
@@ -940,6 +1073,13 @@ export class KnockGuideClient {
|
|
|
940
1073
|
});
|
|
941
1074
|
if (!updatedStep) return;
|
|
942
1075
|
|
|
1076
|
+
if (this.shouldSkipEngagementApi()) {
|
|
1077
|
+
this.knock.log(
|
|
1078
|
+
"[Guide] Skipping engagement API call for markAsArchived (debug mode)",
|
|
1079
|
+
);
|
|
1080
|
+
return updatedStep;
|
|
1081
|
+
}
|
|
1082
|
+
|
|
943
1083
|
const params = this.buildEngagementEventBaseParams(guide, updatedStep);
|
|
944
1084
|
|
|
945
1085
|
this.knock.user.markGuideStepAs<MarkAsArchivedParams, MarkGuideAsResponse>(
|
|
@@ -953,6 +1093,10 @@ export class KnockGuideClient {
|
|
|
953
1093
|
return updatedStep;
|
|
954
1094
|
}
|
|
955
1095
|
|
|
1096
|
+
private shouldSkipEngagementApi(): boolean {
|
|
1097
|
+
return !!this.store.state.debug?.skipEngagementTracking;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
956
1100
|
//
|
|
957
1101
|
// Helpers
|
|
958
1102
|
//
|
|
@@ -967,7 +1111,10 @@ export class KnockGuideClient {
|
|
|
967
1111
|
// Get the next unarchived step.
|
|
968
1112
|
getStep() {
|
|
969
1113
|
// If debugging this guide, return the first step regardless of archive status
|
|
970
|
-
if (
|
|
1114
|
+
if (
|
|
1115
|
+
self.store.state.debug?.forcedGuideKey === this.key ||
|
|
1116
|
+
self.store.state.debug?.focusedGuideKeys?.[this.key]
|
|
1117
|
+
) {
|
|
971
1118
|
return this.steps[0];
|
|
972
1119
|
}
|
|
973
1120
|
|
|
@@ -3,23 +3,11 @@ import {
|
|
|
3
3
|
GuideActivationUrlRuleData,
|
|
4
4
|
GuideData,
|
|
5
5
|
GuideGroupData,
|
|
6
|
-
KnockGuide,
|
|
7
6
|
KnockGuideActivationUrlPattern,
|
|
8
7
|
SelectFilterParams,
|
|
9
8
|
StoreState,
|
|
10
9
|
} from "./types";
|
|
11
10
|
|
|
12
|
-
// Extends the map class to allow having metadata on it, which is used to record
|
|
13
|
-
// the guide group context for the selection result (though currently only a
|
|
14
|
-
// default global group is supported).
|
|
15
|
-
export class SelectionResult<K = number, V = KnockGuide> extends Map<K, V> {
|
|
16
|
-
metadata: { guideGroup: GuideGroupData } | undefined;
|
|
17
|
-
|
|
18
|
-
constructor() {
|
|
19
|
-
super();
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
|
|
23
11
|
export const formatGroupStage = (stage: GroupStage) => {
|
|
24
12
|
return `status=${stage.status}, resolved=${stage.resolved}`;
|
|
25
13
|
};
|
|
@@ -76,6 +64,10 @@ export const findDefaultGroup = (guideGroups: GuideGroupData[]) =>
|
|
|
76
64
|
);
|
|
77
65
|
|
|
78
66
|
export const checkStateIfThrottled = (state: StoreState) => {
|
|
67
|
+
if (state.debug?.ignoreDisplayInterval) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
79
71
|
const defaultGroup = findDefaultGroup(state.guideGroups);
|
|
80
72
|
const throttleWindowStartedAt =
|
|
81
73
|
state.guideGroupDisplayLogs[DEFAULT_GROUP_KEY];
|
|
@@ -3,6 +3,7 @@ export {
|
|
|
3
3
|
DEBUG_QUERY_PARAMS,
|
|
4
4
|
checkActivatable,
|
|
5
5
|
} from "./client";
|
|
6
|
+
export { checkStateIfThrottled } from "./helpers";
|
|
6
7
|
export type {
|
|
7
8
|
KnockGuide,
|
|
8
9
|
KnockGuideStep,
|
|
@@ -12,4 +13,6 @@ export type {
|
|
|
12
13
|
SelectGuideOpts as KnockSelectGuideOpts,
|
|
13
14
|
SelectGuidesOpts as KnockSelectGuidesOpts,
|
|
14
15
|
StoreState as KnockGuideClientStoreState,
|
|
16
|
+
GroupStage as KnockGuideClientGroupStage,
|
|
17
|
+
SelectionResult as KnockGuideSelectionResult,
|
|
15
18
|
} from "./types";
|