@toggld/js 0.0.0-stage → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,127 @@
1
+ import { n as mapMaybe, t as makeCore } from "./core-BQfyNOoT.mjs";
2
+ // src/index.ts
3
+ /**
4
+ * `@toggld/js` — the default entry: a plain Promise API with **zero runtime
5
+ * dependencies**.
6
+ *
7
+ * ```ts
8
+ * import { ToggldClient } from "@toggld/js"
9
+ *
10
+ * // Sync mode, the default: a copy of the flag rules stays current in the
11
+ * // background and every evaluation is in-process — microseconds, zero server requests.
12
+ * // `create` waits for the first snapshot (up to `sync.readyTimeoutMs`).
13
+ * const client = await ToggldClient.create({ sdkKey, endpoint })
14
+ * const on = await client.getBooleanValue("new-checkout", false, { targetingKey: "user-123" })
15
+ * await client.close() // stops the sync, so a script can exit
16
+ * ```
17
+ *
18
+ * Pass `mode: "file"` to evaluate from a flag file on disk (`file`, default
19
+ * `./toggld.flags.json`), or `mode: "remote"` to send each evaluation to the
20
+ * flag API over OFREP — for public tokens. On serverless and edge runtimes
21
+ * (Cloudflare Workers, Vercel, AWS Lambda), use `@toggld/js/serverless`, the
22
+ * remote client that sends one bulk request per incoming request.
23
+ * Evaluation never throws: any failure resolves to the supplied default value,
24
+ * with the reason/error available via the `*Details` methods.
25
+ *
26
+ * For an Effect app, `@toggld/js/effect` exposes the same client as a
27
+ * service and `Layer` (it needs the optional `effect` peer dependency).
28
+ */
29
+ /** Calls after `close()` reject, as they always have: the client is unusable then. */
30
+ const closedError = () => /* @__PURE__ */ new Error("[toggld] The client is closed.");
31
+ /**
32
+ * A Toggld client. Create one with {@link ToggldClient.create} and {@link
33
+ * ToggldClient.close} it on shutdown to stop background sync.
34
+ */
35
+ var ToggldClient = class ToggldClient {
36
+ #core;
37
+ constructor(core) {
38
+ this.#core = core;
39
+ }
40
+ /**
41
+ * Create a client. In sync mode (the default) it resolves once the first
42
+ * flag snapshot has synced, or once `sync.readyTimeoutMs` (default 5 s) has
43
+ * elapsed. In that case it warns once, and the client serves caller defaults
44
+ * with `PROVIDER_NOT_READY` while sync keeps retrying in the background. In
45
+ * file mode it resolves after the first read of the file; in remote mode, at
46
+ * once.
47
+ *
48
+ * Rejects with a `TypeError` when a sync-mode client could never sync: no
49
+ * `sdkKey`, a public token (`ffp_…`), or no valid `endpoint`. Use a secret
50
+ * SDK key, or pass `mode: "remote"`. Also rejects options that no longer
51
+ * apply: `mode: "local"` (use `"sync"` or `"file"`), `sync.file` (use
52
+ * `mode: "file", file`), and `file` without `mode: "file"`.
53
+ */
54
+ static async create(options) {
55
+ const core = makeCore(options);
56
+ await core.waitForReady();
57
+ return new ToggldClient(core);
58
+ }
59
+ /**
60
+ * Resolves once the client has flag data to evaluate with (immediately in
61
+ * remote mode). Useful if `create` resolved on the ready timeout and you
62
+ * want to await sync explicitly. It has no timeout of its own, and it never
63
+ * resolves while the key or endpoint is wrong. Rejects if the client is
64
+ * closed first.
65
+ */
66
+ ready() {
67
+ if (this.#core.closed()) return Promise.reject(closedError());
68
+ return this.#core.ready();
69
+ }
70
+ /** Send buffered counts without failing evaluations. Failed windows remain queued. */
71
+ flushTelemetry() {
72
+ if (this.#core.closed()) return Promise.reject(closedError());
73
+ return this.#core.flushTelemetry();
74
+ }
75
+ /** Resolve a flag, returning full details with an untyped (JSON) value. */
76
+ evaluate(key, defaultValue, context) {
77
+ if (this.#core.closed()) return Promise.reject(closedError());
78
+ return Promise.resolve(this.#core.evaluate(key, defaultValue, context));
79
+ }
80
+ /** Resolve every flag for the environment (one request in remote mode; in-process in sync and file modes). */
81
+ evaluateAll(context) {
82
+ if (this.#core.closed()) return Promise.reject(closedError());
83
+ return Promise.resolve(this.#core.evaluateAll(context));
84
+ }
85
+ getBooleanValue(key, defaultValue, context) {
86
+ return this.#value(key, defaultValue, context, "boolean");
87
+ }
88
+ getBooleanDetails(key, defaultValue, context) {
89
+ return this.#details(key, defaultValue, context, "boolean");
90
+ }
91
+ getStringValue(key, defaultValue, context) {
92
+ return this.#value(key, defaultValue, context, "string");
93
+ }
94
+ getStringDetails(key, defaultValue, context) {
95
+ return this.#details(key, defaultValue, context, "string");
96
+ }
97
+ getNumberValue(key, defaultValue, context) {
98
+ return this.#value(key, defaultValue, context, "number");
99
+ }
100
+ getNumberDetails(key, defaultValue, context) {
101
+ return this.#details(key, defaultValue, context, "number");
102
+ }
103
+ getObjectValue(key, defaultValue, context) {
104
+ return this.#value(key, defaultValue, context, "object");
105
+ }
106
+ getObjectDetails(key, defaultValue, context) {
107
+ return this.#details(key, defaultValue, context, "object");
108
+ }
109
+ /**
110
+ * Stop background sync, close the stream, and send remaining telemetry
111
+ * (bounded by `timeoutMs`). The client is unusable afterwards. A sync-mode
112
+ * client's stream and poll (and a file-mode client's watcher) keep the process alive until you call this, so a
113
+ * script must close its client to exit.
114
+ */
115
+ close() {
116
+ return this.#core.close();
117
+ }
118
+ #details(key, defaultValue, context, type) {
119
+ if (this.#core.closed()) return Promise.reject(closedError());
120
+ return Promise.resolve(this.#core.typed(key, defaultValue, context, type));
121
+ }
122
+ #value(key, defaultValue, context, type) {
123
+ if (this.#core.closed()) return Promise.reject(closedError());
124
+ return Promise.resolve(mapMaybe(this.#core.typed(key, defaultValue, context, type), (d) => d.value));
125
+ }
126
+ };
127
+ export { ToggldClient };
@@ -0,0 +1,259 @@
1
+ import { c as isRecord, d as discard, f as makeSend, h as withTimeout, l as apiUrl, n as decodeOfrepBulk, p as readJson, r as decodeOfrepBulkFailure, u as describe } from "./wire-CrkBeqnM.mjs";
2
+ import { _ as wireErrorDetails, g as rejectedMessage, h as okDetails, i as syncCredentialProblem, m as codeFor, n as errorDetails, r as invalidEndpointMessage, t as enforceType, y as makeRequestTelemetry } from "./details-AiMdctXB.mjs";
3
+ // src/internal/cache.ts
4
+ const CACHE_KEY_PREFIX = "toggld:v1:";
5
+ /** JSON with object keys sorted at every level, so equal contexts give equal strings. */
6
+ const canonicalJson = (u) => JSON.stringify(sorted(u)) ?? "null";
7
+ const sorted = (u) => {
8
+ if (Array.isArray(u)) return u.map(sorted);
9
+ if (!isRecord(u)) return u;
10
+ const proto = Object.getPrototypeOf(u);
11
+ if (proto !== null && proto !== Object.prototype) return u;
12
+ const out = {};
13
+ for (const key of Object.keys(u).sort()) out[key] = sorted(u[key]);
14
+ return out;
15
+ };
16
+ const hex = (bytes) => Array.from(new Uint8Array(bytes), (b) => b.toString(16).padStart(2, "0")).join("");
17
+ /** SHA-256 (WebCrypto, available on Workers, Node.js 20+, Deno, and Bun) as lowercase hex. */
18
+ const sha256Hex = async (text) => hex(await crypto.subtle.digest("SHA-256", new TextEncoder().encode(text)));
19
+ /** The cache key of one bulk result. `keyHash` is {@link sha256Hex} of the SDK key, never the key. */
20
+ const cacheKeyFor = async (endpoint, keyHash, canonicalContext) => CACHE_KEY_PREFIX + await sha256Hex(JSON.stringify([
21
+ endpoint,
22
+ keyHash,
23
+ canonicalContext
24
+ ]));
25
+ /** A cached value, checked like a bulk response, or `undefined` if it does not have the shape. */
26
+ const decodeCached = (u) => {
27
+ if (!isRecord(u) || typeof u.storedAt !== "number" || !Number.isFinite(u.storedAt)) return void 0;
28
+ try {
29
+ return decodeOfrepBulk({ flags: u.flags });
30
+ } catch {
31
+ return;
32
+ }
33
+ };
34
+ /** A bulk entry as it is cached: the OFREP entry, without the decoder's `ok` tag. */
35
+ const toCached = (entry) => {
36
+ const { ok: _ok, ...rest } = entry;
37
+ return rest;
38
+ };
39
+ const deepFreeze = (u) => {
40
+ if (typeof u === "object" && u !== null && !Object.isFrozen(u)) {
41
+ Object.freeze(u);
42
+ for (const value of Object.values(u)) deepFreeze(value);
43
+ }
44
+ return u;
45
+ };
46
+ /**
47
+ * An {@link EvaluationCache} in the memory of this isolate or process: a
48
+ * bounded least-recently-used map, with entries that expire `ttlMs` after they
49
+ * are stored. Expiry is checked on `get`; it starts no timer.
50
+ *
51
+ * It holds only plain data (a frozen copy of each value, never a promise or a
52
+ * `Response`), so on Cloudflare Workers requests can share it safely. Each
53
+ * isolate or process has its own: a new isolate starts empty.
54
+ *
55
+ * It trades freshness for fewer billable requests: for up to `ttlMs`, a
56
+ * context that is already cached does not see a flag change.
57
+ */
58
+ const memoryCache = (options = {}) => {
59
+ const ttlMs = options.ttlMs ?? 3e4;
60
+ const maxEntries = options.maxEntries ?? 1e3;
61
+ if (!(Number.isFinite(ttlMs) && ttlMs > 0)) throw new RangeError("[toggld] `ttlMs` of memoryCache must be a positive, finite number of milliseconds.");
62
+ if (!(Number.isSafeInteger(maxEntries) && maxEntries > 0)) throw new RangeError("[toggld] `maxEntries` of memoryCache must be a positive integer.");
63
+ const entries = /* @__PURE__ */ new Map();
64
+ return {
65
+ get: (key) => {
66
+ const entry = entries.get(key);
67
+ if (entry === void 0) return void 0;
68
+ entries.delete(key);
69
+ if (Date.now() >= entry.expiresAt) return void 0;
70
+ entries.set(key, entry);
71
+ return entry.value;
72
+ },
73
+ set: (key, value) => {
74
+ entries.delete(key);
75
+ entries.set(key, {
76
+ expiresAt: Date.now() + ttlMs,
77
+ value: deepFreeze(structuredClone(value))
78
+ });
79
+ while (entries.size > maxEntries) entries.delete(entries.keys().next().value);
80
+ }
81
+ };
82
+ };
83
+ // src/internal/serverless.ts
84
+ const DEFAULT_SERVERLESS_TIMEOUT_MS = 1500;
85
+ const BULK_PATH = "/ofrep/v1/evaluate/flags";
86
+ const positive = (value, fallback) => value !== void 0 && Number.isFinite(value) && value > 0 ? value : fallback;
87
+ const assertCredentials = (options) => {
88
+ switch (syncCredentialProblem(options)) {
89
+ case "missing-key": throw new TypeError("[toggld] `sdkKey` is required. The serverless client needs a secret SDK key (ffk_…).");
90
+ case "public-token": throw new TypeError("[toggld] `sdkKey` is a public token (ffp_…). The serverless client is a server SDK and needs a secret SDK key (ffk_…): public tokens are for browsers, and their budget is shared per IP address, which a server's traffic would exhaust. In the browser, use @toggld/browser.");
91
+ case "invalid-endpoint": throw new TypeError(invalidEndpointMessage(options.endpoint));
92
+ case void 0: return;
93
+ }
94
+ };
95
+ const bulkOf = (flags) => ({
96
+ ok: true,
97
+ flags,
98
+ byKey: new Map(flags.map((entry) => [entry.key, entry]))
99
+ });
100
+ /** A copy for one caller: results are shared by the reads of a view, so nobody may change them. */
101
+ const own = (d) => ({
102
+ ...d,
103
+ value: typeof d.value === "object" && d.value !== null ? structuredClone(d.value) : d.value,
104
+ flagMetadata: { ...d.flagMetadata }
105
+ });
106
+ const entryDetails = (entry, fallback) => own(entry.ok ? okDetails(entry.key, entry) : wireErrorDetails(fallback, entry));
107
+ const createServerlessCore = (options) => {
108
+ assertCredentials(options);
109
+ const timeoutMs = positive(options.timeoutMs, DEFAULT_SERVERLESS_TIMEOUT_MS);
110
+ const send = makeSend(options);
111
+ const cache = options.cache;
112
+ const cacheEndpoint = apiUrl(options.endpoint, "").toString();
113
+ const telemetry = options.telemetry === false ? void 0 : makeRequestTelemetry(send, {
114
+ ...options.telemetry ? { telemetry: options.telemetry } : {},
115
+ timeoutMs
116
+ });
117
+ const record = (entry) => {
118
+ telemetry?.record(entry.key, entry.ok ? {
119
+ variant: entry.variant,
120
+ reason: entry.reason
121
+ } : {
122
+ variant: void 0,
123
+ reason: "ERROR"
124
+ });
125
+ };
126
+ let keyHash;
127
+ const warned = /* @__PURE__ */ new Set();
128
+ const warnOnce = (kind, message) => {
129
+ if (warned.has(kind)) return;
130
+ warned.add(kind);
131
+ console.warn(`[toggld] ${message} The serverless client sends the bulk request instead.`);
132
+ };
133
+ const mergedContext = (context) => ({
134
+ ...options.defaultContext,
135
+ ...context
136
+ });
137
+ const fetchBulk = async (context, signal) => {
138
+ try {
139
+ return await withTimeout(timeoutMs, async (s) => {
140
+ const response = await send("POST", BULK_PATH, { context }, s);
141
+ const status = response.status;
142
+ if (status >= 200 && status < 300) return bulkOf(decodeOfrepBulk(await readJson(response, `POST ${BULK_PATH}`)));
143
+ if (status >= 400 && status < 500) try {
144
+ const failure = decodeOfrepBulkFailure(await response.json());
145
+ return {
146
+ ok: false,
147
+ errorCode: failure.errorCode,
148
+ message: failure.errorDetails ?? rejectedMessage(response)
149
+ };
150
+ } catch {
151
+ return {
152
+ ok: false,
153
+ errorCode: "GENERAL",
154
+ message: rejectedMessage(response)
155
+ };
156
+ }
157
+ discard(response);
158
+ return {
159
+ ok: false,
160
+ errorCode: "GENERAL",
161
+ message: `Unexpected response status ${status}`
162
+ };
163
+ }, signal);
164
+ } catch (error) {
165
+ return {
166
+ ok: false,
167
+ errorCode: codeFor(error),
168
+ message: describe(error)
169
+ };
170
+ }
171
+ };
172
+ const fromCache = async (cache, key) => {
173
+ let value;
174
+ try {
175
+ value = await withTimeout(timeoutMs, async () => cache.get(key));
176
+ } catch (error) {
177
+ warnOnce("get", `The evaluation cache's get failed: ${describe(error)}.`);
178
+ return;
179
+ }
180
+ if (value === void 0 || value === null) return void 0;
181
+ const flags = decodeCached(value);
182
+ if (flags === void 0) {
183
+ warnOnce("shape", "The evaluation cache returned a value that is not a cached bulk result.");
184
+ return;
185
+ }
186
+ return bulkOf(flags);
187
+ };
188
+ const store = async (cache, key, result, waitUntil) => {
189
+ const value = {
190
+ storedAt: Date.now(),
191
+ flags: result.flags.map(toCached)
192
+ };
193
+ const write = (async () => cache.set(key, value))().then(() => {}, (error) => warnOnce("set", `The evaluation cache's set failed: ${describe(error)}.`));
194
+ if (waitUntil !== void 0) return waitUntil(write);
195
+ await withTimeout(timeoutMs, () => write).catch(() => {});
196
+ };
197
+ const resolve = async (context, canonical, view, signal) => {
198
+ let key;
199
+ if (cache !== void 0) {
200
+ try {
201
+ keyHash ??= await sha256Hex(options.sdkKey);
202
+ key = await cacheKeyFor(cacheEndpoint, keyHash, canonical);
203
+ } catch (error) {
204
+ warnOnce("key", `Could not compute the evaluation cache key: ${describe(error)}.`);
205
+ }
206
+ if (key !== void 0) {
207
+ const hit = await fromCache(cache, key);
208
+ if (hit !== void 0) return hit;
209
+ }
210
+ }
211
+ const result = await fetchBulk(context, view.single ? signal : void 0);
212
+ if (cache !== void 0 && key !== void 0 && result.ok) await store(cache, key, result, view.waitUntil);
213
+ return result;
214
+ };
215
+ const load = (view, context, signal) => {
216
+ const merged = mergedContext(context);
217
+ const canonical = canonicalJson(merged);
218
+ let result = view.results.get(canonical);
219
+ if (result === void 0) {
220
+ result = resolve(merged, canonical, view, signal);
221
+ view.results.set(canonical, result);
222
+ }
223
+ return result;
224
+ };
225
+ const evaluate = async (view, key, defaultValue, context, signal) => {
226
+ const bulk = await load(view, context, signal);
227
+ try {
228
+ if (!bulk.ok) return errorDetails(key, defaultValue, bulk.errorCode, bulk.message);
229
+ const entry = bulk.byKey.get(key);
230
+ if (entry === void 0) return errorDetails(key, defaultValue, "FLAG_NOT_FOUND", `Flag '${key}' not found`);
231
+ record(entry);
232
+ return entryDetails(entry, defaultValue);
233
+ } finally {
234
+ telemetry?.check(view.waitUntil);
235
+ }
236
+ };
237
+ return {
238
+ view: (waitUntil, single = false) => ({
239
+ waitUntil,
240
+ single,
241
+ results: /* @__PURE__ */ new Map()
242
+ }),
243
+ load,
244
+ evaluate,
245
+ typed: async (view, key, defaultValue, context, type, signal) => enforceType(await evaluate(view, key, defaultValue, context, signal), key, defaultValue, type),
246
+ evaluateAll: async (view, context, signal) => {
247
+ const bulk = await load(view, context, signal);
248
+ try {
249
+ if (!bulk.ok) return [];
250
+ for (const entry of bulk.flags) record(entry);
251
+ return bulk.flags.map((entry) => entryDetails(entry, null));
252
+ } finally {
253
+ telemetry?.check(view.waitUntil);
254
+ }
255
+ },
256
+ flushTelemetry: () => telemetry?.flush() ?? Promise.resolve()
257
+ };
258
+ };
259
+ export { memoryCache as n, createServerlessCore as t };
@@ -0,0 +1,206 @@
1
+ import { a as ErrorCode, c as FlagValue, l as Reason, o as EvaluationContext, s as FlagMetadata, t as EvaluationDetails } from "./types-BHn_vrQq.mjs";
2
+ /** One flag of a cached bulk result that resolved: the OFREP success entry. */
3
+ interface CachedFlagSuccess {
4
+ readonly key: string;
5
+ readonly value: FlagValue;
6
+ readonly reason: Reason;
7
+ readonly variant?: string;
8
+ readonly metadata?: FlagMetadata;
9
+ }
10
+ /** One flag of a cached bulk result that failed to resolve: the OFREP failure entry. */
11
+ interface CachedFlagFailure {
12
+ readonly key: string;
13
+ readonly errorCode: ErrorCode;
14
+ readonly errorDetails?: string;
15
+ readonly metadata?: FlagMetadata;
16
+ }
17
+ type CachedFlagEvaluation = CachedFlagSuccess | CachedFlagFailure;
18
+ /**
19
+ * What the serverless client stores in an {@link EvaluationCache}: the flags of
20
+ * one bulk evaluation (`POST /ofrep/v1/evaluate/flags`) for one evaluation
21
+ * context, and when it was stored. Plain JSON: `JSON.stringify` and
22
+ * `JSON.parse` round-trip it, so any key-value store can hold it.
23
+ */
24
+ interface CachedEvaluations {
25
+ /** When the client stored this result, in milliseconds since the epoch (`Date.now()`). */
26
+ readonly storedAt: number;
27
+ readonly flags: ReadonlyArray<CachedFlagEvaluation>;
28
+ }
29
+ /**
30
+ * A cache for bulk evaluation results, shared by requests (and, for a shared
31
+ * store, by isolates). The serverless client calls `get` before it sends a bulk
32
+ * request, and `set` after a successful one. **Expiry is the cache's job**: the
33
+ * client uses any value `get` returns, whatever its age. A value that `get`
34
+ * throws for, or that does not have the {@link CachedEvaluations} shape, is a
35
+ * miss. A `set` that throws is ignored.
36
+ *
37
+ * Keys start with `toggld:v1:` and are 64 hex characters after that: a SHA-256
38
+ * hash of the endpoint, a hash of the SDK key, and the evaluation context. They
39
+ * never contain the SDK key or the context.
40
+ */
41
+ interface EvaluationCache {
42
+ get(key: string): CachedEvaluations | undefined | Promise<CachedEvaluations | undefined>;
43
+ set(key: string, value: CachedEvaluations): void | Promise<void>;
44
+ }
45
+ /** Options of {@link memoryCache}. */
46
+ interface MemoryCacheOptions {
47
+ /**
48
+ * How long an entry is used, in milliseconds. Default `30000`. A flag change
49
+ * can take up to this long to show for a context that is already cached.
50
+ */
51
+ readonly ttlMs?: number;
52
+ /**
53
+ * How many entries are kept; the least recently used goes first. Default
54
+ * `1000`. An entry is one evaluation context's result, so this bounds the
55
+ * number of distinct contexts (usually users) held at once.
56
+ */
57
+ readonly maxEntries?: number;
58
+ }
59
+ /**
60
+ * An {@link EvaluationCache} in the memory of this isolate or process: a
61
+ * bounded least-recently-used map, with entries that expire `ttlMs` after they
62
+ * are stored. Expiry is checked on `get`; it starts no timer.
63
+ *
64
+ * It holds only plain data (a frozen copy of each value, never a promise or a
65
+ * `Response`), so on Cloudflare Workers requests can share it safely. Each
66
+ * isolate or process has its own: a new isolate starts empty.
67
+ *
68
+ * It trades freshness for fewer billable requests: for up to `ttlMs`, a
69
+ * context that is already cached does not see a flag change.
70
+ */
71
+ declare const memoryCache: (options?: MemoryCacheOptions) => EvaluationCache;
72
+ /** Configuration for {@link createServerlessClient}. */
73
+ interface ServerlessClientOptions {
74
+ /**
75
+ * A **secret** SDK key (`ffk_…`), sent as `Authorization: Bearer <sdkKey>`.
76
+ * An evaluation key is enough. Public tokens (`ffp_…`) are for browsers, so
77
+ * `createServerlessClient` throws a `TypeError` for them.
78
+ */
79
+ readonly sdkKey: string;
80
+ /**
81
+ * Base URL of the flag API, such as `https://api.toggld.io`. It may have a
82
+ * path prefix, for a proxy on your own domain (`https://flags.example.com/toggld`):
83
+ * requests go to `<endpoint>/ofrep/v1/evaluate/flags`.
84
+ */
85
+ readonly endpoint: string;
86
+ /**
87
+ * Timeout of each bulk request (and each cache call), in milliseconds.
88
+ * Default `1500`. A request that runs out of time resolves every evaluation
89
+ * of its context to the caller's default with `GENERAL`.
90
+ */
91
+ readonly timeoutMs?: number;
92
+ /**
93
+ * Attributes merged into the evaluation context of every evaluation, under
94
+ * the per-call context (per-call attributes win).
95
+ */
96
+ readonly defaultContext?: EvaluationContext;
97
+ /**
98
+ * Custom `fetch`. Defaults to the global `fetch`. On Cloudflare Workers, a
99
+ * service binding to your own proxy Worker fits here:
100
+ * `fetch: (input, init) => env.FLAG_PROXY.fetch(input, init)`.
101
+ */
102
+ readonly fetch?: typeof globalThis.fetch;
103
+ /**
104
+ * An optional cache of bulk results, keyed by endpoint, SDK key, and
105
+ * evaluation context. Without one, every request view sends its own bulk
106
+ * request (per context), and changes show on the next request. With one, a
107
+ * cached context sends no request until the cache expires the entry: fewer
108
+ * billable requests, older values. {@link memoryCache} keeps entries in the
109
+ * isolate's memory; any key-value store (Workers KV, the Cache API, Redis)
110
+ * can implement {@link EvaluationCache}.
111
+ */
112
+ readonly cache?: EvaluationCache;
113
+ /**
114
+ * Evaluation telemetry, on by default. The API does not count bulk requests
115
+ * in your flag analytics (a bulk answer holds every flag, not the ones you
116
+ * read), so the client counts each read that resolves a flag, cache hits
117
+ * included, and sends the counts to `POST /v1/telemetry`: grouped per flag,
118
+ * variant, reason, and minute, with no evaluation context or values.
119
+ *
120
+ * A send is due when the oldest count is `flushIntervalMs` old (default
121
+ * `30000`) or 1,000 cells are counted, and a request view that reads a
122
+ * flag starts it: in the request's `waitUntil` when `forRequest(ctx)` has
123
+ * one, else without waiting for it (on AWS Lambda, which freezes after the
124
+ * handler returns, a send may then be cut off). Evaluations and responses
125
+ * never wait for telemetry, and at most one send runs at a time. Counts
126
+ * live in the client (one per isolate or process), so they are sent by a
127
+ * later request and lost if the isolate or process ends first.
128
+ *
129
+ * Sends go through the same `endpoint` and `fetch` as evaluations. Set
130
+ * `false` to send nothing: these reads are then not in analytics.
131
+ */
132
+ readonly telemetry?: false | {
133
+ readonly flushIntervalMs?: number;
134
+ };
135
+ }
136
+ /**
137
+ * The request context {@link ServerlessClient.forRequest} reads: the Workers
138
+ * `ExecutionContext`, or any object with `waitUntil` (Vercel's `waitUntil`
139
+ * from `@vercel/functions`, a Next.js middleware `event`, …). An object
140
+ * without `waitUntil`, such as the AWS Lambda `context`, counts as none.
141
+ */
142
+ interface WaitUntilContext {
143
+ readonly waitUntil?: ((promise: Promise<unknown>) => void) | undefined;
144
+ }
145
+ /** The evaluation surface for one request. Every method resolves; none rejects. */
146
+ interface ServerlessRequestClient {
147
+ /** Resolve a flag, returning full details with an untyped (JSON) value. */
148
+ evaluate(key: string, defaultValue: FlagValue, context?: EvaluationContext): Promise<EvaluationDetails<FlagValue>>;
149
+ /**
150
+ * Every flag of the environment for this context, from the same bulk request
151
+ * as the other reads. Empty if that request failed.
152
+ */
153
+ evaluateAll(context?: EvaluationContext): Promise<ReadonlyArray<EvaluationDetails<FlagValue>>>;
154
+ getBooleanValue(key: string, defaultValue: boolean, context?: EvaluationContext): Promise<boolean>;
155
+ getBooleanDetails(key: string, defaultValue: boolean, context?: EvaluationContext): Promise<EvaluationDetails<boolean>>;
156
+ getStringValue(key: string, defaultValue: string, context?: EvaluationContext): Promise<string>;
157
+ getStringDetails(key: string, defaultValue: string, context?: EvaluationContext): Promise<EvaluationDetails<string>>;
158
+ getNumberValue(key: string, defaultValue: number, context?: EvaluationContext): Promise<number>;
159
+ getNumberDetails(key: string, defaultValue: number, context?: EvaluationContext): Promise<EvaluationDetails<number>>;
160
+ getObjectValue<T extends FlagValue>(key: string, defaultValue: T, context?: EvaluationContext): Promise<T>;
161
+ getObjectDetails<T extends FlagValue>(key: string, defaultValue: T, context?: EvaluationContext): Promise<EvaluationDetails<T>>;
162
+ }
163
+ /** A serverless client. Create one per isolate or process (in module scope), and a request view per request. */
164
+ interface ServerlessClient {
165
+ /**
166
+ * The evaluation surface for one incoming request. Its first evaluation for
167
+ * a context sends one bulk request; later reads with the same context
168
+ * (`defaultContext` merged in) use that answer, and concurrent first reads
169
+ * share it. A different context sends its own bulk request.
170
+ *
171
+ * `ctx` carries the request's `waitUntil` (Workers `ctx`, Vercel). With
172
+ * one, cache writes and telemetry sends run after the response. Without one
173
+ * (`forRequest()`, AWS Lambda, Node serverless functions), cache writes run
174
+ * inline, bounded by `timeoutMs`, and never fail an evaluation; telemetry
175
+ * sends start without being awaited (see {@link ServerlessClientOptions.telemetry}).
176
+ *
177
+ * Use the returned object only within that request, and create a new one
178
+ * for the next: on Workers, I/O started by one request cannot be awaited by
179
+ * another.
180
+ */
181
+ forRequest(ctx?: WaitUntilContext | object): ServerlessRequestClient;
182
+ /**
183
+ * Send the counted reads now, whether or not a send is due (see
184
+ * {@link ServerlessClientOptions.telemetry}). If a send is already in
185
+ * flight, it waits for that send first, then sends what is left. Bounded by
186
+ * `timeoutMs` as a whole; it never rejects, and a failed send stays queued
187
+ * for retry. With `telemetry: false`, it does nothing.
188
+ *
189
+ * Runtimes with a `waitUntil` (Workers, Vercel) do not need it. On runtimes
190
+ * without one, such as AWS Lambda, which freezes the process when the
191
+ * handler returns, call it before returning, or in the platform's shutdown
192
+ * hook if there is one. Awaiting it delays the response by up to
193
+ * `timeoutMs`.
194
+ */
195
+ flushTelemetry(): Promise<void>;
196
+ }
197
+ /**
198
+ * Create a serverless client. Call it once per isolate or process, in module
199
+ * scope (or lazily on the first request, where Worker bindings are
200
+ * available). It makes no request until the first evaluation.
201
+ *
202
+ * Throws a `TypeError` for options that can never work: no `sdkKey`, a public
203
+ * token (`ffp_…`), or an `endpoint` that is not an http(s) URL.
204
+ */
205
+ declare const createServerlessClient: (options: ServerlessClientOptions) => ServerlessClient;
206
+ export { createServerlessClient as a, CachedFlagFailure as c, MemoryCacheOptions as d, memoryCache as f, WaitUntilContext as i, CachedFlagSuccess as l, ServerlessClientOptions as n, CachedEvaluations as o, ServerlessRequestClient as r, CachedFlagEvaluation as s, ServerlessClient as t, EvaluationCache as u };
@@ -0,0 +1,51 @@
1
+ import { a as ErrorCode, c as FlagValue, l as Reason, o as EvaluationContext, s as FlagMetadata, t as EvaluationDetails } from "./types-BHn_vrQq.mjs";
2
+ import { n as ToggldClientShape, t as ToggldClient } from "./effectService-CGZnDaqS.mjs";
3
+ import { c as CachedFlagFailure, d as MemoryCacheOptions, f as memoryCache, l as CachedFlagSuccess, n as ServerlessClientOptions, o as CachedEvaluations, s as CachedFlagEvaluation, u as EvaluationCache } from "./serverless-e__iCjpQ.mjs";
4
+ import { Context, Effect, Layer } from "effect";
5
+ declare const WaitUntil_base: Context.ServiceClass<WaitUntil, "@toggld/js/serverless/WaitUntil", {
6
+ readonly waitUntil: (promise: Promise<unknown>) => void;
7
+ }>;
8
+ /**
9
+ * The current request's `waitUntil`. Provide it per request (Workers, Vercel)
10
+ * with {@link withRequest}, {@link withWaitUntil}, or {@link waitUntilLayer};
11
+ * leave it out where the runtime freezes after the handler returns (AWS
12
+ * Lambda). It only affects cache writes and telemetry sends.
13
+ */
14
+ export declare class WaitUntil extends WaitUntil_base {}
15
+ declare const RequestScope_base: Context.ServiceClass<RequestScope, "@toggld/js/serverless/RequestScope", {
16
+ readonly views: Map<unknown, unknown>;
17
+ }>;
18
+ /**
19
+ * The evaluation state of one incoming request: its bulk results. Create it
20
+ * with {@link withRequest} or {@link requestLayer}, once per request; do not
21
+ * share it between requests.
22
+ */
23
+ export declare class RequestScope extends RequestScope_base {}
24
+ /** What {@link withWaitUntil} accepts: a Workers `ExecutionContext`, or any object with `waitUntil`. */
25
+ export interface WaitUntilSource {
26
+ readonly waitUntil: (promise: Promise<unknown>) => void;
27
+ }
28
+ /**
29
+ * Run `self` as one incoming request: a fresh {@link RequestScope} (one bulk
30
+ * request per context), plus {@link WaitUntil} when `ctx` has a `waitUntil`
31
+ * (a Workers `ExecutionContext`, Vercel). Without `ctx` (AWS Lambda), cache
32
+ * writes run inline.
33
+ */
34
+ export declare const withRequest: (ctx?: WaitUntilSource | object) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, WaitUntil | RequestScope>>;
35
+ /** {@link withRequest} as a layer. Build it once per incoming request. */
36
+ export declare const requestLayer: (ctx?: WaitUntilSource | object) => Layer.Layer<RequestScope | WaitUntil>;
37
+ /** The same as {@link withRequest} with a `waitUntil` source: a request scope and its {@link WaitUntil}. */
38
+ export declare const withWaitUntil: (source: WaitUntilSource) => <A, E, R>(self: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, WaitUntil | RequestScope>>;
39
+ /** {@link WaitUntil} and a fresh {@link RequestScope} as a layer, for one request. */
40
+ export declare const waitUntilLayer: (source: WaitUntilSource) => Layer.Layer<RequestScope | WaitUntil>;
41
+ /**
42
+ * A `Layer` providing {@link ToggldClient} with one serverless client for the
43
+ * layer's scope. Build it once per isolate or process (a module-scope
44
+ * `ManagedRuntime`), not per request, and provide {@link withRequest} per
45
+ * request. Building it makes no request, and it holds no resource.
46
+ *
47
+ * Options that can never work (no `sdkKey`, a public token, or an invalid
48
+ * `endpoint`) make the layer die with a `TypeError`.
49
+ */
50
+ export declare const layer: (options: ServerlessClientOptions) => Layer.Layer<ToggldClient>;
51
+ export { type CachedEvaluations, type CachedFlagEvaluation, type CachedFlagFailure, type CachedFlagSuccess, type ErrorCode, type EvaluationCache, type EvaluationContext, type EvaluationDetails, type FlagMetadata, type FlagValue, type MemoryCacheOptions, type Reason, type ServerlessClientOptions, ToggldClient, type ToggldClientShape, memoryCache };