@jewel998/config 0.2.1 → 0.2.4
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 +32 -16
- package/dist/cache/types.d.ts +1 -1
- package/dist/createConfig.js +9 -6
- package/dist/errors/index.d.ts +23 -0
- package/dist/errors/index.js +21 -0
- package/dist/events/EventEmitter.d.ts +21 -0
- package/dist/events/EventEmitter.js +54 -8
- package/dist/fetch/TierFetcher.d.ts +71 -0
- package/dist/fetch/TierFetcher.js +154 -0
- package/dist/fetch/batchFetcher.js +3 -3
- package/dist/fetch/projectedFetcher.js +3 -3
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/initConfig.d.ts +79 -54
- package/dist/initConfig.js +256 -83
- 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 +18 -6
- 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 +29 -12
- package/dist/types.d.ts +3 -1
- package/dist/webhook-formats.js +1 -6
- package/package.json +36 -25
package/dist/initConfig.d.ts
CHANGED
|
@@ -1,78 +1,103 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { SdkError } from "./errors/index.js";
|
|
2
2
|
import type { EvaluationContext } from "./plugins/types.js";
|
|
3
|
+
import type { CacheStorage } from "./types.js";
|
|
3
4
|
export interface InitConfigOptions {
|
|
4
5
|
/** Required. API key from the portal (cid_ for client, svr_ for server). */
|
|
5
6
|
clientId: string;
|
|
6
7
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
8
|
+
* Tier 1 keys — fetched immediately, block ready() resolution.
|
|
9
|
+
* Use for values your app cannot render without.
|
|
10
|
+
* e.g. ["app.maintenance_mode", "feature.auth_provider"]
|
|
11
|
+
*/
|
|
12
|
+
prefetch?: string[];
|
|
13
|
+
/**
|
|
14
|
+
* Default/fallback values returned instantly before any tier resolves.
|
|
15
|
+
* get() resolves immediately when a key has a default here.
|
|
9
16
|
*/
|
|
10
17
|
defaults?: Record<string, unknown>;
|
|
11
|
-
/** User context for targeting evaluation. Use autoContext() to auto-detect
|
|
18
|
+
/** User context for targeting evaluation. Use autoContext() to auto-detect. */
|
|
12
19
|
context?: EvaluationContext;
|
|
13
20
|
/**
|
|
14
|
-
* Your self-hosted API URL.
|
|
21
|
+
* Your self-hosted API URL.
|
|
15
22
|
* Default: https://jewel998-config.web.app/api (demo instance)
|
|
16
|
-
*
|
|
17
|
-
* @example "https://your-project.web.app/api"
|
|
18
23
|
*/
|
|
19
24
|
baseUrl?: string;
|
|
25
|
+
/** Cache storage adapter. Default: memoryStorage() */
|
|
26
|
+
storage?: CacheStorage;
|
|
20
27
|
/**
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* - `memoryStorage()` (default) — In-memory, lost on page reload
|
|
24
|
-
* - `browserStorage()` — localStorage, persists across page reloads and sessions
|
|
25
|
-
*
|
|
26
|
-
* @example
|
|
27
|
-
* ```ts
|
|
28
|
-
* import { browserStorage } from "@jewel998/config";
|
|
29
|
-
* initConfig({ storage: browserStorage({ prefix: "myapp" }) })
|
|
30
|
-
* ```
|
|
28
|
+
* Version polling interval in milliseconds.
|
|
29
|
+
* Default: 300_000 (5 minutes). Set to 0 to disable.
|
|
31
30
|
*/
|
|
32
|
-
|
|
31
|
+
pollInterval?: number;
|
|
32
|
+
/**
|
|
33
|
+
* Global timeout in milliseconds for get() calls with no default.
|
|
34
|
+
* If a key does not arrive within this window, get() rejects with
|
|
35
|
+
* SdkError { type: "TIMEOUT" } and onError is called.
|
|
36
|
+
* Default: 30_000 (30 seconds).
|
|
37
|
+
*/
|
|
38
|
+
timeout?: number;
|
|
33
39
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* and only re-fetches full data when the version changes.
|
|
40
|
+
* Global error handler. Called on every SDK-level error:
|
|
41
|
+
* timeouts, fetch failures, auth errors, key-not-found, rate limits.
|
|
37
42
|
*
|
|
38
|
-
*
|
|
43
|
+
* get() also rejects with the same error — onError is for centralised
|
|
44
|
+
* logging/monitoring, not a replacement for catch().
|
|
39
45
|
*/
|
|
40
|
-
|
|
46
|
+
onError?: (error: SdkError) => void;
|
|
41
47
|
}
|
|
42
48
|
export interface Flags {
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Resolves when Tier 1 (init prefetch) keys have been fetched.
|
|
51
|
+
* Resolves instantly if no prefetch keys were declared.
|
|
52
|
+
*/
|
|
53
|
+
ready(): Promise<void>;
|
|
54
|
+
/**
|
|
55
|
+
* Tier 2 fetch hint — fire-and-forget.
|
|
56
|
+
* Keys already tracked in Tier 1 or already fetched are skipped.
|
|
57
|
+
* Emits "updated" and "updated:key" events per key on completion.
|
|
58
|
+
*/
|
|
59
|
+
prefetch(keys: string[]): void;
|
|
60
|
+
/**
|
|
61
|
+
* Get a typed config value asynchronously.
|
|
62
|
+
*
|
|
63
|
+
* Resolution order:
|
|
64
|
+
* 1. Memory / cache hit → resolves instantly
|
|
65
|
+
* 2. Default in defaults map → resolves instantly
|
|
66
|
+
* 3. Inline defaultValue arg → resolves instantly
|
|
67
|
+
* 4. No default → suspends until idle fetch or refresh
|
|
68
|
+
* delivers the key
|
|
69
|
+
*
|
|
70
|
+
* On timeout (global, default 30s):
|
|
71
|
+
* - onError is called with SdkError { type: "TIMEOUT", key }
|
|
72
|
+
* - Promise rejects with the same SdkError
|
|
73
|
+
*/
|
|
74
|
+
get<T = unknown>(key: string): Promise<T>;
|
|
75
|
+
get<T = unknown>(key: string, defaultValue: T): Promise<T>;
|
|
76
|
+
/**
|
|
77
|
+
* Returns a snapshot of all currently fetched values merged with defaults.
|
|
78
|
+
* Does not wait for any in-flight fetches — returns current state only.
|
|
79
|
+
*/
|
|
80
|
+
all(): Promise<Record<string, unknown>>;
|
|
81
|
+
/**
|
|
82
|
+
* Update user context. Fire-and-forget, debounced 100ms.
|
|
83
|
+
* Re-fetches all already-fetched keys in tier order with the new context.
|
|
84
|
+
* Subscribe to "updated" events to react when the re-fetch completes.
|
|
85
|
+
*/
|
|
50
86
|
setContext(context: EvaluationContext): void;
|
|
51
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* Re-fetch all already-fetched keys in tier order.
|
|
89
|
+
* Resolves when all re-fetches complete.
|
|
90
|
+
*/
|
|
52
91
|
refresh(): Promise<void>;
|
|
53
|
-
/**
|
|
54
|
-
|
|
92
|
+
/**
|
|
93
|
+
* Subscribe to events.
|
|
94
|
+
*
|
|
95
|
+
* Key-specific: flags.on("updated:feature.dark_mode", (value) => ...)
|
|
96
|
+
* Batch updated: flags.on("updated", ({ keys, source }) => ...)
|
|
97
|
+
* Fetch errors: flags.on("fetchError", ({ error }) => ...)
|
|
98
|
+
*/
|
|
99
|
+
on(event: string, cb: (payload: unknown) => void): void;
|
|
55
100
|
/** Unsubscribe from events. */
|
|
56
|
-
off
|
|
101
|
+
off(event: string, cb: (payload: unknown) => void): void;
|
|
57
102
|
}
|
|
58
|
-
/**
|
|
59
|
-
* Initialize the feature flags SDK.
|
|
60
|
-
*
|
|
61
|
-
* Returns a Flags instance that immediately serves default values,
|
|
62
|
-
* then resolves to real values from the API in the background.
|
|
63
|
-
*
|
|
64
|
-
* @example
|
|
65
|
-
* ```ts
|
|
66
|
-
* const flags = initConfig({
|
|
67
|
-
* clientId: "cid_xxx",
|
|
68
|
-
* defaults: { "feature.dark_mode": false },
|
|
69
|
-
* context: autoContext({ userId: "user_123", plan: "pro" }),
|
|
70
|
-
* });
|
|
71
|
-
*
|
|
72
|
-
* flags.get("feature.dark_mode"); // → false (instant)
|
|
73
|
-
* flags.on("updated", () => {
|
|
74
|
-
* flags.get("feature.dark_mode"); // → true (from API)
|
|
75
|
-
* });
|
|
76
|
-
* ```
|
|
77
|
-
*/
|
|
78
103
|
export declare function initConfig(options: InitConfigOptions): Flags;
|
package/dist/initConfig.js
CHANGED
|
@@ -1,129 +1,302 @@
|
|
|
1
1
|
// ═══════════════════════════════════════════════════════════════
|
|
2
2
|
// initConfig — Primary SDK entry point
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
// const flags = initConfig({
|
|
6
|
-
// clientId: "cid_xxx",
|
|
7
|
-
// defaults: { "feature.dark_mode": false, "app.upload_limit": 50 },
|
|
8
|
-
// context: autoContext({ userId: "user_123", plan: "pro" }),
|
|
9
|
-
// });
|
|
4
|
+
// Three-tier priority fetching model:
|
|
10
5
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
6
|
+
// Tier 1 — PREFETCH (init)
|
|
7
|
+
// Declared via `prefetch: [...]` at initConfig time.
|
|
8
|
+
// Fetched immediately. ready() blocks until this completes.
|
|
9
|
+
//
|
|
10
|
+
// Tier 2 — PAGE (runtime)
|
|
11
|
+
// Declared via flags.prefetch(keys) per route/component.
|
|
12
|
+
// Fire-and-forget. Keys deduplicated against Tier 1.
|
|
13
|
+
//
|
|
14
|
+
// Tier 3 — IDLE
|
|
15
|
+
// Full fetchAll() during browser idle time via requestIdleCallback.
|
|
16
|
+
// Fills in all remaining keys in the background.
|
|
17
|
+
//
|
|
18
|
+
// get() behaviour:
|
|
19
|
+
// - Memory/cache hit → resolves instantly
|
|
20
|
+
// - Default provided → resolves instantly
|
|
21
|
+
// - No default → suspends until idle fetch or refresh delivers
|
|
22
|
+
// the key, or global timeout fires
|
|
23
|
+
// - Timeout → onError called, Promise rejects with SdkError
|
|
24
|
+
//
|
|
25
|
+
// Context update:
|
|
26
|
+
// setContext() is fire-and-forget (debounced 100ms).
|
|
27
|
+
// Re-fetches only already-fetched keys in tier order.
|
|
14
28
|
// ═══════════════════════════════════════════════════════════════
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
* });
|
|
29
|
-
*
|
|
30
|
-
* flags.get("feature.dark_mode"); // → false (instant)
|
|
31
|
-
* flags.on("updated", () => {
|
|
32
|
-
* flags.get("feature.dark_mode"); // → true (from API)
|
|
33
|
-
* });
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
29
|
+
import { memoryStorage } from "./cache/memoryStorage.js";
|
|
30
|
+
import { SdkError, ConfigError } from "./errors/index.js";
|
|
31
|
+
import { TypedEventEmitter } from "./events/EventEmitter.js";
|
|
32
|
+
import { createProjectedFetcher } from "./fetch/projectedFetcher.js";
|
|
33
|
+
import { TierFetcher } from "./fetch/TierFetcher.js";
|
|
34
|
+
import { withRetry } from "./retry/RetryEngine.js";
|
|
35
|
+
import { createHttpTransport } from "./transport/HttpTransport.js";
|
|
36
|
+
import { DEFAULT_CACHE_TTL, DEFAULT_RETRY } from "./types.js";
|
|
37
|
+
const DEFAULT_BASE_URL = "https://jewel998-config.web.app/api";
|
|
38
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
39
|
+
const CONTEXT_DEBOUNCE_MS = 100;
|
|
40
|
+
const MIN_REFETCH_INTERVAL_MS = 30_000;
|
|
41
|
+
// ─── Implementation ────────────────────────────────────────────
|
|
36
42
|
export function initConfig(options) {
|
|
43
|
+
if (!options.clientId) {
|
|
44
|
+
throw new ConfigError("clientId is required", "MISSING_CLIENT_ID");
|
|
45
|
+
}
|
|
46
|
+
if (typeof window === "undefined" && !options.clientId.startsWith("svr_")) {
|
|
47
|
+
throw new ConfigError("Client keys (cid_) require a browser environment.", "INITIALIZATION_FAILED");
|
|
48
|
+
}
|
|
37
49
|
const defaults = options.defaults ?? {};
|
|
38
|
-
const pollInterval = options.pollInterval ?? 300_000;
|
|
39
|
-
const baseUrl = options.baseUrl ??
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
50
|
+
const pollInterval = options.pollInterval ?? 300_000;
|
|
51
|
+
const baseUrl = options.baseUrl ?? DEFAULT_BASE_URL;
|
|
52
|
+
const storage = options.storage ?? memoryStorage();
|
|
53
|
+
const retry = { ...DEFAULT_RETRY };
|
|
54
|
+
const timeoutMs = options.timeout ?? DEFAULT_TIMEOUT_MS;
|
|
55
|
+
const onError = options.onError;
|
|
56
|
+
const initPrefetch = options.prefetch ?? [];
|
|
57
|
+
const events = new TypedEventEmitter();
|
|
58
|
+
let currentContext = options.context ?? {};
|
|
59
|
+
// ── Transport + fetcher ──────────────────────────────────────
|
|
60
|
+
const transport = createHttpTransport({
|
|
46
61
|
baseUrl,
|
|
62
|
+
clientId: options.clientId,
|
|
63
|
+
evaluationMode: "server",
|
|
64
|
+
getContext: () => currentContext,
|
|
65
|
+
});
|
|
66
|
+
const baseFetcher = createProjectedFetcher(transport);
|
|
67
|
+
const tierFetcher = new TierFetcher(baseFetcher);
|
|
68
|
+
// ── In-memory data store ─────────────────────────────────────
|
|
69
|
+
const data = {};
|
|
70
|
+
function applyResult(result) {
|
|
71
|
+
const keys = Object.keys(result);
|
|
72
|
+
for (const key of keys) {
|
|
73
|
+
data[key] = result[key];
|
|
74
|
+
storage.set(key, result[key], DEFAULT_CACHE_TTL);
|
|
75
|
+
}
|
|
76
|
+
// Update the __all__ snapshot incrementally
|
|
77
|
+
const existing = storage.get("__all__") ?? {};
|
|
78
|
+
storage.set("__all__", { ...existing, ...result }, DEFAULT_CACHE_TTL);
|
|
79
|
+
return keys;
|
|
80
|
+
}
|
|
81
|
+
function emitUpdated(keys, source) {
|
|
82
|
+
if (keys.length === 0)
|
|
83
|
+
return;
|
|
84
|
+
events.emit("updated", { keys, source });
|
|
85
|
+
}
|
|
86
|
+
// ── Tier 1: init prefetch ────────────────────────────────────
|
|
87
|
+
const readyPromise = initPrefetch.length > 0
|
|
88
|
+
? (async () => {
|
|
89
|
+
try {
|
|
90
|
+
const result = await withRetry(() => tierFetcher.fetchTier(initPrefetch, "prefetch"), retry);
|
|
91
|
+
const fetched = applyResult(result);
|
|
92
|
+
tierFetcher.notifyWaiters(result);
|
|
93
|
+
emitUpdated(fetched, "prefetch");
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
const sdkErr = new SdkError("FETCH_FAILED", `Prefetch failed: ${err.message}`, undefined, err);
|
|
97
|
+
onError?.(sdkErr);
|
|
98
|
+
events.emit("fetchError", {
|
|
99
|
+
error: sdkErr,
|
|
100
|
+
retryCount: retry.maxRetries,
|
|
101
|
+
willRetry: false,
|
|
102
|
+
});
|
|
103
|
+
// Don't re-throw — app still runs on defaults
|
|
104
|
+
}
|
|
105
|
+
})()
|
|
106
|
+
: Promise.resolve();
|
|
107
|
+
// ── Tier 3: idle fetch ───────────────────────────────────────
|
|
108
|
+
readyPromise.then(() => {
|
|
109
|
+
scheduleIdleFetch(async () => {
|
|
110
|
+
try {
|
|
111
|
+
const result = await tierFetcher.fetchAll();
|
|
112
|
+
const fetched = applyResult(result);
|
|
113
|
+
tierFetcher.notifyWaiters(result);
|
|
114
|
+
emitUpdated(fetched, "idle");
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Idle fetch is best-effort — silently ignore
|
|
118
|
+
// Pending waiters will eventually time out via get()
|
|
119
|
+
}
|
|
120
|
+
});
|
|
47
121
|
});
|
|
48
|
-
// Version
|
|
122
|
+
// ── Version polling ──────────────────────────────────────────
|
|
49
123
|
let cachedVersion = null;
|
|
50
|
-
let pollTimer = null;
|
|
51
124
|
async function checkVersion() {
|
|
52
125
|
try {
|
|
53
|
-
const res = await fetch(`${baseUrl}/
|
|
54
|
-
headers: cachedVersion
|
|
55
|
-
? { "If-None-Match": `"${cachedVersion}"` }
|
|
56
|
-
: {},
|
|
126
|
+
const res = await fetch(`${baseUrl}/v1/version?clientId=${options.clientId}`, {
|
|
127
|
+
headers: cachedVersion ? { "If-None-Match": `"${cachedVersion}"` } : {},
|
|
57
128
|
});
|
|
58
|
-
// 304 = version unchanged, no action needed
|
|
59
129
|
if (res.status === 304)
|
|
60
130
|
return;
|
|
61
131
|
if (!res.ok)
|
|
62
132
|
return;
|
|
63
|
-
const { version
|
|
64
|
-
// First check — just store version, no refetch (initial fetch already happened)
|
|
133
|
+
const { version } = (await res.json());
|
|
65
134
|
if (cachedVersion === null) {
|
|
66
135
|
cachedVersion = version;
|
|
67
136
|
return;
|
|
68
137
|
}
|
|
69
|
-
// Version changed — need to refresh
|
|
70
138
|
if (version !== cachedVersion) {
|
|
71
139
|
cachedVersion = version;
|
|
72
|
-
|
|
73
|
-
// For now, full refresh (still efficient with CDN + deduplication)
|
|
74
|
-
await client.refresh();
|
|
140
|
+
await doRefresh();
|
|
75
141
|
}
|
|
76
142
|
}
|
|
77
143
|
catch {
|
|
78
|
-
// Silently ignore polling errors
|
|
144
|
+
// Silently ignore polling errors
|
|
79
145
|
}
|
|
80
146
|
}
|
|
81
|
-
// Start polling if interval > 0
|
|
82
147
|
if (pollInterval > 0 && typeof window !== "undefined") {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
void checkVersion();
|
|
86
|
-
});
|
|
87
|
-
pollTimer = setInterval(checkVersion, pollInterval);
|
|
88
|
-
// Also check on tab visibility change (user returns to tab)
|
|
148
|
+
readyPromise.then(() => void checkVersion());
|
|
149
|
+
setInterval(checkVersion, pollInterval);
|
|
89
150
|
document.addEventListener("visibilitychange", () => {
|
|
90
|
-
if (document.visibilityState === "visible")
|
|
151
|
+
if (document.visibilityState === "visible")
|
|
91
152
|
void checkVersion();
|
|
92
|
-
}
|
|
93
153
|
});
|
|
94
154
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
155
|
+
// ── Refresh (re-fetch fetched keys in tier order) ─────────────
|
|
156
|
+
let lastRefetchTime = 0;
|
|
157
|
+
async function doRefresh() {
|
|
158
|
+
if (Date.now() - lastRefetchTime < MIN_REFETCH_INTERVAL_MS)
|
|
159
|
+
return;
|
|
160
|
+
lastRefetchTime = Date.now();
|
|
161
|
+
const { prefetch: pf, page, idle } = tierFetcher.getFetchedKeys();
|
|
162
|
+
const allFetched = [];
|
|
163
|
+
const refetches = [];
|
|
164
|
+
if (pf.length > 0) {
|
|
165
|
+
refetches.push(withRetry(() => tierFetcher.refetchKeys(pf), retry)
|
|
166
|
+
.then((r) => {
|
|
167
|
+
applyResult(r);
|
|
168
|
+
tierFetcher.notifyWaiters(r);
|
|
169
|
+
allFetched.push(...Object.keys(r));
|
|
170
|
+
})
|
|
171
|
+
.catch(() => { }));
|
|
172
|
+
}
|
|
173
|
+
if (page.length > 0) {
|
|
174
|
+
refetches.push(withRetry(() => tierFetcher.refetchKeys(page), retry)
|
|
175
|
+
.then((r) => {
|
|
176
|
+
applyResult(r);
|
|
177
|
+
tierFetcher.notifyWaiters(r);
|
|
178
|
+
allFetched.push(...Object.keys(r));
|
|
179
|
+
})
|
|
180
|
+
.catch(() => { }));
|
|
181
|
+
}
|
|
182
|
+
if (idle.length > 0) {
|
|
183
|
+
refetches.push(tierFetcher
|
|
184
|
+
.fetchAll()
|
|
185
|
+
.then((r) => {
|
|
186
|
+
applyResult(r);
|
|
187
|
+
tierFetcher.notifyWaiters(r);
|
|
188
|
+
allFetched.push(...Object.keys(r));
|
|
189
|
+
})
|
|
190
|
+
.catch(() => { }));
|
|
191
|
+
}
|
|
192
|
+
await Promise.allSettled(refetches);
|
|
193
|
+
emitUpdated(allFetched, "refresh");
|
|
194
|
+
}
|
|
195
|
+
// ── Context re-fetch ─────────────────────────────────────────
|
|
196
|
+
let contextDebounce = null;
|
|
197
|
+
// ── Public API ───────────────────────────────────────────────
|
|
198
|
+
const flags = {
|
|
199
|
+
ready() {
|
|
200
|
+
return readyPromise;
|
|
101
201
|
},
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
202
|
+
prefetch(keys) {
|
|
203
|
+
void (async () => {
|
|
204
|
+
try {
|
|
205
|
+
const result = await withRetry(() => tierFetcher.fetchTier(keys, "page"), retry);
|
|
206
|
+
const fetched = applyResult(result);
|
|
207
|
+
tierFetcher.notifyWaiters(result);
|
|
208
|
+
emitUpdated(fetched, "page");
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
const sdkErr = new SdkError("FETCH_FAILED", `Page prefetch failed: ${err.message}`, undefined, err);
|
|
212
|
+
onError?.(sdkErr);
|
|
213
|
+
events.emit("fetchError", {
|
|
214
|
+
error: sdkErr,
|
|
215
|
+
retryCount: retry.maxRetries,
|
|
216
|
+
willRetry: false,
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
})();
|
|
220
|
+
},
|
|
221
|
+
get(key, defaultValue) {
|
|
222
|
+
// 1. Memory hit
|
|
223
|
+
if (key in data)
|
|
224
|
+
return Promise.resolve(data[key]);
|
|
225
|
+
// 2. Cache hit
|
|
226
|
+
const cached = storage.get(key);
|
|
227
|
+
if (cached !== undefined) {
|
|
228
|
+
data[key] = cached;
|
|
229
|
+
return Promise.resolve(cached);
|
|
230
|
+
}
|
|
231
|
+
// 3. Default from map
|
|
232
|
+
if (key in defaults)
|
|
233
|
+
return Promise.resolve(defaults[key]);
|
|
234
|
+
// 4. Inline default
|
|
235
|
+
if (defaultValue !== undefined)
|
|
236
|
+
return Promise.resolve(defaultValue);
|
|
237
|
+
// 5. Suspend — wait for idle fetch or refresh to deliver this key
|
|
238
|
+
return new Promise((resolve, reject) => {
|
|
239
|
+
const timer = setTimeout(() => {
|
|
240
|
+
const err = new SdkError("TIMEOUT", `get("${key}") timed out after ${timeoutMs}ms — key was not delivered by idle fetch or refresh`, key);
|
|
241
|
+
onError?.(err);
|
|
242
|
+
reject(err);
|
|
243
|
+
}, timeoutMs);
|
|
244
|
+
tierFetcher.waitForKey(key).then((value) => {
|
|
245
|
+
clearTimeout(timer);
|
|
246
|
+
resolve(value);
|
|
247
|
+
}, (err) => {
|
|
248
|
+
clearTimeout(timer);
|
|
249
|
+
const sdkErr = new SdkError("FETCH_FAILED", err.message, key, err);
|
|
250
|
+
onError?.(sdkErr);
|
|
251
|
+
reject(sdkErr);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
110
254
|
},
|
|
111
|
-
all() {
|
|
112
|
-
const
|
|
113
|
-
return { ...defaults, ...
|
|
255
|
+
async all() {
|
|
256
|
+
const snapshot = storage.get("__all__") ?? {};
|
|
257
|
+
return { ...defaults, ...snapshot, ...data };
|
|
114
258
|
},
|
|
115
259
|
setContext(context) {
|
|
116
|
-
|
|
260
|
+
currentContext = { ...context };
|
|
261
|
+
if (contextDebounce)
|
|
262
|
+
clearTimeout(contextDebounce);
|
|
263
|
+
contextDebounce = setTimeout(() => {
|
|
264
|
+
contextDebounce = null;
|
|
265
|
+
void doRefresh();
|
|
266
|
+
}, CONTEXT_DEBOUNCE_MS);
|
|
117
267
|
},
|
|
118
|
-
refresh() {
|
|
119
|
-
|
|
268
|
+
async refresh() {
|
|
269
|
+
await doRefresh();
|
|
120
270
|
},
|
|
121
271
|
on(event, cb) {
|
|
122
|
-
|
|
272
|
+
// Key-specific: "updated:feature.dark_mode"
|
|
273
|
+
if (event.startsWith("updated:")) {
|
|
274
|
+
const key = event.slice("updated:".length);
|
|
275
|
+
events.onKey(key, cb);
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
events.on(event, cb);
|
|
123
279
|
},
|
|
124
280
|
off(event, cb) {
|
|
125
|
-
|
|
281
|
+
if (event.startsWith("updated:")) {
|
|
282
|
+
const key = event.slice("updated:".length);
|
|
283
|
+
events.offKey(key, cb);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
events.off(event, cb);
|
|
126
287
|
},
|
|
127
288
|
};
|
|
289
|
+
return flags;
|
|
290
|
+
}
|
|
291
|
+
// ─── Idle scheduling ──────────────────────────────────────────
|
|
292
|
+
function scheduleIdleFetch(fn) {
|
|
293
|
+
if (typeof window === "undefined")
|
|
294
|
+
return;
|
|
295
|
+
if ("requestIdleCallback" in window) {
|
|
296
|
+
window.requestIdleCallback(fn, { timeout: 2000 });
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
setTimeout(fn, 200);
|
|
300
|
+
}
|
|
128
301
|
}
|
|
129
302
|
//# sourceMappingURL=initConfig.js.map
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
export { archivedPlugin } from "./archivedPlugin.js";
|
|
2
|
-
export { validateStateTransition, VALID_TRANSITIONS
|
|
2
|
+
export { validateStateTransition, VALID_TRANSITIONS } from "./stateTransitions.js";
|
|
3
3
|
export type { LifecycleState } from "./stateTransitions.js";
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
// Lifecycle Plugin — Public Barrel Export
|
|
3
3
|
// ═══════════════════════════════════════════════════════════════
|
|
4
4
|
export { archivedPlugin } from "./archivedPlugin.js";
|
|
5
|
-
export { validateStateTransition, VALID_TRANSITIONS
|
|
5
|
+
export { validateStateTransition, VALID_TRANSITIONS } from "./stateTransitions.js";
|
|
6
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -16,11 +16,7 @@ export function murmurhash3_32(key, seed = 0) {
|
|
|
16
16
|
const c2 = 0x1b873593;
|
|
17
17
|
let i = 0;
|
|
18
18
|
while (i + 4 <= len) {
|
|
19
|
-
let k1 = (bytes[i] |
|
|
20
|
-
(bytes[i + 1] << 8) |
|
|
21
|
-
(bytes[i + 2] << 16) |
|
|
22
|
-
(bytes[i + 3] << 24)) >>>
|
|
23
|
-
0;
|
|
19
|
+
let k1 = (bytes[i] | (bytes[i + 1] << 8) | (bytes[i + 2] << 16) | (bytes[i + 3] << 24)) >>> 0;
|
|
24
20
|
k1 = Math.imul(k1, c1) >>> 0;
|
|
25
21
|
k1 = ((k1 << 15) | (k1 >>> 17)) >>> 0;
|
|
26
22
|
k1 = Math.imul(k1, c2) >>> 0;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AuthenticationError, ConfigError } from "../errors/index.js";
|
|
1
|
+
import { AuthenticationError, ConfigError, RateLimitError } from "../errors/index.js";
|
|
2
2
|
import { DEFAULT_RETRY } from "../types.js";
|
|
3
3
|
export const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
4
4
|
export const isNonRetryableError = (error) => {
|
|
@@ -20,6 +20,20 @@ export const isNonRetryableError = (error) => {
|
|
|
20
20
|
}
|
|
21
21
|
return false;
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Get the retry delay for an error. If the error is a RateLimitError with
|
|
25
|
+
* a server-specified Retry-After value, use that instead of calculated backoff.
|
|
26
|
+
*/
|
|
27
|
+
const getRetryDelay = (error, attempt, config) => {
|
|
28
|
+
// Respect server-specified Retry-After for rate limit errors
|
|
29
|
+
if (error instanceof RateLimitError && error.retryAfterSeconds != null) {
|
|
30
|
+
return error.retryAfterSeconds * 1000;
|
|
31
|
+
}
|
|
32
|
+
// Default: exponential backoff with jitter
|
|
33
|
+
const delay = Math.min(config.baseDelay * Math.pow(config.multiplier, attempt), config.maxDelay);
|
|
34
|
+
// Add jitter (±25%) to prevent thundering herd
|
|
35
|
+
return delay * (0.75 + Math.random() * 0.5);
|
|
36
|
+
};
|
|
23
37
|
export const withRetry = async (fn, options, onRetry) => {
|
|
24
38
|
const config = { ...DEFAULT_RETRY, ...options };
|
|
25
39
|
let lastError;
|
|
@@ -33,11 +47,9 @@ export const withRetry = async (fn, options, onRetry) => {
|
|
|
33
47
|
throw error;
|
|
34
48
|
}
|
|
35
49
|
if (attempt < config.maxRetries) {
|
|
36
|
-
const delay =
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
onRetry?.(attempt + 1, lastError, jitter);
|
|
40
|
-
await sleep(jitter);
|
|
50
|
+
const delay = getRetryDelay(lastError, attempt, config);
|
|
51
|
+
onRetry?.(attempt + 1, lastError, delay);
|
|
52
|
+
await sleep(delay);
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
55
|
}
|