@atproto/bsky 0.0.215 → 0.0.217
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 +13 -0
- package/dist/api/app/bsky/feed/searchPosts.d.ts.map +1 -1
- package/dist/api/app/bsky/feed/searchPosts.js +6 -4
- package/dist/api/app/bsky/feed/searchPosts.js.map +1 -1
- package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js +2 -0
- package/dist/api/app/bsky/graph/getSuggestedFollowsByActor.js.map +1 -1
- package/dist/api/app/bsky/unspecced/getPostThreadV2.js +1 -1
- package/dist/api/app/bsky/unspecced/getPostThreadV2.js.map +1 -1
- package/dist/api/app/bsky/unspecced/getSuggestedOnboardingUsers.d.ts.map +1 -1
- package/dist/api/app/bsky/unspecced/getSuggestedOnboardingUsers.js +10 -3
- package/dist/api/app/bsky/unspecced/getSuggestedOnboardingUsers.js.map +1 -1
- package/dist/api/app/bsky/unspecced/getSuggestedUsers.d.ts.map +1 -1
- package/dist/api/app/bsky/unspecced/getSuggestedUsers.js +9 -2
- package/dist/api/app/bsky/unspecced/getSuggestedUsers.js.map +1 -1
- package/dist/config.d.ts +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +5 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts +3 -3
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +2 -2
- package/dist/context.js.map +1 -1
- package/dist/feature-gates/gates.d.ts +5 -0
- package/dist/feature-gates/gates.d.ts.map +1 -0
- package/dist/feature-gates/gates.js +6 -0
- package/dist/feature-gates/gates.js.map +1 -0
- package/dist/feature-gates/index.d.ts +24 -0
- package/dist/feature-gates/index.d.ts.map +1 -0
- package/dist/feature-gates/index.js +135 -0
- package/dist/feature-gates/index.js.map +1 -0
- package/dist/feature-gates/metrics.d.ts +32 -0
- package/dist/feature-gates/metrics.d.ts.map +1 -0
- package/dist/feature-gates/metrics.js +100 -0
- package/dist/feature-gates/metrics.js.map +1 -0
- package/dist/feature-gates/metrics.test.d.ts +2 -0
- package/dist/feature-gates/metrics.test.d.ts.map +1 -0
- package/dist/feature-gates/metrics.test.js +152 -0
- package/dist/feature-gates/metrics.test.js.map +1 -0
- package/dist/feature-gates/types.d.ts +49 -0
- package/dist/feature-gates/types.d.ts.map +1 -0
- package/dist/feature-gates/types.js +3 -0
- package/dist/feature-gates/types.js.map +1 -0
- package/dist/feature-gates/utils.d.ts +21 -0
- package/dist/feature-gates/utils.d.ts.map +1 -0
- package/dist/feature-gates/utils.js +85 -0
- package/dist/feature-gates/utils.js.map +1 -0
- package/dist/hydration/hydrator.d.ts +8 -3
- package/dist/hydration/hydrator.d.ts.map +1 -1
- package/dist/hydration/hydrator.js +9 -5
- package/dist/hydration/hydrator.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/lexicon/index.d.ts +2 -2
- package/dist/lexicon/index.d.ts.map +1 -1
- package/dist/lexicon/index.js +4 -4
- package/dist/lexicon/index.js.map +1 -1
- package/dist/lexicon/lexicons.d.ts +116 -100
- package/dist/lexicon/lexicons.d.ts.map +1 -1
- package/dist/lexicon/lexicons.js +59 -51
- package/dist/lexicon/lexicons.js.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.d.ts +3 -1
- package/dist/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.js.map +1 -1
- package/dist/lexicon/types/app/bsky/unspecced/{getSuggestedOnboardingUsersSkeleton.d.ts → getOnboardingSuggestedUsersSkeleton.d.ts} +1 -1
- package/dist/lexicon/types/app/bsky/unspecced/{getSuggestedOnboardingUsersSkeleton.d.ts.map → getOnboardingSuggestedUsersSkeleton.d.ts.map} +1 -1
- package/dist/lexicon/types/app/bsky/unspecced/{getSuggestedOnboardingUsersSkeleton.js → getOnboardingSuggestedUsersSkeleton.js} +2 -2
- package/dist/lexicon/types/app/bsky/unspecced/{getSuggestedOnboardingUsersSkeleton.js.map → getOnboardingSuggestedUsersSkeleton.js.map} +1 -1
- package/dist/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.d.ts +3 -1
- package/dist/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.d.ts.map +1 -1
- package/dist/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.js.map +1 -1
- package/dist/views/index.d.ts.map +1 -1
- package/dist/views/index.js +3 -4
- package/dist/views/index.js.map +1 -1
- package/package.json +9 -9
- package/src/api/app/bsky/feed/searchPosts.ts +10 -8
- package/src/api/app/bsky/graph/getSuggestedFollowsByActor.ts +3 -1
- package/src/api/app/bsky/unspecced/getPostThreadV2.ts +3 -3
- package/src/api/app/bsky/unspecced/getSuggestedOnboardingUsers.ts +14 -7
- package/src/api/app/bsky/unspecced/getSuggestedUsers.ts +13 -6
- package/src/config.ts +8 -0
- package/src/context.ts +4 -4
- package/src/feature-gates/README.md +47 -0
- package/src/feature-gates/gates.ts +9 -0
- package/src/feature-gates/index.ts +146 -0
- package/src/feature-gates/metrics.test.ts +196 -0
- package/src/feature-gates/metrics.ts +107 -0
- package/src/feature-gates/types.ts +52 -0
- package/src/feature-gates/utils.ts +90 -0
- package/src/hydration/hydrator.ts +12 -6
- package/src/index.ts +8 -7
- package/src/lexicon/index.ts +13 -13
- package/src/lexicon/lexicons.ts +63 -55
- package/src/lexicon/types/app/bsky/graph/getSuggestedFollowsByActor.ts +3 -1
- package/src/lexicon/types/app/bsky/unspecced/{getSuggestedOnboardingUsersSkeleton.ts → getOnboardingSuggestedUsersSkeleton.ts} +1 -1
- package/src/lexicon/types/app/bsky/unspecced/getSuggestionsSkeleton.ts +3 -1
- package/src/views/index.ts +5 -8
- package/tests/views/get-suggested-onboarding-users.test.ts +1 -1
- package/tests/views/thread.test.ts +2 -0
- package/tsconfig.build.tsbuildinfo +1 -1
- package/dist/feature-gates.d.ts +0 -44
- package/dist/feature-gates.d.ts.map +0 -1
- package/dist/feature-gates.js +0 -133
- package/dist/feature-gates.js.map +0 -1
- package/src/feature-gates.ts +0 -136
package/dist/feature-gates.js
DELETED
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FeatureGates = exports.FeatureGateID = void 0;
|
|
4
|
-
const growthbook_1 = require("@growthbook/growthbook");
|
|
5
|
-
const logger_1 = require("./logger");
|
|
6
|
-
/**
|
|
7
|
-
* We want this to be sufficiently high that we don't time out under
|
|
8
|
-
* normal conditions, but not so high that it takes too long to boot
|
|
9
|
-
* the server.
|
|
10
|
-
*/
|
|
11
|
-
const FETCH_TIMEOUT = 3e3; // 3 seconds
|
|
12
|
-
/**
|
|
13
|
-
* StatSig used to default to every 10s, but I think 1m is fine
|
|
14
|
-
*/
|
|
15
|
-
const REFETCH_INTERVAL = 60e3; // 1 minute
|
|
16
|
-
var FeatureGateID;
|
|
17
|
-
(function (FeatureGateID) {
|
|
18
|
-
/**
|
|
19
|
-
* Left here ensure this is interpreted as a string enum and therefore
|
|
20
|
-
* appease TS
|
|
21
|
-
*/
|
|
22
|
-
FeatureGateID["_"] = "";
|
|
23
|
-
FeatureGateID["SuggestedUsersDiscoverAgentEnable"] = "suggested_users:discover_agent:enable";
|
|
24
|
-
FeatureGateID["SuggestedOnboardingUsersDiscoverAgentEnable"] = "suggested_onboarding_users:discover_agent:enable";
|
|
25
|
-
FeatureGateID["ThreadsReplyRankingExplorationEnable"] = "threads:reply_ranking_exploration:enable";
|
|
26
|
-
FeatureGateID["SearchFilteringExplorationEnable"] = "search:filtering_exploration:enable";
|
|
27
|
-
})(FeatureGateID || (exports.FeatureGateID = FeatureGateID = {}));
|
|
28
|
-
class FeatureGates {
|
|
29
|
-
constructor(config) {
|
|
30
|
-
Object.defineProperty(this, "config", {
|
|
31
|
-
enumerable: true,
|
|
32
|
-
configurable: true,
|
|
33
|
-
writable: true,
|
|
34
|
-
value: config
|
|
35
|
-
});
|
|
36
|
-
Object.defineProperty(this, "ready", {
|
|
37
|
-
enumerable: true,
|
|
38
|
-
configurable: true,
|
|
39
|
-
writable: true,
|
|
40
|
-
value: false
|
|
41
|
-
});
|
|
42
|
-
Object.defineProperty(this, "client", {
|
|
43
|
-
enumerable: true,
|
|
44
|
-
configurable: true,
|
|
45
|
-
writable: true,
|
|
46
|
-
value: undefined
|
|
47
|
-
});
|
|
48
|
-
Object.defineProperty(this, "ids", {
|
|
49
|
-
enumerable: true,
|
|
50
|
-
configurable: true,
|
|
51
|
-
writable: true,
|
|
52
|
-
value: FeatureGateID
|
|
53
|
-
});
|
|
54
|
-
Object.defineProperty(this, "refreshInterval", {
|
|
55
|
-
enumerable: true,
|
|
56
|
-
configurable: true,
|
|
57
|
-
writable: true,
|
|
58
|
-
value: undefined
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
async start() {
|
|
62
|
-
try {
|
|
63
|
-
if (this.config.apiHost && this.config.clientKey) {
|
|
64
|
-
this.client = new growthbook_1.GrowthBookClient({
|
|
65
|
-
apiHost: this.config.apiHost,
|
|
66
|
-
clientKey: this.config.clientKey,
|
|
67
|
-
});
|
|
68
|
-
const { source, error } = await this.client.init({
|
|
69
|
-
timeout: FETCH_TIMEOUT,
|
|
70
|
-
});
|
|
71
|
-
/**
|
|
72
|
-
* This does not necessarily mean that the client completely failed,
|
|
73
|
-
* since it could just be that the request timed out. It may succeed
|
|
74
|
-
* after the timeout, or later during refreshes.
|
|
75
|
-
*
|
|
76
|
-
* @see https://docs.growthbook.io/lib/node#error-handling
|
|
77
|
-
*/
|
|
78
|
-
if (error) {
|
|
79
|
-
logger_1.featureGatesLogger.error({ err: error, source }, 'Client failed to initialize normally');
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Set up periodic refresh of feature definitions
|
|
83
|
-
*
|
|
84
|
-
* @see https://docs.growthbook.io/lib/node#refreshing-features
|
|
85
|
-
*/
|
|
86
|
-
this.refreshInterval = setInterval(async () => {
|
|
87
|
-
try {
|
|
88
|
-
await this.client?.refreshFeatures({
|
|
89
|
-
timeout: FETCH_TIMEOUT,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
catch (err) {
|
|
93
|
-
logger_1.featureGatesLogger.error({ err }, 'Failed to refresh features');
|
|
94
|
-
}
|
|
95
|
-
}, REFETCH_INTERVAL);
|
|
96
|
-
/* Ready or not, here we come */
|
|
97
|
-
this.ready = true;
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
logger_1.featureGatesLogger.error('Missing required config for FeatureGates client');
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
catch (err) {
|
|
104
|
-
logger_1.featureGatesLogger.error({ err }, 'Client initialization failed');
|
|
105
|
-
this.ready = false;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
destroy() {
|
|
109
|
-
if (this.ready) {
|
|
110
|
-
this.ready = false;
|
|
111
|
-
if (this.refreshInterval) {
|
|
112
|
-
clearInterval(this.refreshInterval);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
userContext({ did, }) {
|
|
117
|
-
return { attributes: { did: did ?? null } };
|
|
118
|
-
}
|
|
119
|
-
check(gate, ctx) {
|
|
120
|
-
if (!this.ready || !this.client)
|
|
121
|
-
return false;
|
|
122
|
-
return this.client.isOn(gate, ctx);
|
|
123
|
-
}
|
|
124
|
-
/**
|
|
125
|
-
* Pre-evaluate multiple feature gates for a given user, returning a map of
|
|
126
|
-
* gate ID to boolean result.
|
|
127
|
-
*/
|
|
128
|
-
checkGates(gates, ctx) {
|
|
129
|
-
return new Map(gates.map((g) => [g, this.check(g, ctx)]));
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
exports.FeatureGates = FeatureGates;
|
|
133
|
-
//# sourceMappingURL=feature-gates.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"feature-gates.js","sourceRoot":"","sources":["../src/feature-gates.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n GrowthBookClient,\n type UserContext as GrowthBookUserContext,\n} from '@growthbook/growthbook'\nimport { featureGatesLogger } from './logger'\n\n/**\n * We want this to be sufficiently high that we don't time out under\n * normal conditions, but not so high that it takes too long to boot\n * the server.\n */\nconst FETCH_TIMEOUT = 3e3 // 3 seconds\n\n/**\n * StatSig used to default to every 10s, but I think 1m is fine\n */\nconst REFETCH_INTERVAL = 60e3 // 1 minute\n\nexport type Config = {\n apiHost?: string\n clientKey?: string\n}\n\ntype UserContext = Omit<GrowthBookUserContext, 'attributes'> & {\n attributes?: {\n did?: string | null\n }\n}\n\nexport enum FeatureGateID {\n /**\n * Left here ensure this is interpreted as a string enum and therefore\n * appease TS\n */\n _ = '',\n SuggestedUsersDiscoverAgentEnable = 'suggested_users:discover_agent:enable',\n SuggestedOnboardingUsersDiscoverAgentEnable = 'suggested_onboarding_users:discover_agent:enable',\n ThreadsReplyRankingExplorationEnable = 'threads:reply_ranking_exploration:enable',\n SearchFilteringExplorationEnable = 'search:filtering_exploration:enable',\n}\n\n/**\n * Pre-evaluated feature gates map, the result of `FeatureGates.checkGates()`\n */\nexport type CheckedFeatureGatesMap = Map<FeatureGateID, boolean>\n\nexport class FeatureGates {\n ready = false\n client: GrowthBookClient | undefined = undefined\n ids = FeatureGateID\n refreshInterval: NodeJS.Timeout | undefined = undefined\n\n constructor(private config: Config) {}\n\n async start() {\n try {\n if (this.config.apiHost && this.config.clientKey) {\n this.client = new GrowthBookClient({\n apiHost: this.config.apiHost,\n clientKey: this.config.clientKey,\n })\n\n const { source, error } = await this.client.init({\n timeout: FETCH_TIMEOUT,\n })\n\n /**\n * This does not necessarily mean that the client completely failed,\n * since it could just be that the request timed out. It may succeed\n * after the timeout, or later during refreshes.\n *\n * @see https://docs.growthbook.io/lib/node#error-handling\n */\n if (error) {\n featureGatesLogger.error(\n { err: error, source },\n 'Client failed to initialize normally',\n )\n }\n\n /**\n * Set up periodic refresh of feature definitions\n *\n * @see https://docs.growthbook.io/lib/node#refreshing-features\n */\n this.refreshInterval = setInterval(async () => {\n try {\n await this.client?.refreshFeatures({\n timeout: FETCH_TIMEOUT,\n })\n } catch (err) {\n featureGatesLogger.error({ err }, 'Failed to refresh features')\n }\n }, REFETCH_INTERVAL)\n\n /* Ready or not, here we come */\n this.ready = true\n } else {\n featureGatesLogger.error(\n 'Missing required config for FeatureGates client',\n )\n }\n } catch (err) {\n featureGatesLogger.error({ err }, 'Client initialization failed')\n this.ready = false\n }\n }\n\n destroy() {\n if (this.ready) {\n this.ready = false\n if (this.refreshInterval) {\n clearInterval(this.refreshInterval)\n }\n }\n }\n\n userContext({\n did,\n }: Exclude<UserContext['attributes'], undefined>): UserContext {\n return { attributes: { did: did ?? null } }\n }\n\n check(gate: FeatureGateID, ctx: UserContext): boolean {\n if (!this.ready || !this.client) return false\n return this.client.isOn(gate, ctx)\n }\n\n /**\n * Pre-evaluate multiple feature gates for a given user, returning a map of\n * gate ID to boolean result.\n */\n checkGates(gates: FeatureGateID[], ctx: UserContext): CheckedFeatureGatesMap {\n return new Map(gates.map((g) => [g, this.check(g, ctx)]))\n }\n}\n"]}
|
package/src/feature-gates.ts
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
GrowthBookClient,
|
|
3
|
-
type UserContext as GrowthBookUserContext,
|
|
4
|
-
} from '@growthbook/growthbook'
|
|
5
|
-
import { featureGatesLogger } from './logger'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* We want this to be sufficiently high that we don't time out under
|
|
9
|
-
* normal conditions, but not so high that it takes too long to boot
|
|
10
|
-
* the server.
|
|
11
|
-
*/
|
|
12
|
-
const FETCH_TIMEOUT = 3e3 // 3 seconds
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* StatSig used to default to every 10s, but I think 1m is fine
|
|
16
|
-
*/
|
|
17
|
-
const REFETCH_INTERVAL = 60e3 // 1 minute
|
|
18
|
-
|
|
19
|
-
export type Config = {
|
|
20
|
-
apiHost?: string
|
|
21
|
-
clientKey?: string
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
type UserContext = Omit<GrowthBookUserContext, 'attributes'> & {
|
|
25
|
-
attributes?: {
|
|
26
|
-
did?: string | null
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export enum FeatureGateID {
|
|
31
|
-
/**
|
|
32
|
-
* Left here ensure this is interpreted as a string enum and therefore
|
|
33
|
-
* appease TS
|
|
34
|
-
*/
|
|
35
|
-
_ = '',
|
|
36
|
-
SuggestedUsersDiscoverAgentEnable = 'suggested_users:discover_agent:enable',
|
|
37
|
-
SuggestedOnboardingUsersDiscoverAgentEnable = 'suggested_onboarding_users:discover_agent:enable',
|
|
38
|
-
ThreadsReplyRankingExplorationEnable = 'threads:reply_ranking_exploration:enable',
|
|
39
|
-
SearchFilteringExplorationEnable = 'search:filtering_exploration:enable',
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Pre-evaluated feature gates map, the result of `FeatureGates.checkGates()`
|
|
44
|
-
*/
|
|
45
|
-
export type CheckedFeatureGatesMap = Map<FeatureGateID, boolean>
|
|
46
|
-
|
|
47
|
-
export class FeatureGates {
|
|
48
|
-
ready = false
|
|
49
|
-
client: GrowthBookClient | undefined = undefined
|
|
50
|
-
ids = FeatureGateID
|
|
51
|
-
refreshInterval: NodeJS.Timeout | undefined = undefined
|
|
52
|
-
|
|
53
|
-
constructor(private config: Config) {}
|
|
54
|
-
|
|
55
|
-
async start() {
|
|
56
|
-
try {
|
|
57
|
-
if (this.config.apiHost && this.config.clientKey) {
|
|
58
|
-
this.client = new GrowthBookClient({
|
|
59
|
-
apiHost: this.config.apiHost,
|
|
60
|
-
clientKey: this.config.clientKey,
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
const { source, error } = await this.client.init({
|
|
64
|
-
timeout: FETCH_TIMEOUT,
|
|
65
|
-
})
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* This does not necessarily mean that the client completely failed,
|
|
69
|
-
* since it could just be that the request timed out. It may succeed
|
|
70
|
-
* after the timeout, or later during refreshes.
|
|
71
|
-
*
|
|
72
|
-
* @see https://docs.growthbook.io/lib/node#error-handling
|
|
73
|
-
*/
|
|
74
|
-
if (error) {
|
|
75
|
-
featureGatesLogger.error(
|
|
76
|
-
{ err: error, source },
|
|
77
|
-
'Client failed to initialize normally',
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Set up periodic refresh of feature definitions
|
|
83
|
-
*
|
|
84
|
-
* @see https://docs.growthbook.io/lib/node#refreshing-features
|
|
85
|
-
*/
|
|
86
|
-
this.refreshInterval = setInterval(async () => {
|
|
87
|
-
try {
|
|
88
|
-
await this.client?.refreshFeatures({
|
|
89
|
-
timeout: FETCH_TIMEOUT,
|
|
90
|
-
})
|
|
91
|
-
} catch (err) {
|
|
92
|
-
featureGatesLogger.error({ err }, 'Failed to refresh features')
|
|
93
|
-
}
|
|
94
|
-
}, REFETCH_INTERVAL)
|
|
95
|
-
|
|
96
|
-
/* Ready or not, here we come */
|
|
97
|
-
this.ready = true
|
|
98
|
-
} else {
|
|
99
|
-
featureGatesLogger.error(
|
|
100
|
-
'Missing required config for FeatureGates client',
|
|
101
|
-
)
|
|
102
|
-
}
|
|
103
|
-
} catch (err) {
|
|
104
|
-
featureGatesLogger.error({ err }, 'Client initialization failed')
|
|
105
|
-
this.ready = false
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
destroy() {
|
|
110
|
-
if (this.ready) {
|
|
111
|
-
this.ready = false
|
|
112
|
-
if (this.refreshInterval) {
|
|
113
|
-
clearInterval(this.refreshInterval)
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
userContext({
|
|
119
|
-
did,
|
|
120
|
-
}: Exclude<UserContext['attributes'], undefined>): UserContext {
|
|
121
|
-
return { attributes: { did: did ?? null } }
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
check(gate: FeatureGateID, ctx: UserContext): boolean {
|
|
125
|
-
if (!this.ready || !this.client) return false
|
|
126
|
-
return this.client.isOn(gate, ctx)
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Pre-evaluate multiple feature gates for a given user, returning a map of
|
|
131
|
-
* gate ID to boolean result.
|
|
132
|
-
*/
|
|
133
|
-
checkGates(gates: FeatureGateID[], ctx: UserContext): CheckedFeatureGatesMap {
|
|
134
|
-
return new Map(gates.map((g) => [g, this.check(g, ctx)]))
|
|
135
|
-
}
|
|
136
|
-
}
|