@toggld/node 0.3.0 → 0.4.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/README.md +65 -9
- package/dist/cli.d.mts +1 -1
- package/dist/cli.mjs +4 -6
- package/dist/{core-DBVGNtur.mjs → core-CezJWPV4.mjs} +99 -29
- package/dist/effect.d.mts +6 -7
- package/dist/effect.mjs +1 -2
- package/dist/index.d.mts +5 -6
- package/dist/index.mjs +6 -3
- package/dist/{src-CCPvsrli.mjs → src-B_M-Oix8.mjs} +101 -34
- package/dist/{types-CPtRV5-5.d.mts → types-hdTQWEM5.d.mts} +10 -7
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -16,9 +16,10 @@ Two evaluation modes:
|
|
|
16
16
|
evaluation" over the OFREP endpoints).
|
|
17
17
|
- **Local** (`mode: "local"`) — recommended for long-lived servers. Flag
|
|
18
18
|
definitions sync in the background (a snapshot fetch plus a live WebSocket
|
|
19
|
-
stream, with polling fallback) and every evaluation runs **in-process** on
|
|
20
|
-
the bundled engine
|
|
21
|
-
|
|
19
|
+
stream where supported, with polling fallback) and every evaluation runs **in-process** on
|
|
20
|
+
the bundled engine without a server request per evaluation. Workers currently
|
|
21
|
+
uses polling for definition updates. Cache freshness, polling intervals, and
|
|
22
|
+
network conditions affect when changed definitions arrive. Requires
|
|
22
23
|
a secret SDK key (`ffk_…`) — public browser tokens cannot read rulesets. See
|
|
23
24
|
[ADR-0012](../../docs/adr/0012-definition-sync-and-local-evaluation.md). Local
|
|
24
25
|
mode can also read its snapshot from a **file** instead of the network — see
|
|
@@ -96,12 +97,17 @@ scope.
|
|
|
96
97
|
| `timeoutMs` | `number` | `5000` | Per-request timeout. |
|
|
97
98
|
| `fetch` | `typeof fetch` | `globalThis.fetch` | Custom fetch (tests, proxies, instrumentation). |
|
|
98
99
|
|
|
99
|
-
In local mode `create`
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
100
|
+
In local mode, `create` waits for the first snapshot or `sync.readyTimeoutMs`.
|
|
101
|
+
Until a snapshot arrives, evaluations return your defaults with
|
|
102
|
+
`errorCode: "PROVIDER_NOT_READY"`, and sync keeps retrying. `client.ready()` waits
|
|
103
|
+
for the first snapshot without a timeout.
|
|
104
|
+
|
|
105
|
+
Changes on the connected Node instance normally arrive through WebSocket pushes.
|
|
106
|
+
The SDK also polls every 30 seconds, even while the stream stays healthy.
|
|
107
|
+
Polling recovers writes from other instances and announcements that the bounded
|
|
108
|
+
server buffer drops. With healthy requests, the conservative bound is
|
|
109
|
+
`sync.pollIntervalMs + 2 * timeoutMs`, plus scheduling delays, or 40 seconds by default.
|
|
110
|
+
The [Node sync guide](../../docs/node-sync.md) defines the assumptions and deployment requirements.
|
|
105
111
|
|
|
106
112
|
### How fast is local mode?
|
|
107
113
|
|
|
@@ -160,3 +166,53 @@ error, or a value whose type doesn't match the requested getter, the call
|
|
|
160
166
|
resolves to your **default value**. The `*Details` methods carry `reason:
|
|
161
167
|
"ERROR"` and an OpenFeature `errorCode` (`FLAG_NOT_FOUND`, `TYPE_MISMATCH`,
|
|
162
168
|
`TARGETING_KEY_MISSING`, `PARSE_ERROR`, `GENERAL`, …) describing what happened.
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
## Evaluation telemetry
|
|
172
|
+
|
|
173
|
+
In network-synced local mode, the SDK collects evaluation counts automatically.
|
|
174
|
+
It groups counts by flag, variant, result reason, and UTC minute. The SDK sends
|
|
175
|
+
batches to `POST /v1/telemetry` every 30 seconds and during `close()`.
|
|
176
|
+
Evaluation calls do not wait for these requests. Both SDK APIs use this reporter.
|
|
177
|
+
|
|
178
|
+
The SDK reports engine results for known flags, including engine errors.
|
|
179
|
+
It excludes unknown flags, calls before readiness, and errors from typed getters.
|
|
180
|
+
Each bulk evaluation counts each flag once. The SDK sends no user identities,
|
|
181
|
+
evaluation context, values, or metadata. Remote mode relies on server collection.
|
|
182
|
+
File-based mode sends no telemetry, even when credentials are present.
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
const client = await ToggldClient.create({
|
|
186
|
+
endpoint,
|
|
187
|
+
sdkKey,
|
|
188
|
+
mode: "local",
|
|
189
|
+
telemetry: { flushIntervalMs: 30_000 },
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
await client.getBooleanValue("checkout", false, { targetingKey: "user-123" })
|
|
193
|
+
await client.flushTelemetry()
|
|
194
|
+
await client.close()
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Set `telemetry: false` to disable reporting. Use `flushTelemetry()` to send counts
|
|
198
|
+
before the next interval. Effect users can yield `client.flushTelemetry` and close
|
|
199
|
+
the layer scope to flush remaining counts. A flush failure leaves the batch
|
|
200
|
+
queued for the next attempt. Retries keep the same batch ID to prevent duplicate
|
|
201
|
+
counts after a lost response.
|
|
202
|
+
|
|
203
|
+
The SDK holds at most 10,000 active groups and 10,000 groups awaiting retry.
|
|
204
|
+
If the active buffer fills, the SDK drops new groups. Each request contains at
|
|
205
|
+
most 1,000 groups. Each count stops at 1,000,000,000. Keys and variants longer
|
|
206
|
+
than 256 characters do not produce telemetry. Process crashes can lose buffered
|
|
207
|
+
counts. Closing the client limits its final telemetry attempt to `timeoutMs`,
|
|
208
|
+
which defaults to five seconds. Counts remain approximate.
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
The integration suite can use Postgres for the complete SDK-to-storage path.
|
|
212
|
+
It creates tenants and flags, so use a disposable database. From the repository
|
|
213
|
+
root, set `DATABASE_URL` to that database and run the command below. The test
|
|
214
|
+
server applies migrations before it starts.
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
TOGGLD_TEST_POSTGRES=1 DATABASE_URL=postgres://postgres:postgres@localhost:55439/toggld pnpm --filter @toggld/node exec vitest run test/telemetry.test.ts
|
|
218
|
+
```
|
package/dist/cli.d.mts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export {}
|
package/dist/cli.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as FlagSnapshot } from "./src-
|
|
2
|
+
import { t as FlagSnapshot } from "./src-B_M-Oix8.mjs";
|
|
3
3
|
import { Effect, Schema } from "effect";
|
|
4
4
|
import { readFile, writeFile } from "node:fs/promises";
|
|
5
|
-
|
|
5
|
+
// src/internal/cli.ts
|
|
6
6
|
/**
|
|
7
7
|
* The `toggld` CLI (ADR-0013). Today it has one command:
|
|
8
8
|
*
|
|
@@ -115,7 +115,7 @@ Options for "pull":
|
|
|
115
115
|
|
|
116
116
|
Point a local-mode client at the file with TOGGLD_FLAGS_FILE or
|
|
117
117
|
{ sync: { file: "<path>" } }.`;
|
|
118
|
-
const BOOLEAN_FLAGS = new Set([
|
|
118
|
+
const BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
|
|
119
119
|
"force",
|
|
120
120
|
"f",
|
|
121
121
|
"help",
|
|
@@ -223,8 +223,7 @@ const runMain = async (argv) => {
|
|
|
223
223
|
};
|
|
224
224
|
const asString = (v) => typeof v === "string" && v.length > 0 ? v : void 0;
|
|
225
225
|
const short = (version) => version.length > 12 ? `${version.slice(0, 12)}…` : version;
|
|
226
|
-
|
|
227
|
-
//#region src/cli.ts
|
|
226
|
+
// src/cli.ts
|
|
228
227
|
/**
|
|
229
228
|
* The `toggld` executable. All logic lives in `./internal/cli.ts` (so tests can
|
|
230
229
|
* call it without spawning a process); this is just the entry that runs it.
|
|
@@ -233,5 +232,4 @@ runMain(process.argv.slice(2)).then((code) => process.exit(code), (error) => {
|
|
|
233
232
|
console.error(error);
|
|
234
233
|
process.exit(1);
|
|
235
234
|
});
|
|
236
|
-
//#endregion
|
|
237
235
|
export {};
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { a as OfrepSuccess, i as OfrepFailure, n as SyncServerMessage, r as OfrepBulkResponse, t as FlagSnapshot } from "./src-
|
|
2
|
-
import { Context, Deferred, Duration, Effect, Layer, Schema } from "effect";
|
|
1
|
+
import { a as OfrepSuccess, i as OfrepFailure, n as SyncServerMessage, r as OfrepBulkResponse, t as FlagSnapshot } from "./src-B_M-Oix8.mjs";
|
|
2
|
+
import { Context, Deferred, Duration, Effect, Layer, Schema, Semaphore } from "effect";
|
|
3
3
|
import { readFile, watch } from "node:fs/promises";
|
|
4
4
|
import { basename, dirname } from "node:path";
|
|
5
|
-
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/
|
|
6
|
-
import { Socket } from "effect/
|
|
7
|
-
|
|
5
|
+
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
|
|
6
|
+
import { Socket } from "effect/socket";
|
|
7
|
+
// ../spec/src/index.ts
|
|
8
8
|
/** True when `value` is assignable to a flag declared as `type`. */
|
|
9
9
|
const valueMatchesType = (value, type) => {
|
|
10
10
|
switch (type) {
|
|
@@ -56,8 +56,7 @@ const Resolution = {
|
|
|
56
56
|
flagMetadata: args.flagMetadata ?? {}
|
|
57
57
|
})
|
|
58
58
|
};
|
|
59
|
-
|
|
60
|
-
//#region ../engine/src/hash.ts
|
|
59
|
+
// ../engine/src/hash.ts
|
|
61
60
|
/**
|
|
62
61
|
* MurmurHash3 (x86, 32-bit). A fast, well-distributed, non-cryptographic hash.
|
|
63
62
|
* Used to deterministically bucket a subject into a percentage rollout slice so
|
|
@@ -115,8 +114,7 @@ const BUCKET_SCALE = 1e4;
|
|
|
115
114
|
const bucketFor = (flagKey, ruleId, subject) => {
|
|
116
115
|
return murmur3(`${flagKey}:${ruleId}:${subject}`) % BUCKET_SCALE;
|
|
117
116
|
};
|
|
118
|
-
|
|
119
|
-
//#region ../engine/src/targeting.ts
|
|
117
|
+
// ../engine/src/targeting.ts
|
|
120
118
|
const asNumber = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
|
|
121
119
|
const asString = (v) => typeof v === "string" ? v : void 0;
|
|
122
120
|
/** Evaluates a single condition against the context. */
|
|
@@ -234,8 +232,7 @@ const deepEqual = (a, b) => {
|
|
|
234
232
|
}
|
|
235
233
|
return false;
|
|
236
234
|
};
|
|
237
|
-
|
|
238
|
-
//#region ../engine/src/evaluate.ts
|
|
235
|
+
// ../engine/src/evaluate.ts
|
|
239
236
|
/**
|
|
240
237
|
* Looks up a variant's value and returns a successful resolution, or an
|
|
241
238
|
* abnormal `GENERAL` resolution if the variant is not declared (a
|
|
@@ -287,8 +284,65 @@ const evaluateFlag = (flag, ctx, isMemberOf) => {
|
|
|
287
284
|
}
|
|
288
285
|
return serveVariant(flag, flag.defaultVariant, "STATIC");
|
|
289
286
|
};
|
|
290
|
-
|
|
291
|
-
|
|
287
|
+
// src/internal/telemetry.ts
|
|
288
|
+
const startTelemetry = (http, options) => Effect.gen(function* () {
|
|
289
|
+
const instanceId = crypto.randomUUID();
|
|
290
|
+
const lock = yield* Semaphore.make(1);
|
|
291
|
+
const interval = options.telemetry === false ? 3e4 : options.telemetry?.flushIntervalMs ?? 3e4;
|
|
292
|
+
const timeout = Duration.millis(options.timeoutMs ?? 5e3);
|
|
293
|
+
let cells = /* @__PURE__ */ new Map();
|
|
294
|
+
let pending = [];
|
|
295
|
+
const flush = lock.withPermit(Effect.gen(function* () {
|
|
296
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
297
|
+
if (pending.length === 0) {
|
|
298
|
+
const rollups = [...cells.values()];
|
|
299
|
+
cells = /* @__PURE__ */ new Map();
|
|
300
|
+
for (let i = 0; i < rollups.length; i += 1e3) pending.push({
|
|
301
|
+
sdkInstanceId: instanceId,
|
|
302
|
+
windowId: crypto.randomUUID(),
|
|
303
|
+
rollups: rollups.slice(i, i + 1e3)
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
while (pending.length > 0) {
|
|
307
|
+
if (!(yield* Effect.gen(function* () {
|
|
308
|
+
const response = yield* http.execute(HttpClientRequest.post("/v1/telemetry").pipe(HttpClientRequest.bodyJsonUnsafe(pending[0])));
|
|
309
|
+
return response.status >= 200 && response.status < 300;
|
|
310
|
+
}).pipe(Effect.timeout(timeout), Effect.catchCause(() => Effect.succeed(false))))) return;
|
|
311
|
+
pending.shift();
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}));
|
|
315
|
+
yield* Effect.addFinalizer(() => flush.pipe(Effect.timeout(timeout), Effect.catchCause(() => Effect.void)));
|
|
316
|
+
yield* Effect.forkScoped(Effect.forever(Effect.sleep(Duration.millis(Number.isFinite(interval) && interval > 0 ? interval : 3e4)).pipe(Effect.andThen(flush))));
|
|
317
|
+
return {
|
|
318
|
+
flush,
|
|
319
|
+
record: (flagKey, resolution) => {
|
|
320
|
+
if (flagKey.length === 0 || flagKey.length > 256 || (resolution.variant?.length ?? 0) > 256) return;
|
|
321
|
+
const variant = resolution.variant ?? "";
|
|
322
|
+
const bucketStart = (/* @__PURE__ */ new Date(Math.floor(Date.now() / 6e4) * 6e4)).toISOString();
|
|
323
|
+
const key = JSON.stringify([
|
|
324
|
+
flagKey,
|
|
325
|
+
variant,
|
|
326
|
+
resolution.reason,
|
|
327
|
+
bucketStart
|
|
328
|
+
]);
|
|
329
|
+
const cell = cells.get(key);
|
|
330
|
+
if (cell) {
|
|
331
|
+
if (cell.count < 1e9) cells.set(key, {
|
|
332
|
+
...cell,
|
|
333
|
+
count: cell.count + 1
|
|
334
|
+
});
|
|
335
|
+
} else if (cells.size < 1e4) cells.set(key, {
|
|
336
|
+
flagKey,
|
|
337
|
+
variant,
|
|
338
|
+
reason: resolution.reason,
|
|
339
|
+
bucketStart,
|
|
340
|
+
count: 1
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
// src/internal/core.ts
|
|
292
346
|
/**
|
|
293
347
|
* The Effect core of the Toggld Node SDK.
|
|
294
348
|
*
|
|
@@ -342,19 +396,19 @@ const errorDetails = (key, value, errorCode, errorMessage, flagMetadata = {}) =>
|
|
|
342
396
|
reason: "ERROR",
|
|
343
397
|
errorCode,
|
|
344
398
|
errorMessage,
|
|
345
|
-
flagMetadata
|
|
399
|
+
flagMetadata: { ...flagMetadata }
|
|
346
400
|
});
|
|
347
401
|
/** Map an engine {@link Resolution} to evaluation details, OpenFeature-style:
|
|
348
402
|
* an abnormal resolution serves the **caller's** default, not the engine's
|
|
349
403
|
* internal fallback — exactly what the remote wire does on a 400. */
|
|
350
404
|
const resolutionDetails = (key, resolution, fallback) => resolution.errorCode !== void 0 ? errorDetails(key, fallback, resolution.errorCode, resolution.errorMessage ?? "", resolution.flagMetadata) : {
|
|
351
405
|
flagKey: key,
|
|
352
|
-
value: resolution.value,
|
|
406
|
+
value: typeof resolution.value === "object" && resolution.value !== null ? structuredClone(resolution.value) : resolution.value,
|
|
353
407
|
variant: resolution.variant,
|
|
354
408
|
reason: resolution.reason,
|
|
355
409
|
errorCode: void 0,
|
|
356
410
|
errorMessage: void 0,
|
|
357
|
-
flagMetadata: resolution.flagMetadata
|
|
411
|
+
flagMetadata: { ...resolution.flagMetadata }
|
|
358
412
|
};
|
|
359
413
|
/** A readable message for an arbitrary thrown/failed value. */
|
|
360
414
|
const describe = (e) => typeof e === "object" && e !== null && "message" in e && typeof e.message === "string" ? e.message : String(e);
|
|
@@ -398,7 +452,7 @@ const toStreamUrl = (endpoint) => {
|
|
|
398
452
|
url.pathname = url.pathname.replace(/\/$/, "") + "/sync/v1/stream";
|
|
399
453
|
return url.toString();
|
|
400
454
|
};
|
|
401
|
-
const makeSnapshotStore = (mergedContext) => Effect.gen(function* () {
|
|
455
|
+
const makeSnapshotStore = (mergedContext, record) => Effect.gen(function* () {
|
|
402
456
|
let current;
|
|
403
457
|
let epoch = 0;
|
|
404
458
|
const firstSnapshot = yield* Deferred.make();
|
|
@@ -429,14 +483,20 @@ const makeSnapshotStore = (mergedContext) => Effect.gen(function* () {
|
|
|
429
483
|
const flag = state.byKey.get(key);
|
|
430
484
|
if (flag === void 0) return errorDetails(key, defaultValue, "FLAG_NOT_FOUND", `Flag '${key}' not found`);
|
|
431
485
|
const ctx = mergedContext(context);
|
|
432
|
-
|
|
486
|
+
const resolution = evaluateFlag(flag, ctx, memberOf(state, ctx));
|
|
487
|
+
record(key, resolution);
|
|
488
|
+
return resolutionDetails(key, resolution, defaultValue);
|
|
433
489
|
});
|
|
434
490
|
const evaluateAll = (context) => Effect.sync(() => {
|
|
435
491
|
const state = current;
|
|
436
492
|
if (state === void 0) return [];
|
|
437
493
|
const ctx = mergedContext(context);
|
|
438
494
|
const isMemberOf = memberOf(state, ctx);
|
|
439
|
-
return state.flags.map((flag) =>
|
|
495
|
+
return state.flags.map((flag) => {
|
|
496
|
+
const resolution = evaluateFlag(flag, ctx, isMemberOf);
|
|
497
|
+
record(flag.key, resolution);
|
|
498
|
+
return resolutionDetails(flag.key, resolution, null);
|
|
499
|
+
});
|
|
440
500
|
});
|
|
441
501
|
return {
|
|
442
502
|
adopt,
|
|
@@ -495,9 +555,14 @@ const startNetworkSource = (options, http, store, timeout) => Effect.gen(functio
|
|
|
495
555
|
openTimeout: timeout
|
|
496
556
|
});
|
|
497
557
|
yield* Effect.forever(Effect.gen(function* () {
|
|
498
|
-
yield*
|
|
558
|
+
yield* Effect.scoped(Effect.gen(function* () {
|
|
559
|
+
const pull = yield* Socket.readerString(socket);
|
|
499
560
|
consecutiveFailures = 0;
|
|
500
|
-
|
|
561
|
+
while (true) {
|
|
562
|
+
const messages = yield* pull;
|
|
563
|
+
for (const message of messages) yield* handleMessage(message);
|
|
564
|
+
}
|
|
565
|
+
})).pipe(Effect.ignore);
|
|
501
566
|
consecutiveFailures++;
|
|
502
567
|
const base = Math.min(3e4, 1e3 * 2 ** Math.min(consecutiveFailures - 1, 5));
|
|
503
568
|
yield* Effect.sleep(Duration.millis(base * (.5 + Math.random() * .5)));
|
|
@@ -517,8 +582,8 @@ const startFileSource = (path, store, options) => Effect.gen(function* () {
|
|
|
517
582
|
const decodeSnapshot = Schema.decodeUnknownEffect(FlagSnapshot);
|
|
518
583
|
let lastError;
|
|
519
584
|
const readOnce = Effect.gen(function* () {
|
|
520
|
-
const
|
|
521
|
-
const snapshot = yield* decodeSnapshot(
|
|
585
|
+
const json = yield* Effect.tryPromise(async () => JSON.parse(await readFile(path, "utf8")));
|
|
586
|
+
const snapshot = yield* decodeSnapshot(json);
|
|
522
587
|
lastError = void 0;
|
|
523
588
|
if (store.version() === snapshot.version) return;
|
|
524
589
|
yield* store.adopt(snapshot);
|
|
@@ -572,8 +637,8 @@ const startFileSource = (path, store, options) => Effect.gen(function* () {
|
|
|
572
637
|
yield* Effect.sleep(Duration.millis(500));
|
|
573
638
|
}
|
|
574
639
|
});
|
|
640
|
+
yield* Effect.forkScoped(watchLoop, { startImmediately: true });
|
|
575
641
|
yield* Effect.forkScoped(ticker);
|
|
576
|
-
yield* Effect.forkScoped(watchLoop);
|
|
577
642
|
});
|
|
578
643
|
/**
|
|
579
644
|
* Resolve a local flag-file path, or `undefined` for network sync. Highest
|
|
@@ -592,8 +657,8 @@ const resolveFilePath = (options) => {
|
|
|
592
657
|
* Assemble the local-mode evaluation surface, choosing the file source when a
|
|
593
658
|
* path resolves (env var or `sync.file`) and the network source otherwise.
|
|
594
659
|
*/
|
|
595
|
-
const makeLocal = (options, http, mergedContext, timeout) => Effect.gen(function* () {
|
|
596
|
-
const store = yield* makeSnapshotStore(mergedContext);
|
|
660
|
+
const makeLocal = (options, http, mergedContext, timeout, record) => Effect.gen(function* () {
|
|
661
|
+
const store = yield* makeSnapshotStore(mergedContext, record);
|
|
597
662
|
const filePath = resolveFilePath(options);
|
|
598
663
|
if (filePath !== void 0) yield* startFileSource(filePath, store, options);
|
|
599
664
|
else yield* startNetworkSource(options, http, store, timeout);
|
|
@@ -607,7 +672,12 @@ const make = (options) => Effect.gen(function* () {
|
|
|
607
672
|
...options.defaultContext,
|
|
608
673
|
...context
|
|
609
674
|
});
|
|
610
|
-
const
|
|
675
|
+
const local = options.mode === "local" || resolveFilePath(options) !== void 0;
|
|
676
|
+
const telemetry = local && resolveFilePath(options) === void 0 && options.telemetry !== false ? yield* startTelemetry(http, options) : {
|
|
677
|
+
record: (_key, _resolution) => {},
|
|
678
|
+
flush: Effect.void
|
|
679
|
+
};
|
|
680
|
+
const { evaluate, evaluateAll, ready } = local ? yield* makeLocal(options, http, mergedContext, timeout, telemetry.record) : makeRemote(http, mergedContext, timeout);
|
|
611
681
|
/** Resolve, then enforce the requested value type (else `TYPE_MISMATCH`). */
|
|
612
682
|
const typed = (key, defaultValue, context, type, guard) => Effect.map(evaluate(key, defaultValue, context), (d) => {
|
|
613
683
|
if (d.errorCode !== void 0) return d;
|
|
@@ -617,6 +687,7 @@ const make = (options) => Effect.gen(function* () {
|
|
|
617
687
|
const valueOf = (details) => Effect.map(details, (d) => d.value);
|
|
618
688
|
return {
|
|
619
689
|
ready,
|
|
690
|
+
flushTelemetry: telemetry.flush,
|
|
620
691
|
evaluate,
|
|
621
692
|
evaluateAll,
|
|
622
693
|
getBooleanDetails: (key, defaultValue, context) => typed(key, defaultValue, context, "boolean", isBoolean),
|
|
@@ -632,7 +703,7 @@ const make = (options) => Effect.gen(function* () {
|
|
|
632
703
|
/** The HttpClient layer, optionally backed by a caller-supplied `fetch`. */
|
|
633
704
|
const httpLayer = (fetchImpl) => fetchImpl ? FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchImpl))) : FetchHttpClient.layer;
|
|
634
705
|
/** The WebSocket constructor for the sync stream, optionally caller-supplied. */
|
|
635
|
-
const webSocketLayer = (ctor) => ctor ? Layer.succeed(Socket.WebSocketConstructor, (url,
|
|
706
|
+
const webSocketLayer = (ctor) => ctor ? Layer.succeed(Socket.WebSocketConstructor, (url, options) => new ctor(url, typeof options === "object" && !Array.isArray(options) ? options.protocols : options)) : Socket.layerWebSocketConstructorGlobal;
|
|
636
707
|
/**
|
|
637
708
|
* A self-contained `Layer` providing {@link ToggldClient} — it wires its own
|
|
638
709
|
* HTTP and WebSocket backends, so Effect users need only provide this one
|
|
@@ -640,5 +711,4 @@ const webSocketLayer = (ctor) => ctor ? Layer.succeed(Socket.WebSocketConstructo
|
|
|
640
711
|
* closing it (e.g. `ManagedRuntime.dispose`) stops sync and closes the stream.
|
|
641
712
|
*/
|
|
642
713
|
const layer = (options) => Layer.effect(ToggldClient, make(options)).pipe(Layer.provide(httpLayer(options.fetch)), Layer.provide(webSocketLayer(options.sync?.webSocketConstructor)));
|
|
643
|
-
//#endregion
|
|
644
714
|
export { layer as n, ToggldClient as t };
|
package/dist/effect.d.mts
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
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-
|
|
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-hdTQWEM5.mjs";
|
|
2
2
|
import { Context, Effect, Layer } from "effect";
|
|
3
|
-
|
|
4
|
-
//#region src/internal/core.d.ts
|
|
5
3
|
/** The evaluation surface of a Toggld client. Every method is non-failing. */
|
|
6
4
|
interface ToggldClientShape {
|
|
7
5
|
/**
|
|
@@ -10,6 +8,8 @@ interface ToggldClientShape {
|
|
|
10
8
|
* resolve to the caller's default with `PROVIDER_NOT_READY`.
|
|
11
9
|
*/
|
|
12
10
|
readonly ready: Effect.Effect<void>;
|
|
11
|
+
/** Send buffered telemetry. Failed windows remain queued for retry. */
|
|
12
|
+
readonly flushTelemetry: Effect.Effect<void>;
|
|
13
13
|
/** Resolve a flag, returning full details with an untyped (JSON) value. */
|
|
14
14
|
readonly evaluate: (key: string, defaultValue: FlagValue, context?: EvaluationContext) => Effect.Effect<EvaluationDetails<FlagValue>>;
|
|
15
15
|
/** Resolve every flag for the environment (one request in remote mode; in-process in local mode). */
|
|
@@ -28,13 +28,12 @@ declare const ToggldClient_base: Context.ServiceClass<ToggldClient, "@toggld/nod
|
|
|
28
28
|
* The Toggld client as an Effect service. Build a {@link layer} from options and
|
|
29
29
|
* compose it into your runtime, then `yield* ToggldClient` to evaluate flags.
|
|
30
30
|
*/
|
|
31
|
-
declare class ToggldClient extends ToggldClient_base {}
|
|
31
|
+
export declare class ToggldClient extends ToggldClient_base {}
|
|
32
32
|
/**
|
|
33
33
|
* A self-contained `Layer` providing {@link ToggldClient} — it wires its own
|
|
34
34
|
* HTTP and WebSocket backends, so Effect users need only provide this one
|
|
35
35
|
* layer. In local mode the background sync fibers live in the layer's scope:
|
|
36
36
|
* closing it (e.g. `ManagedRuntime.dispose`) stops sync and closes the stream.
|
|
37
37
|
*/
|
|
38
|
-
declare const layer: (options: ToggldClientOptions) => Layer.Layer<ToggldClient>;
|
|
39
|
-
|
|
40
|
-
export { type ErrorCode, type EvaluationContext, type EvaluationDetails, type EvaluationMode, type FlagMetadata, type FlagValue, type Reason, type SyncOptions, ToggldClient, type ToggldClientOptions, type ToggldClientShape, layer };
|
|
38
|
+
export declare const layer: (options: ToggldClientOptions) => Layer.Layer<ToggldClient>;
|
|
39
|
+
export type { ErrorCode, EvaluationContext, EvaluationDetails, EvaluationMode, FlagMetadata, FlagValue, Reason, SyncOptions, ToggldClientOptions, ToggldClientShape };
|
package/dist/effect.mjs
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export { ToggldClient, layer };
|
|
1
|
+
export { t as ToggldClient, n as layer } from "./core-CezJWPV4.mjs";
|
package/dist/index.d.mts
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
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-
|
|
2
|
-
|
|
3
|
-
//#region src/index.d.ts
|
|
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-hdTQWEM5.mjs";
|
|
4
2
|
/**
|
|
5
3
|
* A Toggld client backed by a private Effect runtime. Create one with {@link
|
|
6
4
|
* ToggldClient.create} and {@link ToggldClient.close} it on shutdown to release
|
|
7
5
|
* the runtime.
|
|
8
6
|
*/
|
|
9
|
-
declare class ToggldClient {
|
|
7
|
+
export declare class ToggldClient {
|
|
10
8
|
#private;
|
|
11
9
|
private constructor();
|
|
12
10
|
/**
|
|
@@ -22,6 +20,8 @@ declare class ToggldClient {
|
|
|
22
20
|
* want to await sync explicitly.
|
|
23
21
|
*/
|
|
24
22
|
ready(): Promise<void>;
|
|
23
|
+
/** Send buffered counts without failing evaluations. Failed windows remain queued. */
|
|
24
|
+
flushTelemetry(): Promise<void>;
|
|
25
25
|
/** Resolve a flag, returning full details with an untyped (JSON) value. */
|
|
26
26
|
evaluate(key: string, defaultValue: FlagValue, context?: EvaluationContext): Promise<EvaluationDetails<FlagValue>>;
|
|
27
27
|
/** Resolve every flag for the environment in one request (OFREP bulk). */
|
|
@@ -37,5 +37,4 @@ declare class ToggldClient {
|
|
|
37
37
|
/** Dispose the underlying runtime. The client is unusable afterwards. */
|
|
38
38
|
close(): Promise<void>;
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
export { type ErrorCode, type EvaluationContext, type EvaluationDetails, type EvaluationMode, type FlagMetadata, type FlagValue, type Reason, type SyncOptions, ToggldClient, type ToggldClientOptions };
|
|
40
|
+
export type { ErrorCode, EvaluationContext, EvaluationDetails, EvaluationMode, FlagMetadata, FlagValue, Reason, SyncOptions, ToggldClientOptions };
|
package/dist/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { n as layer, t as ToggldClient$1 } from "./core-
|
|
1
|
+
import { n as layer, t as ToggldClient$1 } from "./core-CezJWPV4.mjs";
|
|
2
2
|
import { Duration, Effect, ManagedRuntime } from "effect";
|
|
3
|
-
|
|
3
|
+
// src/index.ts
|
|
4
4
|
/**
|
|
5
5
|
* `@toggld/node` — the default entry: a plain Promise API.
|
|
6
6
|
*
|
|
@@ -56,6 +56,10 @@ var ToggldClient = class ToggldClient {
|
|
|
56
56
|
ready() {
|
|
57
57
|
return this.#runtime.runPromise(this.#client.ready);
|
|
58
58
|
}
|
|
59
|
+
/** Send buffered counts without failing evaluations. Failed windows remain queued. */
|
|
60
|
+
flushTelemetry() {
|
|
61
|
+
return this.#runtime.runPromise(this.#client.flushTelemetry);
|
|
62
|
+
}
|
|
59
63
|
/** Resolve a flag, returning full details with an untyped (JSON) value. */
|
|
60
64
|
evaluate(key, defaultValue, context) {
|
|
61
65
|
return this.#runtime.runPromise(this.#client.evaluate(key, defaultValue, context));
|
|
@@ -93,5 +97,4 @@ var ToggldClient = class ToggldClient {
|
|
|
93
97
|
return this.#runtime.dispose();
|
|
94
98
|
}
|
|
95
99
|
};
|
|
96
|
-
//#endregion
|
|
97
100
|
export { ToggldClient };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Schema } from "effect";
|
|
2
|
-
|
|
2
|
+
// ../schema/src/audit.ts
|
|
3
3
|
/**
|
|
4
4
|
* Audit log wire contract (ADR-0009). Every management/control-plane mutation is
|
|
5
5
|
* recorded as one append-only {@link AuditEvent}; a tenant reads its own trail
|
|
@@ -27,12 +27,14 @@ const AuditAction = Schema.Literals([
|
|
|
27
27
|
"segment.create",
|
|
28
28
|
"segment.update",
|
|
29
29
|
"segment.delete",
|
|
30
|
+
"environment.update",
|
|
30
31
|
"environment.create",
|
|
31
32
|
"environment.delete",
|
|
32
33
|
"apiKey.create",
|
|
33
34
|
"apiKey.revoke",
|
|
34
35
|
"tenant.create",
|
|
35
|
-
"tenant.update"
|
|
36
|
+
"tenant.update",
|
|
37
|
+
"tenant.delete"
|
|
36
38
|
]);
|
|
37
39
|
/** The kind of resource an event concerns; pairs with `resourceId`. */
|
|
38
40
|
const AuditResourceType = Schema.Literals([
|
|
@@ -60,8 +62,7 @@ Schema.Struct({
|
|
|
60
62
|
/** ISO 8601 timestamp the change was recorded. */
|
|
61
63
|
occurredAt: Schema.String
|
|
62
64
|
});
|
|
63
|
-
|
|
64
|
-
//#region ../schema/src/auth.ts
|
|
65
|
+
// ../schema/src/auth.ts
|
|
65
66
|
/**
|
|
66
67
|
* Authorization role carried by an API key.
|
|
67
68
|
*
|
|
@@ -73,6 +74,7 @@ Schema.Struct({
|
|
|
73
74
|
id: Schema.String,
|
|
74
75
|
name: Schema.String,
|
|
75
76
|
createdAt: Schema.String,
|
|
77
|
+
defaultEnvironmentId: Schema.optional(Schema.NullOr(Schema.String)),
|
|
76
78
|
/**
|
|
77
79
|
* Max number of flag *definitions* the tenant may hold; `null` = unlimited.
|
|
78
80
|
* The free tier is 10 (the DB column default). The flag API treats this as an
|
|
@@ -84,16 +86,26 @@ Schema.Struct({
|
|
|
84
86
|
Schema.Struct({
|
|
85
87
|
id: Schema.String,
|
|
86
88
|
tenantId: Schema.String,
|
|
87
|
-
|
|
89
|
+
isProduction: Schema.optional(Schema.Boolean),
|
|
90
|
+
isDefault: Schema.optional(Schema.Boolean),
|
|
91
|
+
/** Key, unique within the tenant (e.g. `production`). */
|
|
88
92
|
key: Schema.String,
|
|
89
93
|
name: Schema.String,
|
|
90
94
|
createdAt: Schema.String
|
|
91
95
|
});
|
|
92
96
|
/** Body for creating an environment (POST /admin/v1/tenants/:tenantId/environments). */
|
|
93
97
|
const CreateEnvironmentInput = Schema.Struct({
|
|
98
|
+
isProduction: Schema.optional(Schema.Boolean),
|
|
99
|
+
makeDefault: Schema.optional(Schema.Boolean),
|
|
94
100
|
key: Schema.String.check(Schema.isMinLength(1)),
|
|
95
101
|
name: Schema.String.check(Schema.isMinLength(1))
|
|
96
102
|
});
|
|
103
|
+
Schema.Struct({
|
|
104
|
+
key: Schema.optional(Schema.String.check(Schema.isMinLength(1))),
|
|
105
|
+
name: Schema.optional(Schema.String.check(Schema.isMinLength(1))),
|
|
106
|
+
isProduction: Schema.optional(Schema.Boolean),
|
|
107
|
+
makeDefault: Schema.optional(Schema.Boolean)
|
|
108
|
+
});
|
|
97
109
|
Schema.Struct({
|
|
98
110
|
id: Schema.String,
|
|
99
111
|
tenantId: Schema.String,
|
|
@@ -137,8 +149,7 @@ Schema.Struct({
|
|
|
137
149
|
name: Schema.optionalKey(Schema.String),
|
|
138
150
|
environment: Schema.optionalKey(Schema.String.check(Schema.isMinLength(1)))
|
|
139
151
|
});
|
|
140
|
-
|
|
141
|
-
//#region ../schema/src/value.ts
|
|
152
|
+
// ../schema/src/value.ts
|
|
142
153
|
/**
|
|
143
154
|
* The OpenFeature flag value types. Every flag declares one of these; variant
|
|
144
155
|
* values and resolved values are validated against it.
|
|
@@ -168,31 +179,39 @@ const MetadataValue = Schema.Union([
|
|
|
168
179
|
Schema.Boolean
|
|
169
180
|
]);
|
|
170
181
|
const FlagMetadata = Schema.Record(Schema.String, MetadataValue);
|
|
171
|
-
|
|
172
|
-
//#region ../schema/src/context.ts
|
|
182
|
+
// ../schema/src/context.ts
|
|
173
183
|
/**
|
|
174
184
|
* An OpenFeature evaluation context: an optional `targetingKey` "identifying
|
|
175
185
|
* the subject of the flag evaluation" (Requirement 3.1.1) plus arbitrary
|
|
176
186
|
* custom fields (Requirement 3.1.2). We accept any JSON-valued attributes.
|
|
177
187
|
*/
|
|
178
188
|
const EvaluationContext = Schema.Record(Schema.String, FlagValue);
|
|
179
|
-
Schema.
|
|
189
|
+
Schema.Error("EnvironmentConflict")({
|
|
190
|
+
_tag: Schema.tag("EnvironmentConflict"),
|
|
191
|
+
message: Schema.String
|
|
192
|
+
});
|
|
193
|
+
Schema.Error("FlagNotFound")({
|
|
180
194
|
_tag: Schema.tag("FlagNotFound"),
|
|
181
195
|
flagKey: Schema.String,
|
|
182
196
|
message: Schema.String
|
|
183
197
|
});
|
|
184
|
-
Schema.
|
|
198
|
+
Schema.Error("FlagConflict")({
|
|
199
|
+
_tag: Schema.tag("FlagConflict"),
|
|
200
|
+
flagKey: Schema.String,
|
|
201
|
+
message: Schema.String
|
|
202
|
+
});
|
|
203
|
+
Schema.Error("FlagAlreadyExists")({
|
|
185
204
|
_tag: Schema.tag("FlagAlreadyExists"),
|
|
186
205
|
flagKey: Schema.String,
|
|
187
206
|
message: Schema.String
|
|
188
207
|
});
|
|
189
|
-
Schema.
|
|
208
|
+
Schema.Error("FlagValidationError")({
|
|
190
209
|
_tag: Schema.tag("FlagValidationError"),
|
|
191
210
|
flagKey: Schema.String,
|
|
192
211
|
issues: Schema.Array(Schema.String),
|
|
193
212
|
message: Schema.String
|
|
194
213
|
});
|
|
195
|
-
Schema.
|
|
214
|
+
Schema.Error("FlagLimitExceeded")({
|
|
196
215
|
_tag: Schema.tag("FlagLimitExceeded"),
|
|
197
216
|
/** The tenant's current flag cap. */
|
|
198
217
|
limit: Schema.Int,
|
|
@@ -200,64 +219,73 @@ Schema.ErrorClass("FlagLimitExceeded")({
|
|
|
200
219
|
current: Schema.Int,
|
|
201
220
|
message: Schema.String
|
|
202
221
|
});
|
|
203
|
-
Schema.
|
|
222
|
+
Schema.Error("Unauthorized")({
|
|
204
223
|
_tag: Schema.tag("Unauthorized"),
|
|
205
224
|
message: Schema.String
|
|
206
225
|
});
|
|
207
|
-
Schema.
|
|
226
|
+
Schema.Error("Forbidden")({
|
|
208
227
|
_tag: Schema.tag("Forbidden"),
|
|
209
228
|
message: Schema.String
|
|
210
229
|
});
|
|
211
|
-
Schema.
|
|
230
|
+
Schema.Error("TenantNotFound")({
|
|
212
231
|
_tag: Schema.tag("TenantNotFound"),
|
|
213
232
|
tenantId: Schema.String,
|
|
214
233
|
message: Schema.String
|
|
215
234
|
});
|
|
216
|
-
Schema.
|
|
235
|
+
Schema.Error("TenantNotEmpty")({
|
|
236
|
+
_tag: Schema.tag("TenantNotEmpty"),
|
|
237
|
+
tenantId: Schema.String,
|
|
238
|
+
message: Schema.String
|
|
239
|
+
});
|
|
240
|
+
Schema.Error("TenantAlreadyExists")({
|
|
217
241
|
_tag: Schema.tag("TenantAlreadyExists"),
|
|
218
242
|
tenantId: Schema.String,
|
|
219
243
|
message: Schema.String
|
|
220
244
|
});
|
|
221
|
-
Schema.
|
|
245
|
+
Schema.Error("EnvironmentNotFound")({
|
|
222
246
|
_tag: Schema.tag("EnvironmentNotFound"),
|
|
223
247
|
environment: Schema.String,
|
|
224
248
|
message: Schema.String
|
|
225
249
|
});
|
|
226
|
-
Schema.
|
|
250
|
+
Schema.Error("EnvironmentAlreadyExists")({
|
|
227
251
|
_tag: Schema.tag("EnvironmentAlreadyExists"),
|
|
228
252
|
environment: Schema.String,
|
|
229
253
|
message: Schema.String
|
|
230
254
|
});
|
|
231
|
-
Schema.
|
|
255
|
+
Schema.Error("ApiKeyNotFound")({
|
|
232
256
|
_tag: Schema.tag("ApiKeyNotFound"),
|
|
233
257
|
keyId: Schema.String,
|
|
234
258
|
message: Schema.String
|
|
235
259
|
});
|
|
236
|
-
Schema.
|
|
260
|
+
Schema.Error("SegmentNotFound")({
|
|
237
261
|
_tag: Schema.tag("SegmentNotFound"),
|
|
238
262
|
segmentKey: Schema.String,
|
|
239
263
|
message: Schema.String
|
|
240
264
|
});
|
|
241
|
-
Schema.
|
|
265
|
+
Schema.Error("SegmentAlreadyExists")({
|
|
242
266
|
_tag: Schema.tag("SegmentAlreadyExists"),
|
|
243
267
|
segmentKey: Schema.String,
|
|
244
268
|
message: Schema.String
|
|
245
269
|
});
|
|
246
|
-
Schema.
|
|
270
|
+
Schema.Error("SegmentConflict")({
|
|
271
|
+
_tag: Schema.tag("SegmentConflict"),
|
|
272
|
+
segmentKey: Schema.String,
|
|
273
|
+
message: Schema.String
|
|
274
|
+
});
|
|
275
|
+
Schema.Error("SegmentValidationError")({
|
|
247
276
|
_tag: Schema.tag("SegmentValidationError"),
|
|
248
277
|
segmentKey: Schema.String,
|
|
249
278
|
issues: Schema.Array(Schema.String),
|
|
250
279
|
message: Schema.String
|
|
251
280
|
});
|
|
252
|
-
Schema.
|
|
281
|
+
Schema.Error("SegmentReferenced")({
|
|
253
282
|
_tag: Schema.tag("SegmentReferenced"),
|
|
254
283
|
segmentKey: Schema.String,
|
|
255
284
|
/** Keys of the flags whose targeting still references the segment. */
|
|
256
285
|
flagKeys: Schema.Array(Schema.String),
|
|
257
286
|
message: Schema.String
|
|
258
287
|
});
|
|
259
|
-
|
|
260
|
-
//#region ../schema/src/flag.ts
|
|
288
|
+
// ../schema/src/flag.ts
|
|
261
289
|
/**
|
|
262
290
|
* Operators usable in a targeting condition. Comparisons coerce sensibly:
|
|
263
291
|
* numeric operators require numbers, string operators require strings, and
|
|
@@ -389,10 +417,10 @@ const Segment = Schema.Struct({
|
|
|
389
417
|
const Tags = Schema.Array(Schema.String.check(Schema.isMinLength(1)));
|
|
390
418
|
Schema.Struct({
|
|
391
419
|
...Flag.fields,
|
|
392
|
-
tags: Tags
|
|
420
|
+
tags: Tags,
|
|
421
|
+
version: Schema.String
|
|
393
422
|
});
|
|
394
|
-
|
|
395
|
-
//#region ../schema/src/resolution.ts
|
|
423
|
+
// ../schema/src/resolution.ts
|
|
396
424
|
/**
|
|
397
425
|
* Resolution reasons. The provider "SHOULD populate the resolution details
|
|
398
426
|
* structure's reason field with STATIC, DEFAULT, TARGETING_MATCH, SPLIT,
|
|
@@ -445,12 +473,12 @@ const OfrepBulkResponse = Schema.Struct({
|
|
|
445
473
|
flags: Schema.Array(Schema.Union([OfrepSuccess, OfrepFailure])),
|
|
446
474
|
metadata: Schema.optionalKey(FlagMetadata)
|
|
447
475
|
});
|
|
448
|
-
Schema.
|
|
476
|
+
Schema.Error("OfrepFlagNotFound")({
|
|
449
477
|
key: Schema.String,
|
|
450
478
|
errorCode: Schema.Literal("FLAG_NOT_FOUND"),
|
|
451
479
|
errorDetails: Schema.optionalKey(Schema.String)
|
|
452
480
|
});
|
|
453
|
-
Schema.
|
|
481
|
+
Schema.Error("OfrepEvaluationError")({
|
|
454
482
|
key: Schema.String,
|
|
455
483
|
errorCode: ErrorCode,
|
|
456
484
|
errorDetails: Schema.optionalKey(Schema.String)
|
|
@@ -470,6 +498,7 @@ Schema.Struct({
|
|
|
470
498
|
tags: Schema.optionalKey(Tags)
|
|
471
499
|
});
|
|
472
500
|
Schema.Struct({
|
|
501
|
+
expectedVersion: Schema.optionalKey(Schema.String),
|
|
473
502
|
name: Schema.optionalKey(Schema.String),
|
|
474
503
|
description: Schema.optionalKey(Schema.String),
|
|
475
504
|
type: FlagType,
|
|
@@ -482,6 +511,7 @@ Schema.Struct({
|
|
|
482
511
|
tags: Schema.optionalKey(Tags)
|
|
483
512
|
});
|
|
484
513
|
Schema.Struct({
|
|
514
|
+
expectedVersion: Schema.optionalKey(Schema.String),
|
|
485
515
|
name: Schema.optionalKey(Schema.String),
|
|
486
516
|
description: Schema.optionalKey(Schema.String),
|
|
487
517
|
type: Schema.optionalKey(FlagType),
|
|
@@ -517,6 +547,8 @@ Schema.Struct({
|
|
|
517
547
|
rules: Schema.optionalKey(Schema.Array(SegmentRule))
|
|
518
548
|
});
|
|
519
549
|
Schema.Struct({
|
|
550
|
+
/** Reject a stale definition draft when supplied. */
|
|
551
|
+
expectedVersion: Schema.optionalKey(Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))),
|
|
520
552
|
name: Schema.optionalKey(Schema.String),
|
|
521
553
|
description: Schema.optionalKey(Schema.String),
|
|
522
554
|
keyAttribute: Schema.optionalKey(Schema.String),
|
|
@@ -536,8 +568,7 @@ Schema.Struct({
|
|
|
536
568
|
/** Total members matching the (optional) query — the count line in the UI. */
|
|
537
569
|
total: Schema.Number
|
|
538
570
|
});
|
|
539
|
-
|
|
540
|
-
//#region ../schema/src/sync.ts
|
|
571
|
+
// ../schema/src/sync.ts
|
|
541
572
|
/**
|
|
542
573
|
* Definition-sync wire schemas (ADR-0001 §4): the versioned full snapshot the
|
|
543
574
|
* sync surface serves to locally-evaluating SDKs, and the messages exchanged on
|
|
@@ -567,5 +598,41 @@ const SyncSnapshotMessage = Schema.Struct({
|
|
|
567
598
|
*/
|
|
568
599
|
const SyncServerMessage = Schema.Union([SyncSnapshotMessage]);
|
|
569
600
|
Schema.Struct({ type: Schema.Literal("resync") });
|
|
570
|
-
|
|
601
|
+
// ../schema/src/telemetry.ts
|
|
602
|
+
const Identifier = Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(256));
|
|
603
|
+
const EvaluationRollup = Schema.Struct({
|
|
604
|
+
flagKey: Identifier,
|
|
605
|
+
variant: Schema.String.check(Schema.isMaxLength(256)),
|
|
606
|
+
reason: Reason,
|
|
607
|
+
bucketStart: Schema.String.check(Schema.makeFilter((s) => /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00\.000Z$/.test(s) && Number.isFinite(Date.parse(s)) && new Date(s).toISOString() === s)),
|
|
608
|
+
count: Schema.Number.check(Schema.isInt(), Schema.isBetween({
|
|
609
|
+
minimum: 1,
|
|
610
|
+
maximum: 1e9
|
|
611
|
+
}))
|
|
612
|
+
});
|
|
613
|
+
Schema.Struct({
|
|
614
|
+
sdkInstanceId: Identifier,
|
|
615
|
+
windowId: Identifier,
|
|
616
|
+
rollups: Schema.Array(EvaluationRollup).check(Schema.isMinLength(1), Schema.isMaxLength(1e3))
|
|
617
|
+
});
|
|
618
|
+
Schema.Struct({
|
|
619
|
+
range: Schema.Literals([
|
|
620
|
+
"1h",
|
|
621
|
+
"24h",
|
|
622
|
+
"7d"
|
|
623
|
+
]),
|
|
624
|
+
from: Schema.String,
|
|
625
|
+
to: Schema.String,
|
|
626
|
+
total: Schema.Number,
|
|
627
|
+
errors: Schema.Number,
|
|
628
|
+
lastEvaluatedAt: Schema.NullOr(Schema.String),
|
|
629
|
+
timeline: Schema.Array(Schema.Struct({
|
|
630
|
+
bucketStart: Schema.String,
|
|
631
|
+
count: Schema.Number
|
|
632
|
+
})),
|
|
633
|
+
variants: Schema.Array(Schema.Struct({
|
|
634
|
+
variant: Schema.String,
|
|
635
|
+
count: Schema.Number
|
|
636
|
+
}))
|
|
637
|
+
});
|
|
571
638
|
export { OfrepSuccess as a, OfrepFailure as i, SyncServerMessage as n, OfrepBulkResponse as r, FlagSnapshot as t };
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
//#region ../spec/src/index.d.ts
|
|
2
1
|
/**
|
|
3
2
|
* A resolved flag value. OpenFeature permits `boolean | string | number |
|
|
4
3
|
* structure`; we model the union as JSON, which also covers arrays and null for
|
|
@@ -32,8 +31,6 @@ type Reason = "STATIC" | "DEFAULT" | "TARGETING_MATCH" | "SPLIT" | "CACHED" | "D
|
|
|
32
31
|
* @see https://openfeature.dev/specification/types
|
|
33
32
|
*/
|
|
34
33
|
type ErrorCode = "PROVIDER_NOT_READY" | "PROVIDER_FATAL" | "FLAG_NOT_FOUND" | "PARSE_ERROR" | "TYPE_MISMATCH" | "TARGETING_KEY_MISSING" | "INVALID_CONTEXT" | "GENERAL";
|
|
35
|
-
//#endregion
|
|
36
|
-
//#region src/internal/types.d.ts
|
|
37
34
|
/**
|
|
38
35
|
* How the client resolves flags.
|
|
39
36
|
*
|
|
@@ -59,9 +56,12 @@ interface SyncOptions {
|
|
|
59
56
|
*/
|
|
60
57
|
readonly transport?: "ws" | "poll";
|
|
61
58
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
59
|
+
* Delay after each snapshot poll, in milliseconds. Default `30000`.
|
|
60
|
+
* Polling stays active while the stream is healthy to recover changes from
|
|
61
|
+
* other Node instances and dropped announcements. With successful reads,
|
|
62
|
+
* allow this delay plus two request timeouts and scheduling delays.
|
|
63
|
+
* Use a positive, finite value. Until the first snapshot arrives, the client
|
|
64
|
+
* retries faster (exponential from 250 ms).
|
|
65
65
|
*/
|
|
66
66
|
readonly pollIntervalMs?: number;
|
|
67
67
|
/**
|
|
@@ -126,6 +126,10 @@ interface ToggldClientOptions {
|
|
|
126
126
|
readonly mode?: EvaluationMode;
|
|
127
127
|
/** Background sync tuning for `mode: "local"`. Ignored in remote mode. */
|
|
128
128
|
readonly sync?: SyncOptions;
|
|
129
|
+
/** Local network evaluations report grouped counts by default. Set false to disable. */
|
|
130
|
+
readonly telemetry?: false | {
|
|
131
|
+
readonly flushIntervalMs?: number;
|
|
132
|
+
};
|
|
129
133
|
/**
|
|
130
134
|
* Attributes merged into the evaluation context of **every** request, under
|
|
131
135
|
* any per-call context (per-call attributes win on key collision). Handy for
|
|
@@ -159,5 +163,4 @@ interface EvaluationDetails<T extends FlagValue> {
|
|
|
159
163
|
readonly errorMessage: string | undefined;
|
|
160
164
|
readonly flagMetadata: FlagMetadata;
|
|
161
165
|
}
|
|
162
|
-
//#endregion
|
|
163
166
|
export { ErrorCode as a, FlagValue as c, ToggldClientOptions as i, Reason as l, EvaluationMode as n, EvaluationContext as o, SyncOptions as r, FlagMetadata as s, EvaluationDetails as t };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toggld/node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Toggld SDK for Node.js — local flag evaluation with a background definition sync. Plain Promise API by default; native Effect API via @toggld/node/effect.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://toggld.io",
|
|
@@ -40,11 +40,11 @@
|
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"effect": "4.0.
|
|
43
|
+
"effect": "4.0.6"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
|
-
"tsdown": "^0.
|
|
47
|
-
"typescript": "
|
|
46
|
+
"tsdown": "^0.23.2",
|
|
47
|
+
"typescript": "6.0.3",
|
|
48
48
|
"unrun": "^0.3.1",
|
|
49
49
|
"@toggld/engine": "0.1.0",
|
|
50
50
|
"@toggld/schema": "0.1.0",
|