@lunora/flags 1.0.0-alpha.11 → 1.0.0-alpha.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
export { defineFlags, isFlagsDefinition } from './packem_shared/defineFlags-DAIO8cWQ.mjs';
|
|
2
|
-
export { createFlags } from './packem_shared/createFlags-
|
|
2
|
+
export { createFlags } from './packem_shared/createFlags-DGosFql2.mjs';
|
|
@@ -1,6 +1,45 @@
|
|
|
1
1
|
import { LunoraError } from '@lunora/errors';
|
|
2
2
|
import { OpenFeature, ErrorCode } from '@openfeature/server-sdk';
|
|
3
3
|
|
|
4
|
+
const compareKeys = (a, b) => {
|
|
5
|
+
if (a < b) {
|
|
6
|
+
return -1;
|
|
7
|
+
}
|
|
8
|
+
return a > b ? 1 : 0;
|
|
9
|
+
};
|
|
10
|
+
const stableStringify = (value) => {
|
|
11
|
+
if (value === void 0) {
|
|
12
|
+
return "null";
|
|
13
|
+
}
|
|
14
|
+
if (typeof value === "bigint") {
|
|
15
|
+
throw new TypeError("stableStringify: cannot use a bigint in a stable JSON cache key — pass it as a string, or use stableWireKey");
|
|
16
|
+
}
|
|
17
|
+
if (value === null || typeof value !== "object") {
|
|
18
|
+
return JSON.stringify(value);
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
22
|
+
}
|
|
23
|
+
const proto = Object.getPrototypeOf(value);
|
|
24
|
+
if (proto !== null && proto !== Object.prototype) {
|
|
25
|
+
const name = value.constructor?.name ?? "value";
|
|
26
|
+
throw new TypeError(
|
|
27
|
+
`stableStringify: cannot use a ${name} in a stable JSON cache key — only plain objects, arrays, and JSON primitives are supported (wire-typed values key via stableWireKey)`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const record = value;
|
|
31
|
+
const keys = Object.keys(record).toSorted(compareKeys);
|
|
32
|
+
const parts = [];
|
|
33
|
+
for (const key of keys) {
|
|
34
|
+
const raw = record[key];
|
|
35
|
+
if (raw === void 0) {
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
parts.push(`${JSON.stringify(key)}:${stableStringify(raw)}`);
|
|
39
|
+
}
|
|
40
|
+
return `{${parts.join(",")}}`;
|
|
41
|
+
};
|
|
42
|
+
|
|
4
43
|
const DOMAIN = "lunora";
|
|
5
44
|
let clientBinding;
|
|
6
45
|
const bindClient = ({ hooks, logger, provider }) => {
|
|
@@ -27,13 +66,11 @@ const resetFlags = async () => {
|
|
|
27
66
|
await OpenFeature.clearProviders();
|
|
28
67
|
};
|
|
29
68
|
const memoKey = (type, flagKey, defaultValue, context) => {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return `${prefix}:[]`;
|
|
69
|
+
const prefix = stableStringify([type, flagKey, defaultValue]);
|
|
70
|
+
if (Object.keys(context).length === 0) {
|
|
71
|
+
return `${prefix}:{}`;
|
|
34
72
|
}
|
|
35
|
-
|
|
36
|
-
return `${prefix}:${JSON.stringify(entries)}`;
|
|
73
|
+
return `${prefix}:${stableStringify(context)}`;
|
|
37
74
|
};
|
|
38
75
|
const resolveDetails = (client, type, flagKey, defaultValue, context) => {
|
|
39
76
|
switch (type) {
|
|
@@ -65,12 +102,7 @@ const createFlags = (options) => {
|
|
|
65
102
|
const memo = /* @__PURE__ */ new Map();
|
|
66
103
|
const evaluate = (type, flagKey, defaultValue, context) => {
|
|
67
104
|
const merged = resolvedTargetingKey === void 0 ? { ...context } : { targetingKey: resolvedTargetingKey, ...context };
|
|
68
|
-
const
|
|
69
|
-
const cached = memo.get(key);
|
|
70
|
-
if (cached) {
|
|
71
|
-
return cached;
|
|
72
|
-
}
|
|
73
|
-
const pending = bindClient({ hooks, logger, provider }).then((client) => resolveDetails(client, type, flagKey, defaultValue, merged)).catch((error) => {
|
|
105
|
+
const failClosed = (error) => {
|
|
74
106
|
return {
|
|
75
107
|
errorCode: ErrorCode.GENERAL,
|
|
76
108
|
errorMessage: error instanceof Error ? error.message : String(error),
|
|
@@ -79,7 +111,19 @@ const createFlags = (options) => {
|
|
|
79
111
|
reason: "ERROR",
|
|
80
112
|
value: defaultValue
|
|
81
113
|
};
|
|
82
|
-
}
|
|
114
|
+
};
|
|
115
|
+
const run = () => bindClient({ hooks, logger, provider }).then((client) => resolveDetails(client, type, flagKey, defaultValue, merged)).catch(failClosed);
|
|
116
|
+
let key;
|
|
117
|
+
try {
|
|
118
|
+
key = memoKey(type, flagKey, defaultValue, merged);
|
|
119
|
+
} catch {
|
|
120
|
+
return run();
|
|
121
|
+
}
|
|
122
|
+
const cached = memo.get(key);
|
|
123
|
+
if (cached) {
|
|
124
|
+
return cached;
|
|
125
|
+
}
|
|
126
|
+
const pending = run();
|
|
83
127
|
memo.set(key, pending);
|
|
84
128
|
return pending;
|
|
85
129
|
};
|
package/dist/providers/env.mjs
CHANGED
|
@@ -37,7 +37,7 @@ const envProvider = (options = {}) => {
|
|
|
37
37
|
if (FALSE_TOKENS.has(token)) {
|
|
38
38
|
return staticDetails(false);
|
|
39
39
|
}
|
|
40
|
-
return parseError(defaultValue, `env flag "${flagKey}" (${nameOf(flagKey)}) is not a boolean
|
|
40
|
+
return parseError(defaultValue, `env flag "${flagKey}" (${nameOf(flagKey)}) value is not a recognized boolean`);
|
|
41
41
|
},
|
|
42
42
|
resolveNumberEvaluation: (flagKey, defaultValue) => {
|
|
43
43
|
const value = raw(flagKey);
|
|
@@ -46,7 +46,7 @@ const envProvider = (options = {}) => {
|
|
|
46
46
|
}
|
|
47
47
|
const parsed = Number(value);
|
|
48
48
|
if (value.trim() === "" || Number.isNaN(parsed)) {
|
|
49
|
-
return parseError(defaultValue, `env flag "${flagKey}" (${nameOf(flagKey)}) is not a number
|
|
49
|
+
return parseError(defaultValue, `env flag "${flagKey}" (${nameOf(flagKey)}) value is not a number`);
|
|
50
50
|
}
|
|
51
51
|
return staticDetails(parsed);
|
|
52
52
|
},
|
|
@@ -16,6 +16,16 @@ const flagshipProvider = (options) => {
|
|
|
16
16
|
return new FlagshipServerProvider({ binding, ...rest });
|
|
17
17
|
};
|
|
18
18
|
}
|
|
19
|
+
const { appId, endpoint } = options;
|
|
20
|
+
if (appId === void 0 && endpoint === void 0) {
|
|
21
|
+
throw new LunoraError(
|
|
22
|
+
"INTERNAL",
|
|
23
|
+
'flagshipProvider: HTTP mode requires either `appId` (the SDK builds the evaluation URL) or `endpoint` (a full evaluation URL). Pass exactly one, or use binding mode: `flagshipProvider({ binding: "FLAGS" })`.'
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
if (appId !== void 0 && endpoint !== void 0) {
|
|
27
|
+
throw new LunoraError("INTERNAL", "flagshipProvider: `appId` and `endpoint` are mutually exclusive in HTTP mode — pass exactly one.");
|
|
28
|
+
}
|
|
19
29
|
return () => new FlagshipServerProvider(options);
|
|
20
30
|
};
|
|
21
31
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/flags",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.13",
|
|
4
4
|
"description": "OpenFeature-based feature flags for Lunora — ctx.flags, useFlag, and a first-class Cloudflare Flagship provider with any OpenFeature provider pluggable",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"cloudflare",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"access": "public"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
63
|
+
"@lunora/errors": "1.0.0-alpha.5",
|
|
64
64
|
"@openfeature/server-sdk": "^1.22.0"
|
|
65
65
|
},
|
|
66
66
|
"peerDependencies": {
|