@toggld/node 0.1.0 → 0.3.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 +77 -5
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +237 -0
- package/dist/core-DBVGNtur.mjs +644 -0
- package/dist/effect.d.mts +12 -4
- package/dist/effect.mjs +1 -1
- package/dist/index.d.mts +14 -3
- package/dist/index.mjs +30 -10
- package/dist/{core-zMjaOru1.mjs → src-CCPvsrli.mjs} +142 -120
- package/dist/types-CPtRV5-5.d.mts +163 -0
- package/package.json +7 -1
- package/dist/types-RmWNnA7-.d.mts +0 -85
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as
|
|
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-CPtRV5-5.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/index.d.ts
|
|
4
4
|
/**
|
|
@@ -9,8 +9,19 @@ import { a as FlagMetadata, i as EvaluationContext, n as ToggldClientOptions, o
|
|
|
9
9
|
declare class ToggldClient {
|
|
10
10
|
#private;
|
|
11
11
|
private constructor();
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Create a client. Resolves once the underlying runtime is ready — and, in
|
|
14
|
+
* local mode, once the first flag snapshot has synced (or `readyTimeoutMs`,
|
|
15
|
+
* default 5 s, has elapsed; the client then serves caller defaults with
|
|
16
|
+
* `PROVIDER_NOT_READY` while sync keeps retrying in the background).
|
|
17
|
+
*/
|
|
13
18
|
static create(options: ToggldClientOptions): Promise<ToggldClient>;
|
|
19
|
+
/**
|
|
20
|
+
* Resolves once the client has flag data to evaluate with (immediately in
|
|
21
|
+
* remote mode). Useful if `create` resolved on the ready timeout and you
|
|
22
|
+
* want to await sync explicitly.
|
|
23
|
+
*/
|
|
24
|
+
ready(): Promise<void>;
|
|
14
25
|
/** Resolve a flag, returning full details with an untyped (JSON) value. */
|
|
15
26
|
evaluate(key: string, defaultValue: FlagValue, context?: EvaluationContext): Promise<EvaluationDetails<FlagValue>>;
|
|
16
27
|
/** Resolve every flag for the environment in one request (OFREP bulk). */
|
|
@@ -27,4 +38,4 @@ declare class ToggldClient {
|
|
|
27
38
|
close(): Promise<void>;
|
|
28
39
|
}
|
|
29
40
|
//#endregion
|
|
30
|
-
export { type ErrorCode, type EvaluationContext, type EvaluationDetails, type FlagMetadata, type FlagValue, type Reason, ToggldClient, type ToggldClientOptions };
|
|
41
|
+
export { type ErrorCode, type EvaluationContext, type EvaluationDetails, type EvaluationMode, type FlagMetadata, type FlagValue, type Reason, type SyncOptions, ToggldClient, type ToggldClientOptions };
|
package/dist/index.mjs
CHANGED
|
@@ -1,23 +1,27 @@
|
|
|
1
|
-
import { n as layer, t as ToggldClient$1 } from "./core-
|
|
2
|
-
import { ManagedRuntime } from "effect";
|
|
1
|
+
import { n as layer, t as ToggldClient$1 } from "./core-DBVGNtur.mjs";
|
|
2
|
+
import { Duration, Effect, ManagedRuntime } from "effect";
|
|
3
3
|
//#region src/index.ts
|
|
4
4
|
/**
|
|
5
5
|
* `@toggld/node` — the default entry: a plain Promise API.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* under a `ManagedRuntime`.
|
|
7
|
+
* Flag evaluation with **no Effect in any signature**. Internally it runs the
|
|
8
|
+
* Effect implementation from `./effect` under a `ManagedRuntime`.
|
|
10
9
|
*
|
|
11
10
|
* ```ts
|
|
12
11
|
* import { ToggldClient } from "@toggld/node"
|
|
13
12
|
*
|
|
14
|
-
*
|
|
13
|
+
* // Local evaluation (recommended for long-lived servers): flag definitions
|
|
14
|
+
* // sync in the background and every evaluation is in-process — microseconds,
|
|
15
|
+
* // zero server requests.
|
|
16
|
+
* const client = await ToggldClient.create({ sdkKey, endpoint, mode: "local" })
|
|
15
17
|
* const on = await client.getBooleanValue("new-checkout", false, { targetingKey: "user-123" })
|
|
16
18
|
* await client.close()
|
|
17
19
|
* ```
|
|
18
20
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
+
* Omitting `mode` keeps the default **remote** evaluation (each call asks the
|
|
22
|
+
* flag API over OFREP). Evaluation never throws: any failure resolves to the
|
|
23
|
+
* supplied default value, with the reason/error available via the `*Details`
|
|
24
|
+
* methods.
|
|
21
25
|
*/
|
|
22
26
|
/**
|
|
23
27
|
* A Toggld client backed by a private Effect runtime. Create one with {@link
|
|
@@ -31,10 +35,26 @@ var ToggldClient = class ToggldClient {
|
|
|
31
35
|
this.#runtime = runtime;
|
|
32
36
|
this.#client = client;
|
|
33
37
|
}
|
|
34
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* Create a client. Resolves once the underlying runtime is ready — and, in
|
|
40
|
+
* local mode, once the first flag snapshot has synced (or `readyTimeoutMs`,
|
|
41
|
+
* default 5 s, has elapsed; the client then serves caller defaults with
|
|
42
|
+
* `PROVIDER_NOT_READY` while sync keeps retrying in the background).
|
|
43
|
+
*/
|
|
35
44
|
static async create(options) {
|
|
36
45
|
const runtime = ManagedRuntime.make(layer(options));
|
|
37
|
-
|
|
46
|
+
const client = await runtime.runPromise(ToggldClient$1);
|
|
47
|
+
const readyTimeoutMs = options.sync?.readyTimeoutMs ?? 5e3;
|
|
48
|
+
await runtime.runPromise(client.ready.pipe(Effect.timeout(Duration.millis(readyTimeoutMs)), Effect.ignore));
|
|
49
|
+
return new ToggldClient(runtime, client);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Resolves once the client has flag data to evaluate with (immediately in
|
|
53
|
+
* remote mode). Useful if `create` resolved on the ready timeout and you
|
|
54
|
+
* want to await sync explicitly.
|
|
55
|
+
*/
|
|
56
|
+
ready() {
|
|
57
|
+
return this.#runtime.runPromise(this.#client.ready);
|
|
38
58
|
}
|
|
39
59
|
/** Resolve a flag, returning full details with an untyped (JSON) value. */
|
|
40
60
|
evaluate(key, defaultValue, context) {
|
|
@@ -1,16 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { FetchHttpClient, HttpClient, HttpClientRequest, HttpClientResponse } from "effect/unstable/http";
|
|
3
|
-
//#region ../spec/src/index.ts
|
|
4
|
-
/** True when `value` is assignable to a flag declared as `type`. */
|
|
5
|
-
const valueMatchesType = (value, type) => {
|
|
6
|
-
switch (type) {
|
|
7
|
-
case "boolean": return typeof value === "boolean";
|
|
8
|
-
case "string": return typeof value === "string";
|
|
9
|
-
case "number": return typeof value === "number" && Number.isFinite(value);
|
|
10
|
-
case "object": return typeof value === "object" && value !== null;
|
|
11
|
-
}
|
|
12
|
-
};
|
|
13
|
-
//#endregion
|
|
1
|
+
import { Schema } from "effect";
|
|
14
2
|
//#region ../schema/src/audit.ts
|
|
15
3
|
/**
|
|
16
4
|
* Audit log wire contract (ADR-0009). Every management/control-plane mutation is
|
|
@@ -36,6 +24,9 @@ const AuditAction = Schema.Literals([
|
|
|
36
24
|
"flag.create",
|
|
37
25
|
"flag.update",
|
|
38
26
|
"flag.delete",
|
|
27
|
+
"segment.create",
|
|
28
|
+
"segment.update",
|
|
29
|
+
"segment.delete",
|
|
39
30
|
"environment.create",
|
|
40
31
|
"environment.delete",
|
|
41
32
|
"apiKey.create",
|
|
@@ -46,6 +37,7 @@ const AuditAction = Schema.Literals([
|
|
|
46
37
|
/** The kind of resource an event concerns; pairs with `resourceId`. */
|
|
47
38
|
const AuditResourceType = Schema.Literals([
|
|
48
39
|
"flag",
|
|
40
|
+
"segment",
|
|
49
41
|
"environment",
|
|
50
42
|
"apiKey",
|
|
51
43
|
"tenant"
|
|
@@ -102,6 +94,15 @@ const CreateEnvironmentInput = Schema.Struct({
|
|
|
102
94
|
key: Schema.String.check(Schema.isMinLength(1)),
|
|
103
95
|
name: Schema.String.check(Schema.isMinLength(1))
|
|
104
96
|
});
|
|
97
|
+
Schema.Struct({
|
|
98
|
+
id: Schema.String,
|
|
99
|
+
tenantId: Schema.String,
|
|
100
|
+
/** The single environment this token is scoped to. */
|
|
101
|
+
environmentId: Schema.String,
|
|
102
|
+
/** The full token value — intentionally exposed; it is non-secret. */
|
|
103
|
+
token: Schema.String,
|
|
104
|
+
createdAt: Schema.String
|
|
105
|
+
});
|
|
105
106
|
/**
|
|
106
107
|
* Public view of an API key — safe to list. Never includes the secret token or
|
|
107
108
|
* its hash; only a non-secret prefix for human identification.
|
|
@@ -232,6 +233,29 @@ Schema.ErrorClass("ApiKeyNotFound")({
|
|
|
232
233
|
keyId: Schema.String,
|
|
233
234
|
message: Schema.String
|
|
234
235
|
});
|
|
236
|
+
Schema.ErrorClass("SegmentNotFound")({
|
|
237
|
+
_tag: Schema.tag("SegmentNotFound"),
|
|
238
|
+
segmentKey: Schema.String,
|
|
239
|
+
message: Schema.String
|
|
240
|
+
});
|
|
241
|
+
Schema.ErrorClass("SegmentAlreadyExists")({
|
|
242
|
+
_tag: Schema.tag("SegmentAlreadyExists"),
|
|
243
|
+
segmentKey: Schema.String,
|
|
244
|
+
message: Schema.String
|
|
245
|
+
});
|
|
246
|
+
Schema.ErrorClass("SegmentValidationError")({
|
|
247
|
+
_tag: Schema.tag("SegmentValidationError"),
|
|
248
|
+
segmentKey: Schema.String,
|
|
249
|
+
issues: Schema.Array(Schema.String),
|
|
250
|
+
message: Schema.String
|
|
251
|
+
});
|
|
252
|
+
Schema.ErrorClass("SegmentReferenced")({
|
|
253
|
+
_tag: Schema.tag("SegmentReferenced"),
|
|
254
|
+
segmentKey: Schema.String,
|
|
255
|
+
/** Keys of the flags whose targeting still references the segment. */
|
|
256
|
+
flagKeys: Schema.Array(Schema.String),
|
|
257
|
+
message: Schema.String
|
|
258
|
+
});
|
|
235
259
|
//#endregion
|
|
236
260
|
//#region ../schema/src/flag.ts
|
|
237
261
|
/**
|
|
@@ -287,12 +311,19 @@ const ServeRollout = Schema.Struct({
|
|
|
287
311
|
splits: Schema.Array(Split)
|
|
288
312
|
});
|
|
289
313
|
const Serve = Schema.Union([ServeVariant, ServeRollout]);
|
|
314
|
+
/** A reference to a reusable {@link Segment} from a targeting rule (ADR-0006). */
|
|
315
|
+
const SegmentRef = Schema.Struct({
|
|
316
|
+
segment: Schema.String,
|
|
317
|
+
negate: Schema.optionalKey(Schema.Boolean)
|
|
318
|
+
});
|
|
290
319
|
/** An ordered targeting rule. The first rule whose conditions all match wins. */
|
|
291
320
|
const Rule = Schema.Struct({
|
|
292
321
|
id: Schema.String,
|
|
293
322
|
description: Schema.optionalKey(Schema.String),
|
|
294
323
|
/** ANDed conditions. An empty list always matches (useful as a catch-all). */
|
|
295
324
|
conditions: Schema.Array(Condition),
|
|
325
|
+
/** Reusable segments, ANDed with `conditions` (ADR-0006). */
|
|
326
|
+
segments: Schema.optionalKey(Schema.Array(SegmentRef)),
|
|
296
327
|
serve: Serve
|
|
297
328
|
});
|
|
298
329
|
/**
|
|
@@ -320,6 +351,35 @@ const Flag = Schema.Struct({
|
|
|
320
351
|
createdAt: Schema.String,
|
|
321
352
|
updatedAt: Schema.String
|
|
322
353
|
});
|
|
354
|
+
/** How a segment's enumerated member list is distributed for evaluation. */
|
|
355
|
+
const SegmentListMode = Schema.Literals([
|
|
356
|
+
"none",
|
|
357
|
+
"inline",
|
|
358
|
+
"remote"
|
|
359
|
+
]);
|
|
360
|
+
/** A single segment membership rule: ANDed conditions, no `serve` (ADR-0006). */
|
|
361
|
+
const SegmentRule = Schema.Struct({
|
|
362
|
+
id: Schema.String,
|
|
363
|
+
description: Schema.optionalKey(Schema.String),
|
|
364
|
+
conditions: Schema.Array(Condition)
|
|
365
|
+
});
|
|
366
|
+
/**
|
|
367
|
+
* A reusable audience definition as it rides the sync bundle (ADR-0006). Inline
|
|
368
|
+
* member keys are present only for `inline` segments; `remote` segments omit
|
|
369
|
+
* them and resolve via a backing-store lookup.
|
|
370
|
+
*/
|
|
371
|
+
const Segment = Schema.Struct({
|
|
372
|
+
key: Schema.String,
|
|
373
|
+
name: Schema.optionalKey(Schema.String),
|
|
374
|
+
description: Schema.optionalKey(Schema.String),
|
|
375
|
+
keyAttribute: Schema.String,
|
|
376
|
+
rules: Schema.Array(SegmentRule),
|
|
377
|
+
listMode: SegmentListMode,
|
|
378
|
+
members: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
379
|
+
version: Schema.Number,
|
|
380
|
+
createdAt: Schema.String,
|
|
381
|
+
updatedAt: Schema.String
|
|
382
|
+
});
|
|
323
383
|
/**
|
|
324
384
|
* Freeform organization labels for a flag (ADR-0008). A flag may carry any
|
|
325
385
|
* number; they group/filter flags in the dashboard and have **no** effect on
|
|
@@ -433,117 +493,79 @@ Schema.Struct({
|
|
|
433
493
|
metadata: Schema.optionalKey(FlagMetadata),
|
|
434
494
|
tags: Schema.optionalKey(Tags)
|
|
435
495
|
});
|
|
496
|
+
Schema.Struct({
|
|
497
|
+
key: Schema.String,
|
|
498
|
+
name: Schema.optionalKey(Schema.String),
|
|
499
|
+
description: Schema.optionalKey(Schema.String),
|
|
500
|
+
keyAttribute: Schema.String,
|
|
501
|
+
rules: Schema.Array(SegmentRule),
|
|
502
|
+
listMode: Segment.fields.listMode,
|
|
503
|
+
/** Number of enumerated members in the list (0 for a rules-only segment). */
|
|
504
|
+
memberCount: Schema.Number,
|
|
505
|
+
version: Schema.Number,
|
|
506
|
+
/** Keys of the flags whose targeting references this segment, in any environment. */
|
|
507
|
+
usedBy: Schema.Array(Schema.String),
|
|
508
|
+
createdAt: Schema.String,
|
|
509
|
+
updatedAt: Schema.String
|
|
510
|
+
});
|
|
511
|
+
Schema.Struct({
|
|
512
|
+
key: Schema.String.check(Schema.isMinLength(1)),
|
|
513
|
+
name: Schema.optionalKey(Schema.String),
|
|
514
|
+
description: Schema.optionalKey(Schema.String),
|
|
515
|
+
/** Defaults to `targetingKey` server-side when omitted. */
|
|
516
|
+
keyAttribute: Schema.optionalKey(Schema.String),
|
|
517
|
+
rules: Schema.optionalKey(Schema.Array(SegmentRule))
|
|
518
|
+
});
|
|
519
|
+
Schema.Struct({
|
|
520
|
+
name: Schema.optionalKey(Schema.String),
|
|
521
|
+
description: Schema.optionalKey(Schema.String),
|
|
522
|
+
keyAttribute: Schema.optionalKey(Schema.String),
|
|
523
|
+
rules: Schema.optionalKey(Schema.Array(SegmentRule))
|
|
524
|
+
});
|
|
525
|
+
Schema.Struct({
|
|
526
|
+
add: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
527
|
+
remove: Schema.optionalKey(Schema.Array(Schema.String))
|
|
528
|
+
});
|
|
529
|
+
/** One enumerated member of a segment's list. */
|
|
530
|
+
const SegmentMember = Schema.Struct({
|
|
531
|
+
key: Schema.String,
|
|
532
|
+
addedAt: Schema.String
|
|
533
|
+
});
|
|
534
|
+
Schema.Struct({
|
|
535
|
+
members: Schema.Array(SegmentMember),
|
|
536
|
+
/** Total members matching the (optional) query — the count line in the UI. */
|
|
537
|
+
total: Schema.Number
|
|
538
|
+
});
|
|
436
539
|
//#endregion
|
|
437
|
-
//#region src/
|
|
540
|
+
//#region ../schema/src/sync.ts
|
|
438
541
|
/**
|
|
439
|
-
*
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
* a thin facade that runs this under a `ManagedRuntime`. The SDK performs **no
|
|
444
|
-
* local evaluation** — every call hits the flag API, which evaluates the flag
|
|
445
|
-
* against the supplied context and returns the resolved value (OpenFeature
|
|
446
|
-
* "remote evaluation"). Local in-process evaluation via `@toggld/engine` is a
|
|
447
|
-
* later milestone (see ADR-0001).
|
|
542
|
+
* Definition-sync wire schemas (ADR-0001 §4): the versioned full snapshot the
|
|
543
|
+
* sync surface serves to locally-evaluating SDKs, and the messages exchanged on
|
|
544
|
+
* the live sync stream. Like the OFREP schemas, these are shared by the server
|
|
545
|
+
* (producer) and `@toggld/node` (consumer) so the contract cannot drift.
|
|
448
546
|
*
|
|
449
|
-
*
|
|
450
|
-
*
|
|
451
|
-
* match the requested getter — resolves to the caller's default value with the
|
|
452
|
-
* details carrying `reason: "ERROR"` and an `errorCode`.
|
|
547
|
+
* `version` is opaque and equality-compared (ADR-0005 phase 1); the ordered
|
|
548
|
+
* `config_version` lands in this same field later without a wire change.
|
|
453
549
|
*/
|
|
454
|
-
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
});
|
|
468
|
-
const wireErrorDetails = (fallback, e) => ({
|
|
469
|
-
flagKey: e.key,
|
|
470
|
-
value: fallback,
|
|
471
|
-
variant: void 0,
|
|
472
|
-
reason: "ERROR",
|
|
473
|
-
errorCode: e.errorCode,
|
|
474
|
-
errorMessage: e.errorDetails,
|
|
475
|
-
flagMetadata: e.metadata ?? {}
|
|
476
|
-
});
|
|
477
|
-
const errorDetails = (key, value, errorCode, errorMessage, flagMetadata = {}) => ({
|
|
478
|
-
flagKey: key,
|
|
479
|
-
value,
|
|
480
|
-
variant: void 0,
|
|
481
|
-
reason: "ERROR",
|
|
482
|
-
errorCode,
|
|
483
|
-
errorMessage,
|
|
484
|
-
flagMetadata
|
|
485
|
-
});
|
|
486
|
-
/** A readable message for an arbitrary thrown/failed value. */
|
|
487
|
-
const describe = (e) => typeof e === "object" && e !== null && "message" in e && typeof e.message === "string" ? e.message : String(e);
|
|
488
|
-
/** Map a transport/decoding failure to an OpenFeature error code. */
|
|
489
|
-
const codeFor = (e) => typeof e === "object" && e !== null && "_tag" in e && e._tag === "SchemaError" ? "PARSE_ERROR" : "GENERAL";
|
|
490
|
-
/**
|
|
491
|
-
* The Toggld client as an Effect service. Build a {@link layer} from options and
|
|
492
|
-
* compose it into your runtime, then `yield* ToggldClient` to evaluate flags.
|
|
493
|
-
*/
|
|
494
|
-
var ToggldClient = class extends Context.Service()("@toggld/node/ToggldClient") {};
|
|
495
|
-
const make = (options) => Effect.gen(function* () {
|
|
496
|
-
const base = yield* HttpClient.HttpClient;
|
|
497
|
-
const timeout = Duration.millis(options.timeoutMs ?? DEFAULT_TIMEOUT_MS);
|
|
498
|
-
const http = base.pipe(HttpClient.mapRequest(HttpClientRequest.prependUrl(options.endpoint)), HttpClient.mapRequest(HttpClientRequest.bearerToken(options.sdkKey)));
|
|
499
|
-
const mergedContext = (context) => ({
|
|
500
|
-
...options.defaultContext,
|
|
501
|
-
...context
|
|
502
|
-
});
|
|
503
|
-
const decode = HttpClientResponse.schemaBodyJson;
|
|
504
|
-
const evaluate = (key, defaultValue, context) => Effect.gen(function* () {
|
|
505
|
-
const request = HttpClientRequest.post(`/ofrep/v1/evaluate/flags/${encodeURIComponent(key)}`).pipe(HttpClientRequest.bodyJsonUnsafe({ context: mergedContext(context) }));
|
|
506
|
-
const response = yield* http.execute(request);
|
|
507
|
-
return yield* HttpClientResponse.matchStatus(response, {
|
|
508
|
-
"2xx": (r) => Effect.map(decode(OfrepSuccess)(r), (s) => okDetails(key, s)),
|
|
509
|
-
"4xx": (r) => Effect.map(decode(OfrepFailure)(r), (e) => wireErrorDetails(defaultValue, e)),
|
|
510
|
-
orElse: (r) => Effect.succeed(errorDetails(key, defaultValue, "GENERAL", `Unexpected response status ${r.status}`))
|
|
511
|
-
});
|
|
512
|
-
}).pipe(Effect.timeout(timeout), Effect.catch((error) => Effect.succeed(errorDetails(key, defaultValue, codeFor(error), describe(error)))));
|
|
513
|
-
const evaluateAll = (context) => Effect.gen(function* () {
|
|
514
|
-
const request = HttpClientRequest.post("/ofrep/v1/evaluate/flags").pipe(HttpClientRequest.bodyJsonUnsafe({ context: mergedContext(context) }));
|
|
515
|
-
const response = yield* http.execute(request);
|
|
516
|
-
return (yield* HttpClientResponse.matchStatus(response, {
|
|
517
|
-
"2xx": (r) => decode(OfrepBulkResponse)(r),
|
|
518
|
-
orElse: (r) => Effect.fail(/* @__PURE__ */ new Error(`Unexpected response status ${r.status}`))
|
|
519
|
-
})).flags.map((entry) => "errorCode" in entry ? wireErrorDetails(null, entry) : okDetails(entry.key, entry));
|
|
520
|
-
}).pipe(Effect.timeout(timeout), Effect.catch(() => Effect.succeed([])));
|
|
521
|
-
/** Resolve, then enforce the requested value type (else `TYPE_MISMATCH`). */
|
|
522
|
-
const typed = (key, defaultValue, context, type, guard) => Effect.map(evaluate(key, defaultValue, context), (d) => {
|
|
523
|
-
if (d.errorCode !== void 0) return d;
|
|
524
|
-
if (guard(d.value)) return d;
|
|
525
|
-
return errorDetails(key, defaultValue, "TYPE_MISMATCH", `Flag '${key}' value is not of type ${type}`, d.flagMetadata);
|
|
526
|
-
});
|
|
527
|
-
const valueOf = (details) => Effect.map(details, (d) => d.value);
|
|
528
|
-
return {
|
|
529
|
-
evaluate,
|
|
530
|
-
evaluateAll,
|
|
531
|
-
getBooleanDetails: (key, defaultValue, context) => typed(key, defaultValue, context, "boolean", isBoolean),
|
|
532
|
-
getBooleanValue: (key, defaultValue, context) => valueOf(typed(key, defaultValue, context, "boolean", isBoolean)),
|
|
533
|
-
getStringDetails: (key, defaultValue, context) => typed(key, defaultValue, context, "string", isString),
|
|
534
|
-
getStringValue: (key, defaultValue, context) => valueOf(typed(key, defaultValue, context, "string", isString)),
|
|
535
|
-
getNumberDetails: (key, defaultValue, context) => typed(key, defaultValue, context, "number", isNumber),
|
|
536
|
-
getNumberValue: (key, defaultValue, context) => valueOf(typed(key, defaultValue, context, "number", isNumber)),
|
|
537
|
-
getObjectDetails: (key, defaultValue, context) => typed(key, defaultValue, context, "object", isObject),
|
|
538
|
-
getObjectValue: (key, defaultValue, context) => valueOf(typed(key, defaultValue, context, "object", isObject))
|
|
539
|
-
};
|
|
540
|
-
});
|
|
541
|
-
/** The HttpClient layer, optionally backed by a caller-supplied `fetch`. */
|
|
542
|
-
const httpLayer = (fetchImpl) => fetchImpl ? FetchHttpClient.layer.pipe(Layer.provide(Layer.succeed(FetchHttpClient.Fetch, fetchImpl))) : FetchHttpClient.layer;
|
|
550
|
+
/** A versioned full snapshot of one environment's resolved flag set. */
|
|
551
|
+
const FlagSnapshot = Schema.Struct({
|
|
552
|
+
version: Schema.String,
|
|
553
|
+
flags: Schema.Array(Flag),
|
|
554
|
+
/** Segment definitions referenced by the flags (ADR-0006). */
|
|
555
|
+
segments: Schema.optionalKey(Schema.Array(Segment))
|
|
556
|
+
});
|
|
557
|
+
/** Server→client: the current snapshot (connect greeting and change pushes). */
|
|
558
|
+
const SyncSnapshotMessage = Schema.Struct({
|
|
559
|
+
type: Schema.Literal("snapshot"),
|
|
560
|
+
version: Schema.String,
|
|
561
|
+
flags: Schema.Array(Flag),
|
|
562
|
+
segments: Schema.optionalKey(Schema.Array(Segment))
|
|
563
|
+
});
|
|
543
564
|
/**
|
|
544
|
-
*
|
|
545
|
-
*
|
|
565
|
+
* Every server→client sync-stream message. A single-member union today; the
|
|
566
|
+
* `type` discriminant is the extension point.
|
|
546
567
|
*/
|
|
547
|
-
const
|
|
568
|
+
const SyncServerMessage = Schema.Union([SyncSnapshotMessage]);
|
|
569
|
+
Schema.Struct({ type: Schema.Literal("resync") });
|
|
548
570
|
//#endregion
|
|
549
|
-
export {
|
|
571
|
+
export { OfrepSuccess as a, OfrepFailure as i, SyncServerMessage as n, OfrepBulkResponse as r, FlagSnapshot as t };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
//#region ../spec/src/index.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* A resolved flag value. OpenFeature permits `boolean | string | number |
|
|
4
|
+
* structure`; we model the union as JSON, which also covers arrays and null for
|
|
5
|
+
* "object" typed flags.
|
|
6
|
+
*/
|
|
7
|
+
type FlagValue = null | boolean | number | string | ReadonlyArray<FlagValue> | {
|
|
8
|
+
readonly [key: string]: FlagValue;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Flag metadata: "arbitrary properties, with keys of type string, and values of
|
|
12
|
+
* type boolean, string, or number." (Requirement 2.2.10)
|
|
13
|
+
*/
|
|
14
|
+
type MetadataValue = string | number | boolean;
|
|
15
|
+
type FlagMetadata = {
|
|
16
|
+
readonly [key: string]: MetadataValue;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* An OpenFeature evaluation context: an optional `targetingKey` identifying the
|
|
20
|
+
* subject (Requirement 3.1.1) plus arbitrary custom JSON-valued attributes
|
|
21
|
+
* (Requirement 3.1.2).
|
|
22
|
+
*/
|
|
23
|
+
type EvaluationContext = {
|
|
24
|
+
readonly [attribute: string]: FlagValue;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Resolution reasons. (Requirement 2.2.5)
|
|
28
|
+
*/
|
|
29
|
+
type Reason = "STATIC" | "DEFAULT" | "TARGETING_MATCH" | "SPLIT" | "CACHED" | "DISABLED" | "UNKNOWN" | "STALE" | "ERROR";
|
|
30
|
+
/**
|
|
31
|
+
* OpenFeature error codes.
|
|
32
|
+
* @see https://openfeature.dev/specification/types
|
|
33
|
+
*/
|
|
34
|
+
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
|
+
/**
|
|
38
|
+
* How the client resolves flags.
|
|
39
|
+
*
|
|
40
|
+
* - `"remote"` — every evaluation is an HTTP request to the flag API's OFREP
|
|
41
|
+
* endpoints; the server evaluates and returns the value.
|
|
42
|
+
* - `"local"` — the client backgrounds a **definition sync** (a snapshot fetch
|
|
43
|
+
* plus a live WebSocket stream with polling fallback) and evaluates flags
|
|
44
|
+
* **in-process** with the bundled engine: zero server requests per
|
|
45
|
+
* evaluation, microsecond latency, identical resolutions to the server. The
|
|
46
|
+
* mode for long-lived servers.
|
|
47
|
+
*/
|
|
48
|
+
type EvaluationMode = "remote" | "local";
|
|
49
|
+
/** Tuning for the background definition sync (`mode: "local"`). */
|
|
50
|
+
interface SyncOptions {
|
|
51
|
+
/**
|
|
52
|
+
* How updates reach the client.
|
|
53
|
+
*
|
|
54
|
+
* - `"ws"` (default) — a live WebSocket stream (`/sync/v1/stream`) pushes a
|
|
55
|
+
* fresh snapshot on every change, with the snapshot poll below as a
|
|
56
|
+
* safety net (it also serves runtimes where the stream is unavailable —
|
|
57
|
+
* the client degrades to polling automatically).
|
|
58
|
+
* - `"poll"` — no stream; only the periodic snapshot poll.
|
|
59
|
+
*/
|
|
60
|
+
readonly transport?: "ws" | "poll";
|
|
61
|
+
/**
|
|
62
|
+
* Interval between snapshot polls in milliseconds. Default `30000`. Until
|
|
63
|
+
* the first snapshot arrives, the client retries faster (exponential from
|
|
64
|
+
* 250 ms) so cold starts aren't gated on this interval.
|
|
65
|
+
*/
|
|
66
|
+
readonly pollIntervalMs?: number;
|
|
67
|
+
/**
|
|
68
|
+
* How long `ToggldClient.create` waits for the first snapshot before
|
|
69
|
+
* resolving anyway, in milliseconds. Default `5000`. A client that isn't
|
|
70
|
+
* ready evaluates to caller defaults (`PROVIDER_NOT_READY`) while sync keeps
|
|
71
|
+
* retrying in the background.
|
|
72
|
+
*/
|
|
73
|
+
readonly readyTimeoutMs?: number;
|
|
74
|
+
/**
|
|
75
|
+
* Custom `WebSocket` constructor for the stream transport. Defaults to the
|
|
76
|
+
* global `WebSocket` (Node ≥ 22). Useful for tests or proxies.
|
|
77
|
+
*/
|
|
78
|
+
readonly webSocketConstructor?: typeof globalThis.WebSocket;
|
|
79
|
+
/**
|
|
80
|
+
* Evaluate from a local snapshot **file** instead of syncing over the network
|
|
81
|
+
* (ADR-0013) — for local development, offline runs, CI, or pinning a known
|
|
82
|
+
* ruleset. The file is a `FlagSnapshot` on disk; generate one with `npx
|
|
83
|
+
* @toggld/node pull`. A file-backed client makes no network requests and needs
|
|
84
|
+
* no reachable endpoint, key, or stream.
|
|
85
|
+
*
|
|
86
|
+
* The path is resolved highest-precedence first:
|
|
87
|
+
*
|
|
88
|
+
* 1. the `TOGGLD_FLAGS_FILE` environment variable (always wins, so a deployed
|
|
89
|
+
* app can be redirected at a file with no code change),
|
|
90
|
+
* 2. this option when given a string path,
|
|
91
|
+
* 3. `./toggld.flags.json` when this is `true`.
|
|
92
|
+
*
|
|
93
|
+
* The file is **watched**: edits are adopted live, no restart. The poll
|
|
94
|
+
* interval above doubles as a re-read safety net (and picks the file up if it
|
|
95
|
+
* is created after the client starts).
|
|
96
|
+
*/
|
|
97
|
+
readonly file?: string | boolean;
|
|
98
|
+
}
|
|
99
|
+
/** Configuration for a {@link https://toggld.io Toggld} client. */
|
|
100
|
+
interface ToggldClientOptions {
|
|
101
|
+
/**
|
|
102
|
+
* An **evaluation** (or management) API key, sent as `Authorization: Bearer
|
|
103
|
+
* <sdkKey>`. The key is scoped to one tenant + environment server-side, so a
|
|
104
|
+
* client always evaluates against exactly that environment. Network local
|
|
105
|
+
* evaluation requires a secret key (`ffk_…`) — public browser tokens cannot
|
|
106
|
+
* read flag definitions.
|
|
107
|
+
*
|
|
108
|
+
* Optional **only** for a file-backed client ({@link SyncOptions.file}), which
|
|
109
|
+
* reads its snapshot from disk and never authenticates. Required for remote
|
|
110
|
+
* and network-synced local evaluation.
|
|
111
|
+
*/
|
|
112
|
+
readonly sdkKey?: string;
|
|
113
|
+
/**
|
|
114
|
+
* Base URL of the flag API, e.g. `https://toggld-api.example.workers.dev` or
|
|
115
|
+
* `http://localhost:3000` in local development. No trailing path — the client
|
|
116
|
+
* appends the OFREP and sync routes (`/ofrep/v1/...`, `/sync/v1/...`).
|
|
117
|
+
*
|
|
118
|
+
* Optional **only** for a file-backed client ({@link SyncOptions.file}); it
|
|
119
|
+
* makes no requests. Required for remote and network-synced local evaluation.
|
|
120
|
+
*/
|
|
121
|
+
readonly endpoint?: string;
|
|
122
|
+
/**
|
|
123
|
+
* Evaluation mode. Defaults to `"remote"`; prefer `"local"` on long-lived
|
|
124
|
+
* servers. See {@link EvaluationMode}.
|
|
125
|
+
*/
|
|
126
|
+
readonly mode?: EvaluationMode;
|
|
127
|
+
/** Background sync tuning for `mode: "local"`. Ignored in remote mode. */
|
|
128
|
+
readonly sync?: SyncOptions;
|
|
129
|
+
/**
|
|
130
|
+
* Attributes merged into the evaluation context of **every** request, under
|
|
131
|
+
* any per-call context (per-call attributes win on key collision). Handy for
|
|
132
|
+
* process-wide dimensions such as `region` or `serviceVersion`.
|
|
133
|
+
*/
|
|
134
|
+
readonly defaultContext?: EvaluationContext;
|
|
135
|
+
/** Per-request timeout in milliseconds. Default `5000`. */
|
|
136
|
+
readonly timeoutMs?: number;
|
|
137
|
+
/**
|
|
138
|
+
* Custom `fetch` implementation. Defaults to the global `fetch`. Useful for
|
|
139
|
+
* tests, proxies, or injecting retries/instrumentation.
|
|
140
|
+
*/
|
|
141
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* The outcome of evaluating a single flag — OpenFeature "evaluation details"
|
|
145
|
+
* with a value typed to the requested flag type. Mirrors {@link
|
|
146
|
+
* https://openfeature.dev/specification/sections/evaluation-details}.
|
|
147
|
+
*
|
|
148
|
+
* On any abnormal evaluation (network failure, unknown flag, type mismatch, a
|
|
149
|
+
* server-reported error) the call still resolves: `value` holds the caller's
|
|
150
|
+
* default, `reason` is `"ERROR"`, and `errorCode`/`errorMessage` describe what
|
|
151
|
+
* went wrong. Evaluation never throws.
|
|
152
|
+
*/
|
|
153
|
+
interface EvaluationDetails<T extends FlagValue> {
|
|
154
|
+
readonly flagKey: string;
|
|
155
|
+
readonly value: T;
|
|
156
|
+
readonly variant: string | undefined;
|
|
157
|
+
readonly reason: Reason;
|
|
158
|
+
readonly errorCode: ErrorCode | undefined;
|
|
159
|
+
readonly errorMessage: string | undefined;
|
|
160
|
+
readonly flagMetadata: FlagMetadata;
|
|
161
|
+
}
|
|
162
|
+
//#endregion
|
|
163
|
+
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.3.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",
|
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
"files": [
|
|
23
23
|
"dist"
|
|
24
24
|
],
|
|
25
|
+
"bin": {
|
|
26
|
+
"toggld": "./dist/cli.mjs"
|
|
27
|
+
},
|
|
25
28
|
"exports": {
|
|
26
29
|
"./package.json": "./package.json",
|
|
27
30
|
".": {
|
|
@@ -41,8 +44,11 @@
|
|
|
41
44
|
},
|
|
42
45
|
"devDependencies": {
|
|
43
46
|
"tsdown": "^0.22.2",
|
|
47
|
+
"typescript": "5.8.3",
|
|
44
48
|
"unrun": "^0.3.1",
|
|
49
|
+
"@toggld/engine": "0.1.0",
|
|
45
50
|
"@toggld/schema": "0.1.0",
|
|
51
|
+
"@toggld/server": "0.1.0",
|
|
46
52
|
"@toggld/spec": "0.1.0"
|
|
47
53
|
},
|
|
48
54
|
"scripts": {
|