@jewel998/config 0.2.2 → 0.2.6
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 +30 -14
- package/dist/cache/types.d.ts +1 -1
- package/dist/createConfig.js +9 -6
- package/dist/errors/index.d.ts +18 -0
- package/dist/errors/index.js +12 -0
- package/dist/events/EventEmitter.d.ts +21 -0
- package/dist/events/EventEmitter.js +54 -8
- package/dist/fetch/TierFetcher.d.ts +77 -0
- package/dist/fetch/TierFetcher.js +162 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/initConfig.d.ts +84 -53
- package/dist/initConfig.js +360 -90
- package/dist/plugins/lifecycle/index.d.ts +1 -1
- package/dist/plugins/lifecycle/index.js +1 -1
- package/dist/plugins/rollout/murmurhash3.js +1 -5
- package/dist/retry/RetryEngine.js +1 -1
- package/dist/server/initServerConfig.d.ts +110 -0
- package/dist/server/initServerConfig.js +159 -0
- package/dist/server/serverContext.d.ts +63 -0
- package/dist/server/serverContext.js +64 -0
- package/dist/server.d.ts +10 -0
- package/dist/server.js +20 -0
- package/dist/transport/HttpTransport.js +26 -16
- package/dist/types.d.ts +3 -1
- package/dist/webhook-formats.js +1 -6
- package/package.json +36 -25
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @jewel998/config
|
|
2
2
|
|
|
3
|
+
> See also: [Getting Started](/guide/getting-started) · [SDK Reference](/api/) · [Self-Hosting](/guide/self-hosting) · [Cloud Functions](/api/cloud-functions)
|
|
4
|
+
|
|
3
5
|
[](https://www.npmjs.com/package/@jewel998/config)
|
|
4
6
|
[](https://www.npmjs.com/package/@jewel998/config)
|
|
5
7
|
[](./LICENSE)
|
|
@@ -12,12 +14,16 @@ A free, self-hostable feature flag and remote configuration platform. Deploy to
|
|
|
12
14
|
## Features
|
|
13
15
|
|
|
14
16
|
- **Feature flags** — Boolean, string, number, JSON, array value types
|
|
15
|
-
- **
|
|
16
|
-
- **
|
|
17
|
-
- **
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
20
|
-
- **
|
|
17
|
+
- **A/B testing & multivariate testing** — Targeting rules with variant arms; sticky bucketing keeps users in a consistent group
|
|
18
|
+
- **Canary releases & progressive delivery** — Percentage rollouts with deterministic bucketing (MurmurHash3); ramp 5% → 100% safely
|
|
19
|
+
- **Kill switch** — Instantly revert any feature to its safe state with zero code deploy
|
|
20
|
+
- **Targeting rules** — User segmentation: serve different values based on attributes (plan, country, role, etc.)
|
|
21
|
+
- **Segments** — Reusable audience cohorts with one-click targeting across all flags
|
|
22
|
+
- **Ring deployments** — Combine segments + rollouts to release through internal → beta → general rings
|
|
23
|
+
- **Dark launch** — Deploy code behind a flag, activate on schedule without a new deploy
|
|
24
|
+
- **Timed release** — Schedule flag changes for a precise date/time
|
|
25
|
+
- **Prerequisites / guard flags** — Dependency-based feature gating; prevent inconsistent feature states
|
|
26
|
+
- **Environments** — Separate dev/staging/production configs, keys, and targeting rules
|
|
21
27
|
- **Audit log** — Track every change with actor, timestamp, and diff viewer
|
|
22
28
|
- **Webhooks** — Notify Slack, Discord, Google Chat, MS Teams, or custom endpoints
|
|
23
29
|
- **RBAC** — Viewer, Editor, Admin roles per project
|
|
@@ -52,21 +58,30 @@ import { initConfig, autoContext } from "@jewel998/config";
|
|
|
52
58
|
|
|
53
59
|
const flags = initConfig({
|
|
54
60
|
clientId: "cid_xxx", // From your Portal → API Keys
|
|
55
|
-
baseUrl: "https://your-project.web.app/api",
|
|
61
|
+
baseUrl: "https://your-project.web.app/api",
|
|
62
|
+
prefetch: ["app.maintenance_mode"], // Fetch these immediately, block ready()
|
|
56
63
|
defaults: {
|
|
57
64
|
"feature.dark_mode": false,
|
|
58
65
|
"app.upload_limit": 50,
|
|
59
66
|
},
|
|
60
67
|
context: autoContext({ userId: "user_123", plan: "pro" }),
|
|
68
|
+
onError: (err) => console.error(err.type, err.key),
|
|
61
69
|
});
|
|
62
70
|
|
|
63
|
-
//
|
|
64
|
-
flags.
|
|
71
|
+
// Wait for prefetched keys before rendering
|
|
72
|
+
await flags.ready();
|
|
65
73
|
|
|
66
|
-
//
|
|
67
|
-
flags.
|
|
68
|
-
|
|
69
|
-
|
|
74
|
+
// Instant — in memory or has a default
|
|
75
|
+
const darkMode = await flags.get<boolean>("feature.dark_mode"); // → true
|
|
76
|
+
|
|
77
|
+
// Declare page-level keys (Tier 2, fire-and-forget)
|
|
78
|
+
flags.prefetch(["feature.new_checkout", "app.upload_limit"]);
|
|
79
|
+
|
|
80
|
+
// React to any key changing
|
|
81
|
+
flags.on("updated", ({ keys }) => rerender());
|
|
82
|
+
|
|
83
|
+
// React to a specific key changing
|
|
84
|
+
flags.on("updated:feature.dark_mode", (value) => applyTheme(value));
|
|
70
85
|
```
|
|
71
86
|
|
|
72
87
|
### API Key Types
|
|
@@ -103,7 +118,7 @@ config.getFlag("feature.checkout_v2"); // Evaluated locally — no round-trip
|
|
|
103
118
|
│ │
|
|
104
119
|
│ import { initConfig } from "@jewel998/config" │
|
|
105
120
|
│ const flags = initConfig({ clientId, baseUrl }) │
|
|
106
|
-
│ flags.get("feature.dark_mode") → true
|
|
121
|
+
│ await flags.get("feature.dark_mode") → true │
|
|
107
122
|
└──────────────────────┬──────────────────────────────┘
|
|
108
123
|
│ POST /api/v1/config
|
|
109
124
|
▼
|
|
@@ -192,6 +207,7 @@ apps/
|
|
|
192
207
|
landing/ — Marketing site (Astro + React)
|
|
193
208
|
docs/ — Documentation (VitePress)
|
|
194
209
|
packages/
|
|
210
|
+
api/ — Internal micro-framework for Cloud Functions (@jewel998/api)
|
|
195
211
|
config/ — SDK (@jewel998/config)
|
|
196
212
|
tour/ — Tour framework (@jewel998/tour)
|
|
197
213
|
functions/ — Firebase Cloud Functions (API + webhooks)
|
package/dist/cache/types.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export type { CacheEntry, CacheStorage, BrowserStorageOptions
|
|
1
|
+
export type { CacheEntry, CacheStorage, BrowserStorageOptions } from "../types.js";
|
|
2
2
|
export { DEFAULT_CACHE_TTL } from "../types.js";
|
package/dist/createConfig.js
CHANGED
|
@@ -15,9 +15,14 @@ export function createConfig(options) {
|
|
|
15
15
|
if (!options.clientId) {
|
|
16
16
|
throw new ConfigError("clientId is required", "MISSING_CLIENT_ID");
|
|
17
17
|
}
|
|
18
|
-
// 2. Check browser environment
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// 2. Check browser environment (for client keys only)
|
|
19
|
+
// Server keys (svr_) can run in any environment.
|
|
20
|
+
// Client keys (cid_) require a browser because they rely on server-side evaluation
|
|
21
|
+
// with origin/referer headers for domain validation.
|
|
22
|
+
if (typeof window === "undefined" && !options.clientId.startsWith("svr_")) {
|
|
23
|
+
throw new ConfigError("Client keys (cid_) require a browser environment. " +
|
|
24
|
+
"For server-side usage, use `initServerConfig` from '@jewel998/config/server' " +
|
|
25
|
+
"with a server key (svr_).", "INITIALIZATION_FAILED");
|
|
21
26
|
}
|
|
22
27
|
// 3. Resolve defaults
|
|
23
28
|
const strategy = options.loadingStrategy ?? "optimistic";
|
|
@@ -42,9 +47,7 @@ export function createConfig(options) {
|
|
|
42
47
|
getContext: () => currentContext,
|
|
43
48
|
});
|
|
44
49
|
const events = new TypedEventEmitter();
|
|
45
|
-
const fetcher = granularity === "batch"
|
|
46
|
-
? createBatchFetcher(transport)
|
|
47
|
-
: createProjectedFetcher(transport);
|
|
50
|
+
const fetcher = granularity === "batch" ? createBatchFetcher(transport) : createProjectedFetcher(transport);
|
|
48
51
|
// 6. Create loading context
|
|
49
52
|
const context = {
|
|
50
53
|
clientId: options.clientId,
|
package/dist/errors/index.d.ts
CHANGED
|
@@ -19,3 +19,21 @@ export declare class TimeoutError extends ConfigError {
|
|
|
19
19
|
export declare class AuthenticationError extends ConfigError {
|
|
20
20
|
constructor(message: string);
|
|
21
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Error type passed to onError and thrown by get() on failure.
|
|
24
|
+
*
|
|
25
|
+
* type:
|
|
26
|
+
* TIMEOUT — get() waited longer than global timeout, no default provided
|
|
27
|
+
* FETCH_FAILED — network error or non-2xx response during any tier fetch
|
|
28
|
+
* KEY_NOT_FOUND — key does not exist in the project
|
|
29
|
+
* AUTH — 401/403 from the API (circuit breaker opens)
|
|
30
|
+
* RATE_LIMITED — 429 from the API
|
|
31
|
+
*/
|
|
32
|
+
export type SdkErrorType = "TIMEOUT" | "FETCH_FAILED" | "KEY_NOT_FOUND" | "AUTH" | "RATE_LIMITED";
|
|
33
|
+
export declare class SdkError extends Error {
|
|
34
|
+
readonly type: SdkErrorType;
|
|
35
|
+
readonly key?: string | undefined;
|
|
36
|
+
readonly cause?: Error | undefined;
|
|
37
|
+
name: string;
|
|
38
|
+
constructor(type: SdkErrorType, message: string, key?: string | undefined, cause?: Error | undefined);
|
|
39
|
+
}
|
package/dist/errors/index.js
CHANGED
|
@@ -35,4 +35,16 @@ export class AuthenticationError extends ConfigError {
|
|
|
35
35
|
this.name = "AuthenticationError";
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
|
+
export class SdkError extends Error {
|
|
39
|
+
type;
|
|
40
|
+
key;
|
|
41
|
+
cause;
|
|
42
|
+
name = "SdkError";
|
|
43
|
+
constructor(type, message, key, cause) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.type = type;
|
|
46
|
+
this.key = key;
|
|
47
|
+
this.cause = cause;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
38
50
|
//# sourceMappingURL=index.js.map
|
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import type { ConfigEventCallback, ConfigEventPayloads, ConfigEventType, EventEmitterInterface } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* TypedEventEmitter supports two event patterns:
|
|
4
|
+
*
|
|
5
|
+
* 1. Typed batch events (ConfigEventType):
|
|
6
|
+
* emitter.on("updated", ({ keys }) => ...)
|
|
7
|
+
* emitter.on("fetchError", ({ error }) => ...)
|
|
8
|
+
*
|
|
9
|
+
* 2. Key-specific events (template literal `updated:${key}`):
|
|
10
|
+
* emitter.on("updated:feature.dark_mode", (value) => ...)
|
|
11
|
+
* Fired individually per key alongside the batch "updated" event.
|
|
12
|
+
*/
|
|
2
13
|
export declare class TypedEventEmitter implements EventEmitterInterface {
|
|
3
14
|
private listeners;
|
|
15
|
+
private keyListeners;
|
|
4
16
|
on<E extends ConfigEventType>(event: E, callback: ConfigEventCallback<E>): void;
|
|
5
17
|
off<E extends ConfigEventType>(event: E, callback: ConfigEventCallback<E>): void;
|
|
18
|
+
/**
|
|
19
|
+
* Subscribe to a key-specific update event.
|
|
20
|
+
* Fires when that exact key's value is updated.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* emitter.onKey("feature.dark_mode", (value) => applyTheme(value));
|
|
24
|
+
*/
|
|
25
|
+
onKey(key: string, callback: (value: unknown) => void): void;
|
|
26
|
+
offKey(key: string, callback: (value: unknown) => void): void;
|
|
6
27
|
emit<E extends ConfigEventType>(event: E, payload: ConfigEventPayloads[E]): void;
|
|
7
28
|
removeAllListeners(): void;
|
|
8
29
|
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypedEventEmitter supports two event patterns:
|
|
3
|
+
*
|
|
4
|
+
* 1. Typed batch events (ConfigEventType):
|
|
5
|
+
* emitter.on("updated", ({ keys }) => ...)
|
|
6
|
+
* emitter.on("fetchError", ({ error }) => ...)
|
|
7
|
+
*
|
|
8
|
+
* 2. Key-specific events (template literal `updated:${key}`):
|
|
9
|
+
* emitter.on("updated:feature.dark_mode", (value) => ...)
|
|
10
|
+
* Fired individually per key alongside the batch "updated" event.
|
|
11
|
+
*/
|
|
1
12
|
export class TypedEventEmitter {
|
|
2
13
|
listeners = new Map();
|
|
14
|
+
keyListeners = new Map();
|
|
3
15
|
on(event, callback) {
|
|
4
16
|
if (!this.listeners.has(event)) {
|
|
5
17
|
this.listeners.set(event, new Set());
|
|
@@ -9,22 +21,56 @@ export class TypedEventEmitter {
|
|
|
9
21
|
off(event, callback) {
|
|
10
22
|
this.listeners.get(event)?.delete(callback);
|
|
11
23
|
}
|
|
24
|
+
/**
|
|
25
|
+
* Subscribe to a key-specific update event.
|
|
26
|
+
* Fires when that exact key's value is updated.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* emitter.onKey("feature.dark_mode", (value) => applyTheme(value));
|
|
30
|
+
*/
|
|
31
|
+
onKey(key, callback) {
|
|
32
|
+
if (!this.keyListeners.has(key)) {
|
|
33
|
+
this.keyListeners.set(key, new Set());
|
|
34
|
+
}
|
|
35
|
+
this.keyListeners.get(key).add(callback);
|
|
36
|
+
}
|
|
37
|
+
offKey(key, callback) {
|
|
38
|
+
this.keyListeners.get(key)?.delete(callback);
|
|
39
|
+
}
|
|
12
40
|
emit(event, payload) {
|
|
41
|
+
// Fire batch listeners
|
|
13
42
|
const callbacks = this.listeners.get(event);
|
|
14
|
-
if (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
43
|
+
if (callbacks) {
|
|
44
|
+
for (const cb of callbacks) {
|
|
45
|
+
try {
|
|
46
|
+
cb(payload);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
console.error(`[config] Event handler error for "${event}":`, err);
|
|
50
|
+
}
|
|
20
51
|
}
|
|
21
|
-
|
|
22
|
-
|
|
52
|
+
}
|
|
53
|
+
// For "updated" events, also fire per-key listeners
|
|
54
|
+
if (event === "updated") {
|
|
55
|
+
const updatedPayload = payload;
|
|
56
|
+
for (const key of updatedPayload.keys) {
|
|
57
|
+
const keyCallbacks = this.keyListeners.get(key);
|
|
58
|
+
if (keyCallbacks) {
|
|
59
|
+
for (const cb of keyCallbacks) {
|
|
60
|
+
try {
|
|
61
|
+
cb(key); // value is read from data store by the subscriber
|
|
62
|
+
}
|
|
63
|
+
catch (err) {
|
|
64
|
+
console.error(`[config] Key listener error for "updated:${key}":`, err);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
23
68
|
}
|
|
24
69
|
}
|
|
25
70
|
}
|
|
26
71
|
removeAllListeners() {
|
|
27
72
|
this.listeners.clear();
|
|
73
|
+
this.keyListeners.clear();
|
|
28
74
|
}
|
|
29
75
|
}
|
|
30
76
|
//# sourceMappingURL=EventEmitter.js.map
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ConfigFetcher } from "../types.js";
|
|
2
|
+
export type FetchTier = "prefetch" | "page" | "idle";
|
|
3
|
+
/**
|
|
4
|
+
* TierFetcher — wraps a ConfigFetcher with three responsibilities:
|
|
5
|
+
*
|
|
6
|
+
* 1. Tier tracking: records which keys were fetched under which tier
|
|
7
|
+
* so context re-fetches are scoped to already-fetched keys only.
|
|
8
|
+
*
|
|
9
|
+
* 2. Cross-tier deduplication: a key registered in a higher-priority tier
|
|
10
|
+
* is never re-fetched by a lower-priority tier call.
|
|
11
|
+
* Priority order: prefetch > page > idle
|
|
12
|
+
*
|
|
13
|
+
* 3. Pending waiters: get() calls that arrive for an unfetched key
|
|
14
|
+
* register a waiter here. When the key eventually arrives (via idle
|
|
15
|
+
* fetch or refresh), all waiters for that key are resolved.
|
|
16
|
+
*/
|
|
17
|
+
export declare class TierFetcher {
|
|
18
|
+
private readonly fetcher;
|
|
19
|
+
private readonly fetchedKeys;
|
|
20
|
+
private readonly allFetched;
|
|
21
|
+
private readonly waiters;
|
|
22
|
+
constructor(fetcher: ConfigFetcher);
|
|
23
|
+
/**
|
|
24
|
+
* Fetch a specific set of keys under a given tier.
|
|
25
|
+
* Keys already tracked under any tier are skipped (deduplication).
|
|
26
|
+
*/
|
|
27
|
+
fetchTier(keys: string[], tier: FetchTier): Promise<Record<string, unknown>>;
|
|
28
|
+
/**
|
|
29
|
+
* Fetch all remaining keys (idle tier).
|
|
30
|
+
* Only keys not already tracked are added to the idle set.
|
|
31
|
+
*/
|
|
32
|
+
fetchAll(): Promise<Record<string, unknown>>;
|
|
33
|
+
/** Check the remote version (passthrough). */
|
|
34
|
+
checkVersion(): ReturnType<ConfigFetcher["checkVersion"]>;
|
|
35
|
+
/**
|
|
36
|
+
* Re-fetch specific keys without deduplication check.
|
|
37
|
+
* Used by doRefresh() — we're intentionally re-fetching already-fetched keys
|
|
38
|
+
* with a new context, so the allFetched deduplication should be bypassed.
|
|
39
|
+
*/
|
|
40
|
+
refetchKeys(keys: string[]): Promise<Record<string, unknown>>;
|
|
41
|
+
/**
|
|
42
|
+
* Record keys as fetched under a tier WITHOUT performing a network fetch.
|
|
43
|
+
* Used when values are hydrated from persistent storage (the boot gate),
|
|
44
|
+
* so later context re-fetches correctly scope to these keys.
|
|
45
|
+
*/
|
|
46
|
+
markFetched(keys: string[], tier: FetchTier): void;
|
|
47
|
+
/**
|
|
48
|
+
* Register a waiter for a key that hasn't been fetched yet.
|
|
49
|
+
* Returns a Promise that resolves when the key arrives, or rejects on error.
|
|
50
|
+
*/
|
|
51
|
+
waitForKey(key: string): Promise<unknown>;
|
|
52
|
+
/**
|
|
53
|
+
* Notify all waiters for the given keys with their resolved values.
|
|
54
|
+
* Called by initConfig after any successful fetch.
|
|
55
|
+
*/
|
|
56
|
+
notifyWaiters(result: Record<string, unknown>): void;
|
|
57
|
+
/**
|
|
58
|
+
* Reject all waiters for all pending keys.
|
|
59
|
+
* Called when a fetch fails and we need to unblock all suspended get() calls.
|
|
60
|
+
*/
|
|
61
|
+
rejectAllWaiters(error: Error): void;
|
|
62
|
+
/**
|
|
63
|
+
* Reject waiters for specific keys only.
|
|
64
|
+
* Used when a targeted fetch fails for a subset of keys.
|
|
65
|
+
*/
|
|
66
|
+
rejectWaiters(keys: string[], error: Error): void;
|
|
67
|
+
/** Whether there are any pending waiters for a given key. */
|
|
68
|
+
hasWaiters(key: string): boolean;
|
|
69
|
+
getFetchedKeys(): {
|
|
70
|
+
prefetch: string[];
|
|
71
|
+
page: string[];
|
|
72
|
+
idle: string[];
|
|
73
|
+
};
|
|
74
|
+
isKeyFetched(key: string): boolean;
|
|
75
|
+
hasFetchedTier(tier: FetchTier): boolean;
|
|
76
|
+
private recordKeys;
|
|
77
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TierFetcher — wraps a ConfigFetcher with three responsibilities:
|
|
3
|
+
*
|
|
4
|
+
* 1. Tier tracking: records which keys were fetched under which tier
|
|
5
|
+
* so context re-fetches are scoped to already-fetched keys only.
|
|
6
|
+
*
|
|
7
|
+
* 2. Cross-tier deduplication: a key registered in a higher-priority tier
|
|
8
|
+
* is never re-fetched by a lower-priority tier call.
|
|
9
|
+
* Priority order: prefetch > page > idle
|
|
10
|
+
*
|
|
11
|
+
* 3. Pending waiters: get() calls that arrive for an unfetched key
|
|
12
|
+
* register a waiter here. When the key eventually arrives (via idle
|
|
13
|
+
* fetch or refresh), all waiters for that key are resolved.
|
|
14
|
+
*/
|
|
15
|
+
export class TierFetcher {
|
|
16
|
+
fetcher;
|
|
17
|
+
// Keys fetched per tier — used to scope context re-fetches
|
|
18
|
+
fetchedKeys = new Map([
|
|
19
|
+
["prefetch", new Set()],
|
|
20
|
+
["page", new Set()],
|
|
21
|
+
["idle", new Set()],
|
|
22
|
+
]);
|
|
23
|
+
// All fetched keys across all tiers — fast membership check for deduplication
|
|
24
|
+
allFetched = new Set();
|
|
25
|
+
// Pending waiters for suspended get() calls
|
|
26
|
+
// key → array of { resolve, reject } pairs
|
|
27
|
+
waiters = new Map();
|
|
28
|
+
constructor(fetcher) {
|
|
29
|
+
this.fetcher = fetcher;
|
|
30
|
+
}
|
|
31
|
+
// ─── Fetch methods ───────────────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* Fetch a specific set of keys under a given tier.
|
|
34
|
+
* Keys already tracked under any tier are skipped (deduplication).
|
|
35
|
+
*/
|
|
36
|
+
async fetchTier(keys, tier) {
|
|
37
|
+
const toFetch = keys.filter((k) => !this.allFetched.has(k));
|
|
38
|
+
if (toFetch.length === 0)
|
|
39
|
+
return {};
|
|
40
|
+
const result = await this.fetcher.fetchKeys(toFetch);
|
|
41
|
+
this.recordKeys(Object.keys(result), tier);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Fetch all remaining keys (idle tier).
|
|
46
|
+
* Only keys not already tracked are added to the idle set.
|
|
47
|
+
*/
|
|
48
|
+
async fetchAll() {
|
|
49
|
+
const result = await this.fetcher.fetchAll();
|
|
50
|
+
for (const key of Object.keys(result)) {
|
|
51
|
+
if (!this.allFetched.has(key)) {
|
|
52
|
+
this.fetchedKeys.get("idle").add(key);
|
|
53
|
+
this.allFetched.add(key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
/** Check the remote version (passthrough). */
|
|
59
|
+
checkVersion() {
|
|
60
|
+
return this.fetcher.checkVersion();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Re-fetch specific keys without deduplication check.
|
|
64
|
+
* Used by doRefresh() — we're intentionally re-fetching already-fetched keys
|
|
65
|
+
* with a new context, so the allFetched deduplication should be bypassed.
|
|
66
|
+
*/
|
|
67
|
+
async refetchKeys(keys) {
|
|
68
|
+
return this.fetcher.fetchKeys(keys);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Record keys as fetched under a tier WITHOUT performing a network fetch.
|
|
72
|
+
* Used when values are hydrated from persistent storage (the boot gate),
|
|
73
|
+
* so later context re-fetches correctly scope to these keys.
|
|
74
|
+
*/
|
|
75
|
+
markFetched(keys, tier) {
|
|
76
|
+
this.recordKeys(keys, tier);
|
|
77
|
+
}
|
|
78
|
+
// ─── Waiter management ───────────────────────────────────────
|
|
79
|
+
/**
|
|
80
|
+
* Register a waiter for a key that hasn't been fetched yet.
|
|
81
|
+
* Returns a Promise that resolves when the key arrives, or rejects on error.
|
|
82
|
+
*/
|
|
83
|
+
waitForKey(key) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
if (!this.waiters.has(key)) {
|
|
86
|
+
this.waiters.set(key, []);
|
|
87
|
+
}
|
|
88
|
+
this.waiters.get(key).push({ resolve, reject });
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Notify all waiters for the given keys with their resolved values.
|
|
93
|
+
* Called by initConfig after any successful fetch.
|
|
94
|
+
*/
|
|
95
|
+
notifyWaiters(result) {
|
|
96
|
+
for (const [key, value] of Object.entries(result)) {
|
|
97
|
+
const keyWaiters = this.waiters.get(key);
|
|
98
|
+
if (keyWaiters && keyWaiters.length > 0) {
|
|
99
|
+
for (const { resolve } of keyWaiters) {
|
|
100
|
+
resolve(value);
|
|
101
|
+
}
|
|
102
|
+
this.waiters.delete(key);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Reject all waiters for all pending keys.
|
|
108
|
+
* Called when a fetch fails and we need to unblock all suspended get() calls.
|
|
109
|
+
*/
|
|
110
|
+
rejectAllWaiters(error) {
|
|
111
|
+
for (const [key, keyWaiters] of this.waiters) {
|
|
112
|
+
for (const { reject } of keyWaiters) {
|
|
113
|
+
reject(error);
|
|
114
|
+
}
|
|
115
|
+
this.waiters.delete(key);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Reject waiters for specific keys only.
|
|
120
|
+
* Used when a targeted fetch fails for a subset of keys.
|
|
121
|
+
*/
|
|
122
|
+
rejectWaiters(keys, error) {
|
|
123
|
+
for (const key of keys) {
|
|
124
|
+
const keyWaiters = this.waiters.get(key);
|
|
125
|
+
if (keyWaiters) {
|
|
126
|
+
for (const { reject } of keyWaiters) {
|
|
127
|
+
reject(error);
|
|
128
|
+
}
|
|
129
|
+
this.waiters.delete(key);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Whether there are any pending waiters for a given key. */
|
|
134
|
+
hasWaiters(key) {
|
|
135
|
+
return (this.waiters.get(key)?.length ?? 0) > 0;
|
|
136
|
+
}
|
|
137
|
+
// ─── Tier introspection ──────────────────────────────────────
|
|
138
|
+
getFetchedKeys() {
|
|
139
|
+
return {
|
|
140
|
+
prefetch: Array.from(this.fetchedKeys.get("prefetch")),
|
|
141
|
+
page: Array.from(this.fetchedKeys.get("page")),
|
|
142
|
+
idle: Array.from(this.fetchedKeys.get("idle")),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
isKeyFetched(key) {
|
|
146
|
+
return this.allFetched.has(key);
|
|
147
|
+
}
|
|
148
|
+
hasFetchedTier(tier) {
|
|
149
|
+
return this.fetchedKeys.get(tier).size > 0;
|
|
150
|
+
}
|
|
151
|
+
// ─── Private helpers ─────────────────────────────────────────
|
|
152
|
+
recordKeys(keys, tier) {
|
|
153
|
+
const tierSet = this.fetchedKeys.get(tier);
|
|
154
|
+
for (const key of keys) {
|
|
155
|
+
if (!this.allFetched.has(key)) {
|
|
156
|
+
tierSet.add(key);
|
|
157
|
+
this.allFetched.add(key);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=TierFetcher.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -9,5 +9,5 @@ export { WEBHOOK_FORMATS, WEBHOOK_FORMAT_INFO, TEMPLATE_VARIABLES, WEBHOOK_EVENT
|
|
|
9
9
|
export type { WebhookFormat, WebhookEventType, WebhookResourceCategory, } from "./webhook-formats.js";
|
|
10
10
|
export { memoryStorage } from "./cache/memoryStorage.js";
|
|
11
11
|
export { browserStorage } from "./cache/browserStorage.js";
|
|
12
|
-
export { AuthenticationError, ConfigError, InitializationError, RateLimitError, TimeoutError, } from "./errors/index.js";
|
|
13
|
-
export type { ConfigErrorCode } from "./errors/index.js";
|
|
12
|
+
export { AuthenticationError, ConfigError, InitializationError, RateLimitError, SdkError, TimeoutError, } from "./errors/index.js";
|
|
13
|
+
export type { ConfigErrorCode, SdkErrorType } from "./errors/index.js";
|
package/dist/index.js
CHANGED
|
@@ -14,5 +14,5 @@ export { WEBHOOK_FORMATS, WEBHOOK_FORMAT_INFO, TEMPLATE_VARIABLES, WEBHOOK_EVENT
|
|
|
14
14
|
export { memoryStorage } from "./cache/memoryStorage.js";
|
|
15
15
|
export { browserStorage } from "./cache/browserStorage.js";
|
|
16
16
|
// Errors
|
|
17
|
-
export { AuthenticationError, ConfigError, InitializationError, RateLimitError, TimeoutError, } from "./errors/index.js";
|
|
17
|
+
export { AuthenticationError, ConfigError, InitializationError, RateLimitError, SdkError, TimeoutError, } from "./errors/index.js";
|
|
18
18
|
//# sourceMappingURL=index.js.map
|