@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.
@@ -0,0 +1,365 @@
1
+ import { d as discard, h as withTimeout, m as refusalMessage, t as WireError } from "./wire-CrkBeqnM.mjs";
2
+ // src/internal/telemetry.ts
3
+ const DEFAULT_FLUSH_INTERVAL_MS = 3e4;
4
+ const MAX_CELLS = 1e4;
5
+ const MAX_COUNT = 1e9;
6
+ const BATCH_SIZE = 1e3;
7
+ /** A client that does not report: file mode, or `telemetry: false`. */
8
+ const noTelemetry = {
9
+ record: () => {},
10
+ flush: () => Promise.resolve(),
11
+ close: () => Promise.resolve()
12
+ };
13
+ /** The configured flush interval, or the default for a missing or unusable one. */
14
+ const flushInterval = (telemetry) => {
15
+ const configured = telemetry === false ? void 0 : telemetry?.flushIntervalMs;
16
+ return configured !== void 0 && Number.isFinite(configured) && configured > 0 ? configured : DEFAULT_FLUSH_INTERVAL_MS;
17
+ };
18
+ /**
19
+ * The counting half: bounded, synchronous `record`, and `take`, which turns the
20
+ * counts so far into batches ready to send (each with a fresh `windowId`) and
21
+ * starts a new window.
22
+ */
23
+ const makeRollups = () => {
24
+ let cells = /* @__PURE__ */ new Map();
25
+ let openedAt;
26
+ return {
27
+ isEmpty: () => cells.size === 0,
28
+ size: () => cells.size,
29
+ openedAt: () => openedAt,
30
+ take: (instanceId) => {
31
+ const rollups = [...cells.values()];
32
+ cells = /* @__PURE__ */ new Map();
33
+ openedAt = void 0;
34
+ const batches = [];
35
+ for (let i = 0; i < rollups.length; i += BATCH_SIZE) batches.push({
36
+ sdkInstanceId: instanceId,
37
+ windowId: crypto.randomUUID(),
38
+ rollups: rollups.slice(i, i + BATCH_SIZE)
39
+ });
40
+ return batches;
41
+ },
42
+ record: (flagKey, resolution) => {
43
+ if (flagKey.length === 0 || flagKey.length > 256 || (resolution.variant?.length ?? 0) > 256) return;
44
+ const variant = resolution.variant ?? "";
45
+ const now = Date.now();
46
+ const bucketStart = (/* @__PURE__ */ new Date(Math.floor(now / 6e4) * 6e4)).toISOString();
47
+ const key = JSON.stringify([
48
+ flagKey,
49
+ variant,
50
+ resolution.reason,
51
+ bucketStart
52
+ ]);
53
+ const cell = cells.get(key);
54
+ if (cell) {
55
+ if (cell.count < MAX_COUNT) cells.set(key, {
56
+ ...cell,
57
+ count: cell.count + 1
58
+ });
59
+ } else if (cells.size < MAX_CELLS) cells.set(key, {
60
+ flagKey,
61
+ variant,
62
+ reason: resolution.reason,
63
+ bucketStart,
64
+ count: 1
65
+ });
66
+ else return;
67
+ openedAt ??= now;
68
+ }
69
+ };
70
+ };
71
+ /** Send one batch; `true` once the API acknowledged it. Never rejects. */
72
+ const sendBatch = async (send, batch, timeoutMs, signal) => {
73
+ try {
74
+ const response = await withTimeout(timeoutMs, (s) => send("POST", "/v1/telemetry", batch, s), signal);
75
+ discard(response);
76
+ return response.status >= 200 && response.status < 300;
77
+ } catch {
78
+ return false;
79
+ }
80
+ };
81
+ const startTelemetry = (send, options) => {
82
+ const instanceId = crypto.randomUUID();
83
+ const interval = flushInterval(options.telemetry);
84
+ const timeoutMs = options.timeoutMs ?? 5e3;
85
+ const rollups = makeRollups();
86
+ const pending = [];
87
+ let tail = Promise.resolve();
88
+ const serialized = (signal) => {
89
+ const run = tail.then(() => flushNow(signal));
90
+ tail = run.catch(() => {});
91
+ return run;
92
+ };
93
+ const flushNow = async (signal) => {
94
+ for (let pass = 0; pass < 2; pass++) {
95
+ if (pending.length === 0) pending.push(...rollups.take(instanceId));
96
+ while (pending.length > 0) {
97
+ if (!await sendBatch(send, pending[0], timeoutMs, signal)) return;
98
+ pending.shift();
99
+ }
100
+ }
101
+ };
102
+ const periodic = new AbortController();
103
+ let timer;
104
+ const schedule = () => {
105
+ timer = setTimeout(() => {
106
+ serialized(periodic.signal).then(() => {
107
+ if (!periodic.signal.aborted) schedule();
108
+ });
109
+ }, interval);
110
+ if (options.unref === true) timer?.unref?.();
111
+ };
112
+ schedule();
113
+ let closing;
114
+ return {
115
+ flush: () => serialized(void 0),
116
+ close: () => closing ??= (async () => {
117
+ clearTimeout(timer);
118
+ periodic.abort();
119
+ await withTimeout(timeoutMs, (signal) => serialized(signal)).catch(() => {});
120
+ })(),
121
+ record: rollups.record
122
+ };
123
+ };
124
+ /**
125
+ * {@link startTelemetry} on the first count: no id, timer, or request before
126
+ * a client has something to report. Remote mode uses it, since most remote
127
+ * clients only evaluate single flags, which the API counts itself.
128
+ */
129
+ const lazyTelemetry = (start) => {
130
+ let started;
131
+ let closed = false;
132
+ return {
133
+ record: (flagKey, resolution) => {
134
+ if (closed) return;
135
+ (started ??= start()).record(flagKey, resolution);
136
+ },
137
+ flush: () => started?.flush() ?? Promise.resolve(),
138
+ close: () => {
139
+ closed = true;
140
+ return started?.close() ?? Promise.resolve();
141
+ }
142
+ };
143
+ };
144
+ /** How often `flush` looks whether a send in flight has ended. */
145
+ const FLUSH_POLL_MS = 25;
146
+ /**
147
+ * Telemetry flushed by the requests that record it, for runtimes where
148
+ * nothing may run between requests (the serverless client, ADR-0020). All of
149
+ * its state is plain data (counts, numbers, batches), never a promise, so
150
+ * any request of the isolate or process may send it: on Workers, I/O started
151
+ * by one request cannot be awaited by another.
152
+ *
153
+ * - **Due** when the oldest count not sent is `flushIntervalMs` old (default
154
+ * 30 seconds), there are {@link REQUEST_FLUSH_CELLS} cells, or a failed
155
+ * window waits for its retry; checked by the request that records or
156
+ * finishes a read ({@link RequestTelemetry.check}).
157
+ * - **At most one send in flight.** It holds a lease (the time its sends may
158
+ * take, with their timeouts) instead of a promise: a runtime may stop a
159
+ * `waitUntil` without settling it, and the lease lets a later request send.
160
+ * - **Failed windows** stay queued with their `windowId` and are retried
161
+ * first, one interval later; meanwhile new counts add up in the bounded
162
+ * cells. The queue holds at most one take (10,000 cells, so at most 10
163
+ * batches of 1,000). The API does not deduplicate windows, so a window
164
+ * resent after a lost response is counted twice, which is accepted.
165
+ * - Nothing random runs before the first send: Workers refuse random values
166
+ * in global scope, where the client is created. The instance id is made on
167
+ * the first send.
168
+ */
169
+ const makeRequestTelemetry = (send, options) => {
170
+ const interval = flushInterval(options.telemetry);
171
+ const timeoutMs = options.timeoutMs ?? 5e3;
172
+ const rollups = makeRollups();
173
+ let instanceId;
174
+ let pending = [];
175
+ let lease = {
176
+ id: 0,
177
+ until: 0
178
+ };
179
+ let leases = 0;
180
+ let retryAt = 0;
181
+ const due = (now) => {
182
+ if (now < lease.until || now < retryAt) return false;
183
+ if (pending.length > 0 || rollups.size() >= 1e3) return true;
184
+ const openedAt = rollups.openedAt();
185
+ return openedAt !== void 0 && now - openedAt >= interval;
186
+ };
187
+ const run = async () => {
188
+ const id = ++leases;
189
+ try {
190
+ for (let pass = 0; pass < 2; pass++) {
191
+ if (pending.length === 0 && !rollups.isEmpty()) pending = rollups.take(instanceId ??= crypto.randomUUID());
192
+ const batches = [...pending];
193
+ if (batches.length === 0) return;
194
+ lease = {
195
+ id,
196
+ until: Date.now() + timeoutMs * (batches.length + 1)
197
+ };
198
+ for (const batch of batches) {
199
+ if (!await sendBatch(send, batch, timeoutMs, void 0)) {
200
+ retryAt = Date.now() + interval;
201
+ return;
202
+ }
203
+ const index = pending.indexOf(batch);
204
+ if (index >= 0) pending.splice(index, 1);
205
+ retryAt = 0;
206
+ }
207
+ }
208
+ } finally {
209
+ if (lease.id === id) lease = {
210
+ id,
211
+ until: 0
212
+ };
213
+ }
214
+ };
215
+ return {
216
+ record: rollups.record,
217
+ check: (waitUntil) => {
218
+ if (!due(Date.now())) return;
219
+ const sending = run().catch(() => {});
220
+ if (waitUntil !== void 0) waitUntil(sending);
221
+ },
222
+ flush: async () => {
223
+ const deadline = Date.now() + timeoutMs;
224
+ while (Date.now() < lease.until) {
225
+ const left = deadline - Date.now();
226
+ if (left <= 0) return;
227
+ await new Promise((resolve) => setTimeout(resolve, Math.min(FLUSH_POLL_MS, left)));
228
+ }
229
+ const left = deadline - Date.now();
230
+ if (left <= 0) return;
231
+ await withTimeout(left, () => run()).catch(() => {});
232
+ }
233
+ };
234
+ };
235
+ // src/internal/remote.ts
236
+ /** The details of an OFREP success. */
237
+ const okDetails = (key, s) => ({
238
+ flagKey: key,
239
+ value: s.value,
240
+ variant: s.variant,
241
+ reason: s.reason,
242
+ errorCode: void 0,
243
+ errorMessage: void 0,
244
+ flagMetadata: s.metadata ?? {}
245
+ });
246
+ /** The details of an OFREP failure: the caller's `fallback` with the server's error code. */
247
+ const wireErrorDetails = (fallback, e) => ({
248
+ flagKey: e.key,
249
+ value: fallback,
250
+ variant: void 0,
251
+ reason: "ERROR",
252
+ errorCode: e.errorCode,
253
+ errorMessage: e.errorDetails,
254
+ flagMetadata: e.metadata ?? {}
255
+ });
256
+ /** Why the API refused an OFREP request whose response is not an OFREP failure. */
257
+ const rejectedMessage = (response) => refusalMessage(response, "The SDK key cannot evaluate flags (HTTP 403).");
258
+ /** A body of the wrong shape is a `PARSE_ERROR`; anything else (network, timeout, not JSON) is `GENERAL`. */
259
+ const codeFor = (e) => e instanceof WireError ? "PARSE_ERROR" : "GENERAL";
260
+ // ../spec/src/index.ts
261
+ /** True when `value` is assignable to a flag declared as `type`. */
262
+ const valueMatchesType = (value, type) => {
263
+ switch (type) {
264
+ case "boolean": return typeof value === "boolean";
265
+ case "string": return typeof value === "string";
266
+ case "number": return typeof value === "number" && Number.isFinite(value);
267
+ case "object": return typeof value === "object" && value !== null;
268
+ }
269
+ };
270
+ /**
271
+ * The sync stream's WebSocket subprotocol name. Offered first by clients (the
272
+ * server echoes the first offer back, completing the RFC 6455 negotiation).
273
+ */
274
+ const SYNC_PROTOCOL = "toggld.sync.v1";
275
+ /**
276
+ * Subprotocol entry carrying the bearer credential on a sync WebSocket
277
+ * upgrade: `toggld.bearer.<token>`. The standard `WebSocket` constructor
278
+ * cannot set an `Authorization` header, so the token rides the
279
+ * `Sec-WebSocket-Protocol` offer instead (a recognized pattern — it stays out
280
+ * of URLs and access logs). Servers accept either this or a real
281
+ * `Authorization: Bearer` header.
282
+ */
283
+ const SYNC_BEARER_PROTOCOL_PREFIX = "toggld.bearer.";
284
+ /**
285
+ * Heartbeat on the evaluation stream and the sync stream: the client sends the
286
+ * text frame {@link STREAM_PING} (not JSON) and the server answers {@link STREAM_PONG}.
287
+ * Without it, a connection that a proxy or a sleeping laptop dropped silently
288
+ * would look open forever. Plain fixed strings, so a Durable Object can answer
289
+ * with an auto-response without waking from hibernation.
290
+ */
291
+ const STREAM_PING = "ping";
292
+ const STREAM_PONG = "pong";
293
+ /** How often the client pings, and how long it waits for the pong before it reconnects. */
294
+ const STREAM_PING_INTERVAL_MS = 3e4;
295
+ const STREAM_PONG_TIMEOUT_MS = 1e4;
296
+ const TARGETING_KEY = "targetingKey";
297
+ /** Reads the targeting key from a context, if present and a non-empty string. */
298
+ const targetingKeyOf = (ctx) => {
299
+ const v = ctx[TARGETING_KEY];
300
+ return typeof v === "string" && v.length > 0 ? v : void 0;
301
+ };
302
+ /** Reads an arbitrary attribute, treating `targetingKey` like any other field. */
303
+ const attributeOf = (ctx, attribute) => Object.prototype.hasOwnProperty.call(ctx, attribute) ? ctx[attribute] : void 0;
304
+ const Resolution = {
305
+ /** A successful resolution. */
306
+ ok: (args) => ({
307
+ value: args.value,
308
+ variant: args.variant,
309
+ reason: args.reason,
310
+ errorCode: void 0,
311
+ errorMessage: void 0,
312
+ flagMetadata: args.flagMetadata ?? {}
313
+ }),
314
+ /** An abnormal resolution carrying an error code and a fallback value. */
315
+ error: (args) => ({
316
+ value: args.value,
317
+ variant: args.variant,
318
+ reason: "ERROR",
319
+ errorCode: args.errorCode,
320
+ errorMessage: args.errorMessage,
321
+ flagMetadata: args.flagMetadata ?? {}
322
+ })
323
+ };
324
+ const isBoolean = (v) => typeof v === "boolean";
325
+ const isString = (v) => typeof v === "string";
326
+ const isNumber = (v) => typeof v === "number" && Number.isFinite(v);
327
+ const isObject = (v) => valueMatchesType(v, "object");
328
+ const GUARDS = {
329
+ boolean: isBoolean,
330
+ string: isString,
331
+ number: isNumber,
332
+ object: isObject
333
+ };
334
+ const errorDetails = (key, value, errorCode, errorMessage, flagMetadata = {}) => ({
335
+ flagKey: key,
336
+ value,
337
+ variant: void 0,
338
+ reason: "ERROR",
339
+ errorCode,
340
+ errorMessage,
341
+ flagMetadata: { ...flagMetadata }
342
+ });
343
+ /**
344
+ * Enforce the requested value type on resolved details: a value of another
345
+ * type serves the caller's default with `TYPE_MISMATCH`. Error details pass
346
+ * through unchanged.
347
+ */
348
+ const enforceType = (d, key, defaultValue, type) => {
349
+ if (d.errorCode !== void 0 || GUARDS[type](d.value)) return d;
350
+ return errorDetails(key, defaultValue, "TYPE_MISMATCH", `Flag '${key}' value is not of type ${type}`, d.flagMetadata);
351
+ };
352
+ const syncCredentialProblem = (options) => {
353
+ const key = options.sdkKey;
354
+ if (typeof key !== "string" || key.trim() === "") return "missing-key";
355
+ if (key.startsWith("ffp_")) return "public-token";
356
+ let url;
357
+ try {
358
+ url = new URL(options.endpoint ?? "");
359
+ } catch {
360
+ url = void 0;
361
+ }
362
+ if (url === void 0 || url.protocol !== "https:" && url.protocol !== "http:") return "invalid-endpoint";
363
+ };
364
+ const invalidEndpointMessage = (endpoint) => `[toggld] \`endpoint\` must be the http(s) URL of the Toggld API, such as https://api.toggld.io (got ${JSON.stringify(endpoint)}).`;
365
+ export { wireErrorDetails as _, Resolution as a, noTelemetry as b, STREAM_PONG as c, SYNC_PROTOCOL as d, attributeOf as f, rejectedMessage as g, okDetails as h, syncCredentialProblem as i, STREAM_PONG_TIMEOUT_MS as l, codeFor as m, errorDetails as n, STREAM_PING as o, targetingKeyOf as p, invalidEndpointMessage as r, STREAM_PING_INTERVAL_MS as s, enforceType as t, SYNC_BEARER_PROTOCOL_PREFIX as u, lazyTelemetry as v, startTelemetry as x, makeRequestTelemetry as y };
@@ -0,0 +1,17 @@
1
+ import { a as ErrorCode, c as FlagValue, i as ToggldClientOptions, l as Reason, n as EvaluationMode, o as EvaluationContext, r as SyncOptions, 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 { Layer } from "effect";
4
+ /**
5
+ * A self-contained `Layer` providing {@link ToggldClient}. Building it waits
6
+ * for the first snapshot for up to `sync.readyTimeoutMs` (default 5 s), like
7
+ * `ToggldClient.create`; set it to `0` not to wait, and `yield* flags.ready`
8
+ * where you need the data. In sync mode (the default) background sync lives
9
+ * in the layer's scope: closing it (e.g. `ManagedRuntime.dispose`) stops sync,
10
+ * closes the stream, and flushes telemetry.
11
+ *
12
+ * Options that can never sync — no `sdkKey`, a public token (`ffp_…`), or no
13
+ * valid `endpoint` in sync mode — make the layer die with a `TypeError`, as do
14
+ * `mode: "local"`, `sync.file`, and `file` without `mode: "file"`.
15
+ */
16
+ export declare const layer: (options: ToggldClientOptions) => Layer.Layer<ToggldClient>;
17
+ export { type ErrorCode, type EvaluationContext, type EvaluationDetails, type EvaluationMode, type FlagMetadata, type FlagValue, type Reason, type SyncOptions, ToggldClient, type ToggldClientOptions, type ToggldClientShape };
@@ -0,0 +1,75 @@
1
+ import { t as makeCore } from "./core-BQfyNOoT.mjs";
2
+ import { t as ToggldClient } from "./effectService-DPD-fNCI.mjs";
3
+ import { Effect, Layer } from "effect";
4
+ // src/effect.ts
5
+ /**
6
+ * `@toggld/js/effect` — the native Effect API. Needs the optional `effect`
7
+ * peer dependency (`^4.0.5`); the default `@toggld/js` entry does not.
8
+ *
9
+ * The client as a scoped service: build a {@link layer} from your options,
10
+ * compose it into your runtime, and `yield* ToggldClient` to evaluate. The
11
+ * layer's scope owns the client: closing it stops sync, closes the stream, and
12
+ * sends remaining telemetry.
13
+ *
14
+ * ```ts
15
+ * import { Effect } from "effect"
16
+ * import { ToggldClient, layer } from "@toggld/js/effect"
17
+ *
18
+ * const program = Effect.gen(function* () {
19
+ * const flags = yield* ToggldClient
20
+ * return yield* flags.getBooleanValue("new-checkout", false, { targetingKey: "user-123" })
21
+ * })
22
+ *
23
+ * program.pipe(Effect.provide(layer({ sdkKey, endpoint })), Effect.runPromise)
24
+ * ```
25
+ *
26
+ * Like `ToggldClient.create`, the layer runs sync mode by default and waits
27
+ * for the first snapshot for up to `sync.readyTimeoutMs`; pass `mode: "file"`
28
+ * to evaluate from a flag file, or `mode: "remote"` to send each evaluation to
29
+ * the API instead.
30
+ *
31
+ * This is a thin wrapper over the dependency-free core in `./internal/core.ts`,
32
+ * which is the implementation of both entries. In-process (sync and file
33
+ * mode) evaluations are synchronous effects; remote ones are interruptible (interrupting one aborts
34
+ * its request).
35
+ */
36
+ /** Lift a core call: a plain (in-process) result stays synchronous; a remote one is interruptible. */
37
+ const lift = (core, run) => core.synchronous ? Effect.sync(() => run()) : Effect.promise((signal) => Promise.resolve(run(signal)));
38
+ const fromCore = (core) => {
39
+ const typed = (key, defaultValue, context, type) => lift(core, (signal) => core.typed(key, defaultValue, context, type, signal));
40
+ const value = (details) => Effect.map(details, (d) => d.value);
41
+ return {
42
+ ready: Effect.promise(() => core.ready()),
43
+ flushTelemetry: Effect.promise(() => core.flushTelemetry()),
44
+ evaluate: (key, defaultValue, context) => lift(core, (signal) => core.evaluate(key, defaultValue, context, signal)),
45
+ evaluateAll: (context) => lift(core, (signal) => core.evaluateAll(context, signal)),
46
+ getBooleanDetails: (key, defaultValue, context) => typed(key, defaultValue, context, "boolean"),
47
+ getBooleanValue: (key, defaultValue, context) => value(typed(key, defaultValue, context, "boolean")),
48
+ getStringDetails: (key, defaultValue, context) => typed(key, defaultValue, context, "string"),
49
+ getStringValue: (key, defaultValue, context) => value(typed(key, defaultValue, context, "string")),
50
+ getNumberDetails: (key, defaultValue, context) => typed(key, defaultValue, context, "number"),
51
+ getNumberValue: (key, defaultValue, context) => value(typed(key, defaultValue, context, "number")),
52
+ getObjectDetails: (key, defaultValue, context) => typed(key, defaultValue, context, "object"),
53
+ getObjectValue: (key, defaultValue, context) => value(typed(key, defaultValue, context, "object"))
54
+ };
55
+ };
56
+ /**
57
+ * Create a client owned by the current scope: it closes when the scope does.
58
+ * Waits for the first snapshot like `ToggldClient.create`, bounded by
59
+ * `sync.readyTimeoutMs`.
60
+ */
61
+ const make = (options) => Effect.acquireRelease(Effect.sync(() => makeCore(options)), (core) => Effect.promise(() => core.close())).pipe(Effect.tap((core) => Effect.promise(() => core.waitForReady())), Effect.map(fromCore));
62
+ /**
63
+ * A self-contained `Layer` providing {@link ToggldClient}. Building it waits
64
+ * for the first snapshot for up to `sync.readyTimeoutMs` (default 5 s), like
65
+ * `ToggldClient.create`; set it to `0` not to wait, and `yield* flags.ready`
66
+ * where you need the data. In sync mode (the default) background sync lives
67
+ * in the layer's scope: closing it (e.g. `ManagedRuntime.dispose`) stops sync,
68
+ * closes the stream, and flushes telemetry.
69
+ *
70
+ * Options that can never sync — no `sdkKey`, a public token (`ffp_…`), or no
71
+ * valid `endpoint` in sync mode — make the layer die with a `TypeError`, as do
72
+ * `mode: "local"`, `sync.file`, and `file` without `mode: "file"`.
73
+ */
74
+ const layer = (options) => Layer.effect(ToggldClient, make(options));
75
+ export { ToggldClient, layer };
@@ -0,0 +1,48 @@
1
+ import { c as FlagValue, o as EvaluationContext, t as EvaluationDetails } from "./types-BHn_vrQq.mjs";
2
+ import { Context, Effect } from "effect";
3
+ /** The evaluation surface of a Toggld client. Every method is non-failing. */
4
+ interface ToggldClientShape {
5
+ /**
6
+ * Completes once the client is ready to evaluate.
7
+ *
8
+ * - `@toggld/js/effect`: on the first synced snapshot in sync mode, the
9
+ * first read of the file in file mode;
10
+ * immediately in remote mode. The layer already waits for this for up to
11
+ * `sync.readyTimeoutMs`; this effect has no timeout of its own.
12
+ * - `@toggld/js/serverless/effect`: immediately (it is a remote client).
13
+ *
14
+ * In sync and file modes, until a snapshot arrives, evaluations resolve to
15
+ * the caller's default with `PROVIDER_NOT_READY`.
16
+ */
17
+ readonly ready: Effect.Effect<void>;
18
+ /**
19
+ * Send buffered telemetry. Failed windows remain queued for retry. Sync
20
+ * mode counts every evaluation; remote mode only `evaluateAll` entries (the
21
+ * API counts single evaluations); the serverless client every read that
22
+ * resolves a flag, and completes at once if a send is already in flight.
23
+ * Completes at once in file mode or with `telemetry: false`.
24
+ */
25
+ readonly flushTelemetry: Effect.Effect<void>;
26
+ /** Resolve a flag, returning full details with an untyped (JSON) value. */
27
+ readonly evaluate: (key: string, defaultValue: FlagValue, context?: EvaluationContext) => Effect.Effect<EvaluationDetails<FlagValue>>;
28
+ /** Resolve every flag for the environment (one request in remote mode; in-process otherwise). */
29
+ readonly evaluateAll: (context?: EvaluationContext) => Effect.Effect<ReadonlyArray<EvaluationDetails<FlagValue>>>;
30
+ readonly getBooleanValue: (key: string, defaultValue: boolean, context?: EvaluationContext) => Effect.Effect<boolean>;
31
+ readonly getBooleanDetails: (key: string, defaultValue: boolean, context?: EvaluationContext) => Effect.Effect<EvaluationDetails<boolean>>;
32
+ readonly getStringValue: (key: string, defaultValue: string, context?: EvaluationContext) => Effect.Effect<string>;
33
+ readonly getStringDetails: (key: string, defaultValue: string, context?: EvaluationContext) => Effect.Effect<EvaluationDetails<string>>;
34
+ readonly getNumberValue: (key: string, defaultValue: number, context?: EvaluationContext) => Effect.Effect<number>;
35
+ readonly getNumberDetails: (key: string, defaultValue: number, context?: EvaluationContext) => Effect.Effect<EvaluationDetails<number>>;
36
+ readonly getObjectValue: <T extends FlagValue>(key: string, defaultValue: T, context?: EvaluationContext) => Effect.Effect<T>;
37
+ readonly getObjectDetails: <T extends FlagValue>(key: string, defaultValue: T, context?: EvaluationContext) => Effect.Effect<EvaluationDetails<T>>;
38
+ }
39
+ declare const ToggldClient_base: Context.ServiceClass<ToggldClient, "@toggld/js/ToggldClient", ToggldClientShape>;
40
+ /**
41
+ * The Toggld client as an Effect service. Build a layer from options and
42
+ * compose it into your runtime, then `yield* ToggldClient` to evaluate flags.
43
+ * One tag for both Effect entries: `layer` from `@toggld/js/effect` (sync,
44
+ * file, or remote mode) and `layer` from `@toggld/js/serverless/effect` provide it, so
45
+ * program code is the same with either.
46
+ */
47
+ declare class ToggldClient extends ToggldClient_base {}
48
+ export { ToggldClientShape as n, ToggldClient as t };
@@ -0,0 +1,17 @@
1
+ import { Context } from "effect";
2
+ // src/internal/effectService.ts
3
+ /**
4
+ * The Effect service shared by `@toggld/js/effect` and
5
+ * `@toggld/js/serverless/effect`: the {@link ToggldClient} tag and its
6
+ * {@link ToggldClientShape}. It imports only `effect` and types, so the
7
+ * serverless Effect entry can use it without the main client's `node:` imports.
8
+ */
9
+ /**
10
+ * The Toggld client as an Effect service. Build a layer from options and
11
+ * compose it into your runtime, then `yield* ToggldClient` to evaluate flags.
12
+ * One tag for both Effect entries: `layer` from `@toggld/js/effect` (sync,
13
+ * file, or remote mode) and `layer` from `@toggld/js/serverless/effect` provide it, so
14
+ * program code is the same with either.
15
+ */
16
+ var ToggldClient = class extends Context.Service()("@toggld/js/ToggldClient") {};
17
+ export { ToggldClient as t };
@@ -0,0 +1,54 @@
1
+ import { a as ErrorCode, c as FlagValue, i as ToggldClientOptions, l as Reason, n as EvaluationMode, o as EvaluationContext, r as SyncOptions, s as FlagMetadata, t as EvaluationDetails } from "./types-BHn_vrQq.mjs";
2
+ /**
3
+ * A Toggld client. Create one with {@link ToggldClient.create} and {@link
4
+ * ToggldClient.close} it on shutdown to stop background sync.
5
+ */
6
+ export declare class ToggldClient {
7
+ #private;
8
+ private constructor();
9
+ /**
10
+ * Create a client. In sync mode (the default) it resolves once the first
11
+ * flag snapshot has synced, or once `sync.readyTimeoutMs` (default 5 s) has
12
+ * elapsed. In that case it warns once, and the client serves caller defaults
13
+ * with `PROVIDER_NOT_READY` while sync keeps retrying in the background. In
14
+ * file mode it resolves after the first read of the file; in remote mode, at
15
+ * once.
16
+ *
17
+ * Rejects with a `TypeError` when a sync-mode client could never sync: no
18
+ * `sdkKey`, a public token (`ffp_…`), or no valid `endpoint`. Use a secret
19
+ * SDK key, or pass `mode: "remote"`. Also rejects options that no longer
20
+ * apply: `mode: "local"` (use `"sync"` or `"file"`), `sync.file` (use
21
+ * `mode: "file", file`), and `file` without `mode: "file"`.
22
+ */
23
+ static create(options: ToggldClientOptions): Promise<ToggldClient>;
24
+ /**
25
+ * Resolves once the client has flag data to evaluate with (immediately in
26
+ * remote mode). Useful if `create` resolved on the ready timeout and you
27
+ * want to await sync explicitly. It has no timeout of its own, and it never
28
+ * resolves while the key or endpoint is wrong. Rejects if the client is
29
+ * closed first.
30
+ */
31
+ ready(): Promise<void>;
32
+ /** Send buffered counts without failing evaluations. Failed windows remain queued. */
33
+ flushTelemetry(): Promise<void>;
34
+ /** Resolve a flag, returning full details with an untyped (JSON) value. */
35
+ evaluate(key: string, defaultValue: FlagValue, context?: EvaluationContext): Promise<EvaluationDetails<FlagValue>>;
36
+ /** Resolve every flag for the environment (one request in remote mode; in-process in sync and file modes). */
37
+ evaluateAll(context?: EvaluationContext): Promise<ReadonlyArray<EvaluationDetails<FlagValue>>>;
38
+ getBooleanValue(key: string, defaultValue: boolean, context?: EvaluationContext): Promise<boolean>;
39
+ getBooleanDetails(key: string, defaultValue: boolean, context?: EvaluationContext): Promise<EvaluationDetails<boolean>>;
40
+ getStringValue(key: string, defaultValue: string, context?: EvaluationContext): Promise<string>;
41
+ getStringDetails(key: string, defaultValue: string, context?: EvaluationContext): Promise<EvaluationDetails<string>>;
42
+ getNumberValue(key: string, defaultValue: number, context?: EvaluationContext): Promise<number>;
43
+ getNumberDetails(key: string, defaultValue: number, context?: EvaluationContext): Promise<EvaluationDetails<number>>;
44
+ getObjectValue<T extends FlagValue>(key: string, defaultValue: T, context?: EvaluationContext): Promise<T>;
45
+ getObjectDetails<T extends FlagValue>(key: string, defaultValue: T, context?: EvaluationContext): Promise<EvaluationDetails<T>>;
46
+ /**
47
+ * Stop background sync, close the stream, and send remaining telemetry
48
+ * (bounded by `timeoutMs`). The client is unusable afterwards. A sync-mode
49
+ * client's stream and poll (and a file-mode client's watcher) keep the process alive until you call this, so a
50
+ * script must close its client to exit.
51
+ */
52
+ close(): Promise<void>;
53
+ }
54
+ export type { ErrorCode, EvaluationContext, EvaluationDetails, EvaluationMode, FlagMetadata, FlagValue, Reason, SyncOptions, ToggldClientOptions };