org.inovus.flags 1.0.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/LICENSE +17 -0
- package/README.md +354 -0
- package/dist/index.cjs +203 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +52 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +175 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
Closed Source. Proprietary & Confidential.
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Inovus Ltd. All rights reserved.
|
|
4
|
+
|
|
5
|
+
This software and associated documentation (the "Software") are the
|
|
6
|
+
proprietary and confidential property of Inovus Ltd.
|
|
7
|
+
|
|
8
|
+
No part of the Software may be used, copied, modified, merged, published,
|
|
9
|
+
distributed, sublicensed, or sold without prior written permission from
|
|
10
|
+
Inovus Ltd.
|
|
11
|
+
|
|
12
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
13
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
14
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
|
15
|
+
INOVUS LTD BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
|
16
|
+
AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
17
|
+
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
# org.inovus.flags
|
|
2
|
+
|
|
3
|
+
TypeScript client for Totum feature flags. Call `init()` once at startup, then read flags **synchronously** from memory — with background refresh and safe fallbacks when the network is unavailable.
|
|
4
|
+
|
|
5
|
+
**Zero runtime dependencies.** Requires global `fetch` (Node 18+, browsers, Workers).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install org.inovus.flags
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { createFlagsClient } from 'org.inovus.flags';
|
|
17
|
+
import type {
|
|
18
|
+
FlagDeclaration,
|
|
19
|
+
EvaluationContext,
|
|
20
|
+
FlagsClient,
|
|
21
|
+
FlagType,
|
|
22
|
+
} from 'org.inovus.flags';
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Prerequisites
|
|
28
|
+
|
|
29
|
+
Your platform team provides two values — add them to `.env`, hosting secrets, or CI:
|
|
30
|
+
|
|
31
|
+
| Variable | Purpose |
|
|
32
|
+
|----------|---------|
|
|
33
|
+
| `FLAGS_PROXY_URL` | Base URL of the flags HTTP API (no trailing slash required) |
|
|
34
|
+
| `FLAGS_API_KEY` | API key sent as header `X-Flags-Api-Key` |
|
|
35
|
+
|
|
36
|
+
```env
|
|
37
|
+
FLAGS_PROXY_URL=https://flags.example.com
|
|
38
|
+
FLAGS_API_KEY=your-proxy-key
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Do not** put Cloudflare account tokens or other infrastructure credentials in application code.
|
|
42
|
+
|
|
43
|
+
**Next.js:** use `.env.local` / project settings. Prefix with `NEXT_PUBLIC_` only if the browser bundle must read them (both values will be visible in the client — the proxy key is a limited gate, not an account secret).
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
import { createFlagsClient } from 'org.inovus.flags';
|
|
51
|
+
|
|
52
|
+
const flags = createFlagsClient({
|
|
53
|
+
proxyUrl: process.env.FLAGS_PROXY_URL!,
|
|
54
|
+
apiKey: process.env.FLAGS_API_KEY!,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await flags.init([
|
|
58
|
+
{ flagKey: 'my-app-dark-mode', type: 'boolean', defaultValue: false },
|
|
59
|
+
{ flagKey: 'my-app-checkout-flow', type: 'string', defaultValue: 'v1' },
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
// Synchronous — no await on getters
|
|
63
|
+
const darkMode = flags.getBoolean('my-app-dark-mode', false);
|
|
64
|
+
const checkout = flags.getString('my-app-checkout-flow', 'v1');
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Flag keys
|
|
70
|
+
|
|
71
|
+
Use one shared flag platform for the organisation. Separate products by **flag key prefix** (hyphen-separated):
|
|
72
|
+
|
|
73
|
+
- `totum-admin-dark-mode`
|
|
74
|
+
- `mobile-checkout-flow`
|
|
75
|
+
- `video-uploader-metrics-enabled`
|
|
76
|
+
|
|
77
|
+
Flag keys allow **letters, numbers, and hyphens only** — no dots.
|
|
78
|
+
|
|
79
|
+
Each `flagKey` in code must exist in the flag dashboard with the **matching type** (`boolean`, `string`, `number`, `object`).
|
|
80
|
+
|
|
81
|
+
### Dashboard: “Enabled” is not the same as `true`
|
|
82
|
+
|
|
83
|
+
| Control | Meaning | Typical API `reason` |
|
|
84
|
+
|---------|---------|----------------------|
|
|
85
|
+
| **Enabled** (toggle) | Flag is active and evaluated | If off → `DISABLED` |
|
|
86
|
+
| **Default** (variant) | Value when no targeting rules match | `DEFAULT` |
|
|
87
|
+
|
|
88
|
+
**Enabled does not force boolean `true`.** A flag can be Enabled with **Default = `false`** and no rules — the API correctly returns `false` with `"reason":"DEFAULT"`.
|
|
89
|
+
|
|
90
|
+
To enable a feature for **everyone**, set **Default** to the `true` variant. For partial rollouts, add targeting rules and pass matching `context` on `init` / `refresh`.
|
|
91
|
+
|
|
92
|
+
| `reason` | Meaning |
|
|
93
|
+
|----------|---------|
|
|
94
|
+
| `DEFAULT` | No rule matched; default variant served |
|
|
95
|
+
| `DISABLED` | Flag toggle off |
|
|
96
|
+
| `TARGETING_MATCH` | A targeting rule matched |
|
|
97
|
+
| `SPLIT` | Percentage / rollout bucket |
|
|
98
|
+
| `ERROR` | Evaluation problem — check `errorCode` in the response |
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## Integration pattern (required)
|
|
103
|
+
|
|
104
|
+
1. **One client** per app (singleton or React context).
|
|
105
|
+
2. **`await flags.init(declarations[, context])`** once before relying on flag values — list **every** flag the app reads.
|
|
106
|
+
3. **Synchronous reads:** `getBoolean`, `getString`, `getNumber`, `getObject` — **never** `await` these.
|
|
107
|
+
4. **`await flags.refresh(context?)`** after login or when targeting attributes change (`userId`, `plan`, etc.).
|
|
108
|
+
5. **`flags.destroy()`** optional — stops the background refresh timer on app teardown.
|
|
109
|
+
|
|
110
|
+
### Targeting context
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
await flags.init(
|
|
114
|
+
[{ flagKey: 'my-app-dark-mode', type: 'boolean', defaultValue: false }],
|
|
115
|
+
{ userId: 'anonymous' }
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
await flags.refresh({ userId: user.id, plan: user.plan });
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### React (sketch)
|
|
122
|
+
|
|
123
|
+
```typescript
|
|
124
|
+
import { createFlagsClient, type FlagDeclaration } from 'org.inovus.flags';
|
|
125
|
+
|
|
126
|
+
const declarations: FlagDeclaration[] = [
|
|
127
|
+
{ flagKey: 'my-app-dark-mode', type: 'boolean', defaultValue: false },
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
export const flags = createFlagsClient({
|
|
131
|
+
proxyUrl: process.env.NEXT_PUBLIC_FLAGS_PROXY_URL!,
|
|
132
|
+
apiKey: process.env.NEXT_PUBLIC_FLAGS_API_KEY!,
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
let initPromise: Promise<void> | null = null;
|
|
136
|
+
|
|
137
|
+
export function ensureFlagsInit(context?: EvaluationContext): Promise<void> {
|
|
138
|
+
if (!initPromise) initPromise = flags.init(declarations, context);
|
|
139
|
+
return initPromise;
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Gate UI until `ensureFlagsInit()` resolves, or accept `defaultValue` until loaded.
|
|
144
|
+
|
|
145
|
+
### Node backend
|
|
146
|
+
|
|
147
|
+
```typescript
|
|
148
|
+
const flags = createFlagsClient({
|
|
149
|
+
proxyUrl: process.env.FLAGS_PROXY_URL!,
|
|
150
|
+
apiKey: process.env.FLAGS_API_KEY!,
|
|
151
|
+
persist: false,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await flags.init(declarations);
|
|
155
|
+
await flags.refresh({ userId: req.user.id });
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Client options
|
|
161
|
+
|
|
162
|
+
| Option | Required | Default | Description |
|
|
163
|
+
|--------|----------|---------|-------------|
|
|
164
|
+
| `proxyUrl` | Yes | — | Flags API base URL |
|
|
165
|
+
| `apiKey` | Yes | — | `X-Flags-Api-Key` header value |
|
|
166
|
+
| `ttl` | No | `30000` | Background refresh interval (ms) |
|
|
167
|
+
| `persist` | No | `true` | Persist cache to `localStorage` in browsers (`flags_cache` key) |
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## API reference
|
|
172
|
+
|
|
173
|
+
### `createFlagsClient(options): FlagsClient`
|
|
174
|
+
|
|
175
|
+
### `init(declarations, context?): Promise<void>`
|
|
176
|
+
|
|
177
|
+
- **declarations:** `{ flagKey, type, defaultValue }[]`
|
|
178
|
+
- **type:** `'boolean' | 'string' | 'number' | 'object'`
|
|
179
|
+
- One batch HTTP request; starts background refresh
|
|
180
|
+
- Does not throw on network failure (logs warnings)
|
|
181
|
+
|
|
182
|
+
### `refresh(context?): Promise<void>`
|
|
183
|
+
|
|
184
|
+
Re-fetches declared flags. On failure, **keeps** existing cache.
|
|
185
|
+
|
|
186
|
+
### `getBoolean` / `getString` / `getNumber` / `getObject`
|
|
187
|
+
|
|
188
|
+
Synchronous. Never throws. Wrong or missing key → `defaultValue` + console warning.
|
|
189
|
+
|
|
190
|
+
### `destroy(): void`
|
|
191
|
+
|
|
192
|
+
Stops background interval.
|
|
193
|
+
|
|
194
|
+
### Types
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
type FlagType = 'boolean' | 'string' | 'number' | 'object';
|
|
198
|
+
type EvaluationContext = Record<string, string | number | boolean>;
|
|
199
|
+
|
|
200
|
+
interface FlagDeclaration {
|
|
201
|
+
flagKey: string;
|
|
202
|
+
type: FlagType;
|
|
203
|
+
defaultValue: unknown;
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## HTTP API (what the SDK calls)
|
|
210
|
+
|
|
211
|
+
### Authentication
|
|
212
|
+
|
|
213
|
+
```http
|
|
214
|
+
X-Flags-Api-Key: <FLAGS_API_KEY>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### `GET /health`
|
|
218
|
+
|
|
219
|
+
No auth. `{ "status": "ok" }`
|
|
220
|
+
|
|
221
|
+
### `POST /evaluate/batch`
|
|
222
|
+
|
|
223
|
+
Auth required. Body: non-empty array of:
|
|
224
|
+
|
|
225
|
+
```json
|
|
226
|
+
{
|
|
227
|
+
"flagKey": "my-app-dark-mode",
|
|
228
|
+
"type": "boolean",
|
|
229
|
+
"defaultValue": false,
|
|
230
|
+
"context": { "userId": "user-123", "plan": "premium" }
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Response `200`: array of `{ flagKey, value, reason }` in request order.
|
|
235
|
+
|
|
236
|
+
### `401 Unauthorized`
|
|
237
|
+
|
|
238
|
+
Missing or invalid API key.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Caching and failures
|
|
243
|
+
|
|
244
|
+
**Fallback order** (first match wins):
|
|
245
|
+
|
|
246
|
+
1. Fresh batch response
|
|
247
|
+
2. In-memory cache (stale is OK)
|
|
248
|
+
3. `localStorage` `flags_cache` (browsers, if `persist: true`)
|
|
249
|
+
4. `defaultValue` passed to `getX()`
|
|
250
|
+
|
|
251
|
+
**Console warnings** (prefix `[org.inovus.flags]`):
|
|
252
|
+
|
|
253
|
+
- `init: Worker unreachable and no persisted cache; getX() will use defaults`
|
|
254
|
+
- `refresh: failed; keeping existing cache`
|
|
255
|
+
- `get: flag "..." not in cache; using defaultValue`
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Anti-patterns
|
|
260
|
+
|
|
261
|
+
| Wrong | Correct |
|
|
262
|
+
|-------|---------|
|
|
263
|
+
| Infrastructure credentials in the app | `FLAGS_PROXY_URL` + `FLAGS_API_KEY` only |
|
|
264
|
+
| `await flags.getBoolean(...)` | `flags.getBoolean(...)` |
|
|
265
|
+
| HTTP request per flag per render | `init()` once; sync reads |
|
|
266
|
+
| Flag not listed in `init()` | Declare every flag in `init()` |
|
|
267
|
+
| Direct calls to the flag provider API | Use this SDK only |
|
|
268
|
+
| Reset UI to defaults when refresh fails | SDK keeps cache |
|
|
269
|
+
| Skip `await init()` | Await `init()` or gate UI |
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## New flag checklist
|
|
274
|
+
|
|
275
|
+
1. Create the flag in the dashboard with the correct type and **Default** variant.
|
|
276
|
+
2. Add to `init([..., { flagKey, type, defaultValue }])`.
|
|
277
|
+
3. Read with the matching `getX()` and the same `flagKey`.
|
|
278
|
+
4. Add targeting via `context` on `init` / `refresh` if needed.
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
## Troubleshooting
|
|
283
|
+
|
|
284
|
+
| Symptom | Action |
|
|
285
|
+
|---------|--------|
|
|
286
|
+
| Always `defaultValue` | `await init()`; verify `GET /health` |
|
|
287
|
+
| HTTP 401 | Check `FLAGS_API_KEY` with platform team |
|
|
288
|
+
| Wrong value | Align dashboard type and `init` declaration |
|
|
289
|
+
| Stale after login | `await refresh({ userId, plan, ... })` |
|
|
290
|
+
| Tests flaky | `persist: false` in test setup |
|
|
291
|
+
| `reason: "ERROR"` | Flag missing or type mismatch in dashboard |
|
|
292
|
+
| Enabled in UI but `false` + `DEFAULT` | Set **Default** variant to `true` or add targeting |
|
|
293
|
+
|
|
294
|
+
### Verify connectivity (curl)
|
|
295
|
+
|
|
296
|
+
```bash
|
|
297
|
+
curl "https://flags.example.com/health"
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
```bash
|
|
301
|
+
curl -X POST "https://flags.example.com/evaluate/batch" \
|
|
302
|
+
-H "Content-Type: application/json" \
|
|
303
|
+
-H "X-Flags-Api-Key: your-proxy-key" \
|
|
304
|
+
-d '[{"flagKey":"my-app-dark-mode","type":"boolean","defaultValue":false}]'
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**Windows cmd** (escape `"` inside `-d` as `\"`):
|
|
308
|
+
|
|
309
|
+
```bat
|
|
310
|
+
curl "https://flags.example.com/health"
|
|
311
|
+
curl -X POST "https://flags.example.com/evaluate/batch" -H "Content-Type: application/json" -H "X-Flags-Api-Key: your-proxy-key" -d "[{\"flagKey\":\"my-app-dark-mode\",\"type\":\"boolean\",\"defaultValue\":false}]"
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## App template
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
import { createFlagsClient, type FlagDeclaration } from 'org.inovus.flags';
|
|
320
|
+
|
|
321
|
+
const DECLARATIONS: FlagDeclaration[] = [
|
|
322
|
+
// { flagKey: 'my-app-feature-x', type: 'boolean', defaultValue: false },
|
|
323
|
+
];
|
|
324
|
+
|
|
325
|
+
export const flags = createFlagsClient({
|
|
326
|
+
proxyUrl: process.env.FLAGS_PROXY_URL!,
|
|
327
|
+
apiKey: process.env.FLAGS_API_KEY!,
|
|
328
|
+
persist: typeof window !== 'undefined',
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
let ready: Promise<void> | null = null;
|
|
332
|
+
|
|
333
|
+
export function initFlags(context?: Record<string, string | number | boolean>) {
|
|
334
|
+
if (!ready) ready = flags.init(DECLARATIONS, context);
|
|
335
|
+
return ready;
|
|
336
|
+
}
|
|
337
|
+
```
|
|
338
|
+
|
|
339
|
+
---
|
|
340
|
+
|
|
341
|
+
## AI agents
|
|
342
|
+
|
|
343
|
+
When implementing feature flags in an application:
|
|
344
|
+
|
|
345
|
+
- Follow the **integration pattern** and **anti-patterns** above.
|
|
346
|
+
- Use only `FLAGS_PROXY_URL` and `FLAGS_API_KEY` from the environment.
|
|
347
|
+
- List every flag in `init()` before calling `getX()`.
|
|
348
|
+
- Do not bypass this SDK for routine flag reads.
|
|
349
|
+
|
|
350
|
+
---
|
|
351
|
+
|
|
352
|
+
## License
|
|
353
|
+
|
|
354
|
+
Closed Source. Proprietary & Confidential. See `LICENSE`.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FlagsCache: () => FlagsCache,
|
|
24
|
+
createFlagsClient: () => createFlagsClient
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(index_exports);
|
|
27
|
+
|
|
28
|
+
// src/cache.ts
|
|
29
|
+
var FlagsCache = class {
|
|
30
|
+
constructor(ttl, persist) {
|
|
31
|
+
this.ttl = ttl;
|
|
32
|
+
this.persist = persist;
|
|
33
|
+
this.values = {};
|
|
34
|
+
this.timestamp = 0;
|
|
35
|
+
this.load();
|
|
36
|
+
}
|
|
37
|
+
set(values) {
|
|
38
|
+
this.values = values;
|
|
39
|
+
this.timestamp = Date.now();
|
|
40
|
+
if (this.persist) this.save();
|
|
41
|
+
}
|
|
42
|
+
get(flagKey) {
|
|
43
|
+
return this.values[flagKey];
|
|
44
|
+
}
|
|
45
|
+
isStale() {
|
|
46
|
+
return this.timestamp === 0 || Date.now() - this.timestamp > this.ttl;
|
|
47
|
+
}
|
|
48
|
+
clear() {
|
|
49
|
+
this.values = {};
|
|
50
|
+
this.timestamp = 0;
|
|
51
|
+
if (this.persist && typeof localStorage !== "undefined") {
|
|
52
|
+
localStorage.removeItem("flags_cache");
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
save() {
|
|
56
|
+
if (typeof localStorage === "undefined") return;
|
|
57
|
+
try {
|
|
58
|
+
localStorage.setItem(
|
|
59
|
+
"flags_cache",
|
|
60
|
+
JSON.stringify({
|
|
61
|
+
values: this.values,
|
|
62
|
+
timestamp: this.timestamp
|
|
63
|
+
})
|
|
64
|
+
);
|
|
65
|
+
} catch {
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
load() {
|
|
69
|
+
if (!this.persist || typeof localStorage === "undefined") return;
|
|
70
|
+
try {
|
|
71
|
+
const raw = localStorage.getItem("flags_cache");
|
|
72
|
+
if (!raw) return;
|
|
73
|
+
const parsed = JSON.parse(raw);
|
|
74
|
+
if (parsed?.values && typeof parsed.timestamp === "number") {
|
|
75
|
+
this.values = parsed.values;
|
|
76
|
+
this.timestamp = parsed.timestamp;
|
|
77
|
+
}
|
|
78
|
+
} catch {
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/client.ts
|
|
84
|
+
var DEFAULT_TTL = 3e4;
|
|
85
|
+
function warn(message) {
|
|
86
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
87
|
+
console.warn(`[org.inovus.flags] ${message}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
var FlagsClientImpl = class {
|
|
91
|
+
constructor(options) {
|
|
92
|
+
this.declarations = [];
|
|
93
|
+
this.proxyUrl = options.proxyUrl.replace(/\/$/, "");
|
|
94
|
+
this.apiKey = options.apiKey;
|
|
95
|
+
this.ttl = options.ttl ?? DEFAULT_TTL;
|
|
96
|
+
const persist = options.persist ?? true;
|
|
97
|
+
this.cache = new FlagsCache(this.ttl, persist);
|
|
98
|
+
}
|
|
99
|
+
async init(declarations, context) {
|
|
100
|
+
this.declarations = declarations;
|
|
101
|
+
this.context = context;
|
|
102
|
+
const ok = await this.fetchAndCache(context);
|
|
103
|
+
if (!ok) {
|
|
104
|
+
const hasCache = declarations.some((d) => this.cache.get(d.flagKey) !== void 0);
|
|
105
|
+
if (!hasCache) {
|
|
106
|
+
warn("init: Worker unreachable and no persisted cache; getX() will use defaults");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
this.startBackgroundRefresh();
|
|
110
|
+
}
|
|
111
|
+
async refresh(context) {
|
|
112
|
+
if (context !== void 0) {
|
|
113
|
+
this.context = context;
|
|
114
|
+
}
|
|
115
|
+
const ok = await this.fetchAndCache(this.context);
|
|
116
|
+
if (!ok) {
|
|
117
|
+
warn("refresh: failed; keeping existing cache");
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
getBoolean(flagKey, defaultValue) {
|
|
121
|
+
return this.read(flagKey, defaultValue, (v) => typeof v === "boolean");
|
|
122
|
+
}
|
|
123
|
+
getString(flagKey, defaultValue) {
|
|
124
|
+
return this.read(flagKey, defaultValue, (v) => typeof v === "string");
|
|
125
|
+
}
|
|
126
|
+
getNumber(flagKey, defaultValue) {
|
|
127
|
+
return this.read(flagKey, defaultValue, (v) => typeof v === "number");
|
|
128
|
+
}
|
|
129
|
+
getObject(flagKey, defaultValue) {
|
|
130
|
+
return this.read(flagKey, defaultValue, (v) => v !== null && typeof v === "object");
|
|
131
|
+
}
|
|
132
|
+
destroy() {
|
|
133
|
+
if (this.refreshTimer !== void 0) {
|
|
134
|
+
clearInterval(this.refreshTimer);
|
|
135
|
+
this.refreshTimer = void 0;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
read(flagKey, defaultValue, guard) {
|
|
139
|
+
const cached = this.cache.get(flagKey);
|
|
140
|
+
if (cached === void 0) {
|
|
141
|
+
warn(`get: flag "${flagKey}" not in cache; using defaultValue`);
|
|
142
|
+
return defaultValue;
|
|
143
|
+
}
|
|
144
|
+
if (!guard(cached)) {
|
|
145
|
+
warn(`get: flag "${flagKey}" has wrong type in cache; using defaultValue`);
|
|
146
|
+
return defaultValue;
|
|
147
|
+
}
|
|
148
|
+
return cached;
|
|
149
|
+
}
|
|
150
|
+
startBackgroundRefresh() {
|
|
151
|
+
this.destroy();
|
|
152
|
+
this.refreshTimer = setInterval(() => {
|
|
153
|
+
void this.refresh();
|
|
154
|
+
}, this.ttl);
|
|
155
|
+
}
|
|
156
|
+
async fetchAndCache(context) {
|
|
157
|
+
if (this.declarations.length === 0) return true;
|
|
158
|
+
const body = this.declarations.map((d) => ({
|
|
159
|
+
flagKey: d.flagKey,
|
|
160
|
+
type: d.type,
|
|
161
|
+
defaultValue: d.defaultValue,
|
|
162
|
+
...context ? { context } : {}
|
|
163
|
+
}));
|
|
164
|
+
try {
|
|
165
|
+
const response = await fetch(`${this.proxyUrl}/evaluate/batch`, {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: {
|
|
168
|
+
"Content-Type": "application/json",
|
|
169
|
+
"X-Flags-Api-Key": this.apiKey
|
|
170
|
+
},
|
|
171
|
+
body: JSON.stringify(body)
|
|
172
|
+
});
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
176
|
+
const results = await response.json();
|
|
177
|
+
if (!Array.isArray(results)) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const values = {};
|
|
181
|
+
for (const result of results) {
|
|
182
|
+
if (result && typeof result.flagKey === "string") {
|
|
183
|
+
values[result.flagKey] = result.value;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
this.cache.set(values);
|
|
187
|
+
return true;
|
|
188
|
+
} catch {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
// src/index.ts
|
|
195
|
+
function createFlagsClient(options) {
|
|
196
|
+
return new FlagsClientImpl(options);
|
|
197
|
+
}
|
|
198
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
199
|
+
0 && (module.exports = {
|
|
200
|
+
FlagsCache,
|
|
201
|
+
createFlagsClient
|
|
202
|
+
});
|
|
203
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/cache.ts","../src/client.ts"],"sourcesContent":["import { FlagsClientImpl } from './client';\nimport type { FlagsClient, FlagsClientOptions } from './types';\n\nexport function createFlagsClient(options: FlagsClientOptions): FlagsClient {\n return new FlagsClientImpl(options);\n}\n\nexport type {\n CachedFlagStore,\n EvaluationContext,\n EvaluationRequest,\n EvaluationResult,\n FlagDeclaration,\n FlagType,\n FlagsClient,\n FlagsClientOptions,\n} from './types';\n\nexport { FlagsCache } from './cache';\n","export class FlagsCache {\n private values: Record<string, unknown> = {};\n private timestamp = 0;\n\n constructor(\n private ttl: number,\n private persist: boolean\n ) {\n this.load();\n }\n\n set(values: Record<string, unknown>): void {\n this.values = values;\n this.timestamp = Date.now();\n if (this.persist) this.save();\n }\n\n get(flagKey: string): unknown | undefined {\n return this.values[flagKey];\n }\n\n isStale(): boolean {\n return this.timestamp === 0 || Date.now() - this.timestamp > this.ttl;\n }\n\n clear(): void {\n this.values = {};\n this.timestamp = 0;\n if (this.persist && typeof localStorage !== 'undefined') {\n localStorage.removeItem('flags_cache');\n }\n }\n\n private save(): void {\n if (typeof localStorage === 'undefined') return;\n try {\n localStorage.setItem(\n 'flags_cache',\n JSON.stringify({\n values: this.values,\n timestamp: this.timestamp,\n })\n );\n } catch {\n /* quota exceeded */\n }\n }\n\n private load(): void {\n if (!this.persist || typeof localStorage === 'undefined') return;\n try {\n const raw = localStorage.getItem('flags_cache');\n if (!raw) return;\n const parsed = JSON.parse(raw);\n if (parsed?.values && typeof parsed.timestamp === 'number') {\n this.values = parsed.values;\n this.timestamp = parsed.timestamp;\n }\n } catch {\n /* malformed */\n }\n }\n}\n","import { FlagsCache } from './cache';\nimport type {\n EvaluationContext,\n EvaluationResult,\n FlagDeclaration,\n FlagsClient,\n FlagsClientOptions,\n} from './types';\n\nconst DEFAULT_TTL = 30_000;\n\nfunction warn(message: string): void {\n if (typeof console !== 'undefined' && console.warn) {\n console.warn(`[org.inovus.flags] ${message}`);\n }\n}\n\nexport class FlagsClientImpl implements FlagsClient {\n private readonly proxyUrl: string;\n private readonly apiKey: string;\n private readonly ttl: number;\n private readonly cache: FlagsCache;\n private declarations: FlagDeclaration[] = [];\n private context: EvaluationContext | undefined;\n private refreshTimer: ReturnType<typeof setInterval> | undefined;\n\n constructor(options: FlagsClientOptions) {\n this.proxyUrl = options.proxyUrl.replace(/\\/$/, '');\n this.apiKey = options.apiKey;\n this.ttl = options.ttl ?? DEFAULT_TTL;\n const persist = options.persist ?? true;\n this.cache = new FlagsCache(this.ttl, persist);\n }\n\n async init(declarations: FlagDeclaration[], context?: EvaluationContext): Promise<void> {\n this.declarations = declarations;\n this.context = context;\n const ok = await this.fetchAndCache(context);\n if (!ok) {\n const hasCache = declarations.some((d) => this.cache.get(d.flagKey) !== undefined);\n if (!hasCache) {\n warn('init: Worker unreachable and no persisted cache; getX() will use defaults');\n }\n }\n this.startBackgroundRefresh();\n }\n\n async refresh(context?: EvaluationContext): Promise<void> {\n if (context !== undefined) {\n this.context = context;\n }\n const ok = await this.fetchAndCache(this.context);\n if (!ok) {\n warn('refresh: failed; keeping existing cache');\n }\n }\n\n getBoolean(flagKey: string, defaultValue: boolean): boolean {\n return this.read(flagKey, defaultValue, (v): v is boolean => typeof v === 'boolean');\n }\n\n getString(flagKey: string, defaultValue: string): string {\n return this.read(flagKey, defaultValue, (v): v is string => typeof v === 'string');\n }\n\n getNumber(flagKey: string, defaultValue: number): number {\n return this.read(flagKey, defaultValue, (v): v is number => typeof v === 'number');\n }\n\n getObject<T>(flagKey: string, defaultValue: T): T {\n return this.read(flagKey, defaultValue, (v): v is T => v !== null && typeof v === 'object');\n }\n\n destroy(): void {\n if (this.refreshTimer !== undefined) {\n clearInterval(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n }\n\n private read<T>(\n flagKey: string,\n defaultValue: T,\n guard: (value: unknown) => value is T\n ): T {\n const cached = this.cache.get(flagKey);\n if (cached === undefined) {\n warn(`get: flag \"${flagKey}\" not in cache; using defaultValue`);\n return defaultValue;\n }\n if (!guard(cached)) {\n warn(`get: flag \"${flagKey}\" has wrong type in cache; using defaultValue`);\n return defaultValue;\n }\n return cached;\n }\n\n private startBackgroundRefresh(): void {\n this.destroy();\n this.refreshTimer = setInterval(() => {\n void this.refresh();\n }, this.ttl);\n }\n\n private async fetchAndCache(context?: EvaluationContext): Promise<boolean> {\n if (this.declarations.length === 0) return true;\n\n const body = this.declarations.map((d) => ({\n flagKey: d.flagKey,\n type: d.type,\n defaultValue: d.defaultValue,\n ...(context ? { context } : {}),\n }));\n\n try {\n const response = await fetch(`${this.proxyUrl}/evaluate/batch`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Flags-Api-Key': this.apiKey,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n return false;\n }\n\n const results = (await response.json()) as EvaluationResult[];\n if (!Array.isArray(results)) {\n return false;\n }\n\n const values: Record<string, unknown> = {};\n for (const result of results) {\n if (result && typeof result.flagKey === 'string') {\n values[result.flagKey] = result.value;\n }\n }\n this.cache.set(values);\n return true;\n } catch {\n return false;\n }\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
type FlagType = 'boolean' | 'string' | 'number' | 'object';
|
|
2
|
+
type EvaluationContext = Record<string, string | number | boolean>;
|
|
3
|
+
interface FlagDeclaration {
|
|
4
|
+
flagKey: string;
|
|
5
|
+
type: FlagType;
|
|
6
|
+
defaultValue: unknown;
|
|
7
|
+
}
|
|
8
|
+
interface EvaluationRequest extends FlagDeclaration {
|
|
9
|
+
context?: EvaluationContext;
|
|
10
|
+
}
|
|
11
|
+
interface EvaluationResult<T = unknown> {
|
|
12
|
+
flagKey: string;
|
|
13
|
+
value: T;
|
|
14
|
+
reason: string;
|
|
15
|
+
}
|
|
16
|
+
interface CachedFlagStore {
|
|
17
|
+
values: Record<string, unknown>;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
}
|
|
20
|
+
interface FlagsClientOptions {
|
|
21
|
+
proxyUrl: string;
|
|
22
|
+
apiKey: string;
|
|
23
|
+
ttl?: number;
|
|
24
|
+
persist?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface FlagsClient {
|
|
27
|
+
init(declarations: FlagDeclaration[], context?: EvaluationContext): Promise<void>;
|
|
28
|
+
refresh(context?: EvaluationContext): Promise<void>;
|
|
29
|
+
getBoolean(flagKey: string, defaultValue: boolean): boolean;
|
|
30
|
+
getString(flagKey: string, defaultValue: string): string;
|
|
31
|
+
getNumber(flagKey: string, defaultValue: number): number;
|
|
32
|
+
getObject<T>(flagKey: string, defaultValue: T): T;
|
|
33
|
+
destroy(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
declare class FlagsCache {
|
|
37
|
+
private ttl;
|
|
38
|
+
private persist;
|
|
39
|
+
private values;
|
|
40
|
+
private timestamp;
|
|
41
|
+
constructor(ttl: number, persist: boolean);
|
|
42
|
+
set(values: Record<string, unknown>): void;
|
|
43
|
+
get(flagKey: string): unknown | undefined;
|
|
44
|
+
isStale(): boolean;
|
|
45
|
+
clear(): void;
|
|
46
|
+
private save;
|
|
47
|
+
private load;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
declare function createFlagsClient(options: FlagsClientOptions): FlagsClient;
|
|
51
|
+
|
|
52
|
+
export { type CachedFlagStore, type EvaluationContext, type EvaluationRequest, type EvaluationResult, type FlagDeclaration, type FlagType, FlagsCache, type FlagsClient, type FlagsClientOptions, createFlagsClient };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
type FlagType = 'boolean' | 'string' | 'number' | 'object';
|
|
2
|
+
type EvaluationContext = Record<string, string | number | boolean>;
|
|
3
|
+
interface FlagDeclaration {
|
|
4
|
+
flagKey: string;
|
|
5
|
+
type: FlagType;
|
|
6
|
+
defaultValue: unknown;
|
|
7
|
+
}
|
|
8
|
+
interface EvaluationRequest extends FlagDeclaration {
|
|
9
|
+
context?: EvaluationContext;
|
|
10
|
+
}
|
|
11
|
+
interface EvaluationResult<T = unknown> {
|
|
12
|
+
flagKey: string;
|
|
13
|
+
value: T;
|
|
14
|
+
reason: string;
|
|
15
|
+
}
|
|
16
|
+
interface CachedFlagStore {
|
|
17
|
+
values: Record<string, unknown>;
|
|
18
|
+
timestamp: number;
|
|
19
|
+
}
|
|
20
|
+
interface FlagsClientOptions {
|
|
21
|
+
proxyUrl: string;
|
|
22
|
+
apiKey: string;
|
|
23
|
+
ttl?: number;
|
|
24
|
+
persist?: boolean;
|
|
25
|
+
}
|
|
26
|
+
interface FlagsClient {
|
|
27
|
+
init(declarations: FlagDeclaration[], context?: EvaluationContext): Promise<void>;
|
|
28
|
+
refresh(context?: EvaluationContext): Promise<void>;
|
|
29
|
+
getBoolean(flagKey: string, defaultValue: boolean): boolean;
|
|
30
|
+
getString(flagKey: string, defaultValue: string): string;
|
|
31
|
+
getNumber(flagKey: string, defaultValue: number): number;
|
|
32
|
+
getObject<T>(flagKey: string, defaultValue: T): T;
|
|
33
|
+
destroy(): void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
declare class FlagsCache {
|
|
37
|
+
private ttl;
|
|
38
|
+
private persist;
|
|
39
|
+
private values;
|
|
40
|
+
private timestamp;
|
|
41
|
+
constructor(ttl: number, persist: boolean);
|
|
42
|
+
set(values: Record<string, unknown>): void;
|
|
43
|
+
get(flagKey: string): unknown | undefined;
|
|
44
|
+
isStale(): boolean;
|
|
45
|
+
clear(): void;
|
|
46
|
+
private save;
|
|
47
|
+
private load;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
declare function createFlagsClient(options: FlagsClientOptions): FlagsClient;
|
|
51
|
+
|
|
52
|
+
export { type CachedFlagStore, type EvaluationContext, type EvaluationRequest, type EvaluationResult, type FlagDeclaration, type FlagType, FlagsCache, type FlagsClient, type FlagsClientOptions, createFlagsClient };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
// src/cache.ts
|
|
2
|
+
var FlagsCache = class {
|
|
3
|
+
constructor(ttl, persist) {
|
|
4
|
+
this.ttl = ttl;
|
|
5
|
+
this.persist = persist;
|
|
6
|
+
this.values = {};
|
|
7
|
+
this.timestamp = 0;
|
|
8
|
+
this.load();
|
|
9
|
+
}
|
|
10
|
+
set(values) {
|
|
11
|
+
this.values = values;
|
|
12
|
+
this.timestamp = Date.now();
|
|
13
|
+
if (this.persist) this.save();
|
|
14
|
+
}
|
|
15
|
+
get(flagKey) {
|
|
16
|
+
return this.values[flagKey];
|
|
17
|
+
}
|
|
18
|
+
isStale() {
|
|
19
|
+
return this.timestamp === 0 || Date.now() - this.timestamp > this.ttl;
|
|
20
|
+
}
|
|
21
|
+
clear() {
|
|
22
|
+
this.values = {};
|
|
23
|
+
this.timestamp = 0;
|
|
24
|
+
if (this.persist && typeof localStorage !== "undefined") {
|
|
25
|
+
localStorage.removeItem("flags_cache");
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
save() {
|
|
29
|
+
if (typeof localStorage === "undefined") return;
|
|
30
|
+
try {
|
|
31
|
+
localStorage.setItem(
|
|
32
|
+
"flags_cache",
|
|
33
|
+
JSON.stringify({
|
|
34
|
+
values: this.values,
|
|
35
|
+
timestamp: this.timestamp
|
|
36
|
+
})
|
|
37
|
+
);
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
load() {
|
|
42
|
+
if (!this.persist || typeof localStorage === "undefined") return;
|
|
43
|
+
try {
|
|
44
|
+
const raw = localStorage.getItem("flags_cache");
|
|
45
|
+
if (!raw) return;
|
|
46
|
+
const parsed = JSON.parse(raw);
|
|
47
|
+
if (parsed?.values && typeof parsed.timestamp === "number") {
|
|
48
|
+
this.values = parsed.values;
|
|
49
|
+
this.timestamp = parsed.timestamp;
|
|
50
|
+
}
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// src/client.ts
|
|
57
|
+
var DEFAULT_TTL = 3e4;
|
|
58
|
+
function warn(message) {
|
|
59
|
+
if (typeof console !== "undefined" && console.warn) {
|
|
60
|
+
console.warn(`[org.inovus.flags] ${message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
var FlagsClientImpl = class {
|
|
64
|
+
constructor(options) {
|
|
65
|
+
this.declarations = [];
|
|
66
|
+
this.proxyUrl = options.proxyUrl.replace(/\/$/, "");
|
|
67
|
+
this.apiKey = options.apiKey;
|
|
68
|
+
this.ttl = options.ttl ?? DEFAULT_TTL;
|
|
69
|
+
const persist = options.persist ?? true;
|
|
70
|
+
this.cache = new FlagsCache(this.ttl, persist);
|
|
71
|
+
}
|
|
72
|
+
async init(declarations, context) {
|
|
73
|
+
this.declarations = declarations;
|
|
74
|
+
this.context = context;
|
|
75
|
+
const ok = await this.fetchAndCache(context);
|
|
76
|
+
if (!ok) {
|
|
77
|
+
const hasCache = declarations.some((d) => this.cache.get(d.flagKey) !== void 0);
|
|
78
|
+
if (!hasCache) {
|
|
79
|
+
warn("init: Worker unreachable and no persisted cache; getX() will use defaults");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
this.startBackgroundRefresh();
|
|
83
|
+
}
|
|
84
|
+
async refresh(context) {
|
|
85
|
+
if (context !== void 0) {
|
|
86
|
+
this.context = context;
|
|
87
|
+
}
|
|
88
|
+
const ok = await this.fetchAndCache(this.context);
|
|
89
|
+
if (!ok) {
|
|
90
|
+
warn("refresh: failed; keeping existing cache");
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
getBoolean(flagKey, defaultValue) {
|
|
94
|
+
return this.read(flagKey, defaultValue, (v) => typeof v === "boolean");
|
|
95
|
+
}
|
|
96
|
+
getString(flagKey, defaultValue) {
|
|
97
|
+
return this.read(flagKey, defaultValue, (v) => typeof v === "string");
|
|
98
|
+
}
|
|
99
|
+
getNumber(flagKey, defaultValue) {
|
|
100
|
+
return this.read(flagKey, defaultValue, (v) => typeof v === "number");
|
|
101
|
+
}
|
|
102
|
+
getObject(flagKey, defaultValue) {
|
|
103
|
+
return this.read(flagKey, defaultValue, (v) => v !== null && typeof v === "object");
|
|
104
|
+
}
|
|
105
|
+
destroy() {
|
|
106
|
+
if (this.refreshTimer !== void 0) {
|
|
107
|
+
clearInterval(this.refreshTimer);
|
|
108
|
+
this.refreshTimer = void 0;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
read(flagKey, defaultValue, guard) {
|
|
112
|
+
const cached = this.cache.get(flagKey);
|
|
113
|
+
if (cached === void 0) {
|
|
114
|
+
warn(`get: flag "${flagKey}" not in cache; using defaultValue`);
|
|
115
|
+
return defaultValue;
|
|
116
|
+
}
|
|
117
|
+
if (!guard(cached)) {
|
|
118
|
+
warn(`get: flag "${flagKey}" has wrong type in cache; using defaultValue`);
|
|
119
|
+
return defaultValue;
|
|
120
|
+
}
|
|
121
|
+
return cached;
|
|
122
|
+
}
|
|
123
|
+
startBackgroundRefresh() {
|
|
124
|
+
this.destroy();
|
|
125
|
+
this.refreshTimer = setInterval(() => {
|
|
126
|
+
void this.refresh();
|
|
127
|
+
}, this.ttl);
|
|
128
|
+
}
|
|
129
|
+
async fetchAndCache(context) {
|
|
130
|
+
if (this.declarations.length === 0) return true;
|
|
131
|
+
const body = this.declarations.map((d) => ({
|
|
132
|
+
flagKey: d.flagKey,
|
|
133
|
+
type: d.type,
|
|
134
|
+
defaultValue: d.defaultValue,
|
|
135
|
+
...context ? { context } : {}
|
|
136
|
+
}));
|
|
137
|
+
try {
|
|
138
|
+
const response = await fetch(`${this.proxyUrl}/evaluate/batch`, {
|
|
139
|
+
method: "POST",
|
|
140
|
+
headers: {
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
"X-Flags-Api-Key": this.apiKey
|
|
143
|
+
},
|
|
144
|
+
body: JSON.stringify(body)
|
|
145
|
+
});
|
|
146
|
+
if (!response.ok) {
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
const results = await response.json();
|
|
150
|
+
if (!Array.isArray(results)) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
const values = {};
|
|
154
|
+
for (const result of results) {
|
|
155
|
+
if (result && typeof result.flagKey === "string") {
|
|
156
|
+
values[result.flagKey] = result.value;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
this.cache.set(values);
|
|
160
|
+
return true;
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
// src/index.ts
|
|
168
|
+
function createFlagsClient(options) {
|
|
169
|
+
return new FlagsClientImpl(options);
|
|
170
|
+
}
|
|
171
|
+
export {
|
|
172
|
+
FlagsCache,
|
|
173
|
+
createFlagsClient
|
|
174
|
+
};
|
|
175
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/cache.ts","../src/client.ts","../src/index.ts"],"sourcesContent":["export class FlagsCache {\n private values: Record<string, unknown> = {};\n private timestamp = 0;\n\n constructor(\n private ttl: number,\n private persist: boolean\n ) {\n this.load();\n }\n\n set(values: Record<string, unknown>): void {\n this.values = values;\n this.timestamp = Date.now();\n if (this.persist) this.save();\n }\n\n get(flagKey: string): unknown | undefined {\n return this.values[flagKey];\n }\n\n isStale(): boolean {\n return this.timestamp === 0 || Date.now() - this.timestamp > this.ttl;\n }\n\n clear(): void {\n this.values = {};\n this.timestamp = 0;\n if (this.persist && typeof localStorage !== 'undefined') {\n localStorage.removeItem('flags_cache');\n }\n }\n\n private save(): void {\n if (typeof localStorage === 'undefined') return;\n try {\n localStorage.setItem(\n 'flags_cache',\n JSON.stringify({\n values: this.values,\n timestamp: this.timestamp,\n })\n );\n } catch {\n /* quota exceeded */\n }\n }\n\n private load(): void {\n if (!this.persist || typeof localStorage === 'undefined') return;\n try {\n const raw = localStorage.getItem('flags_cache');\n if (!raw) return;\n const parsed = JSON.parse(raw);\n if (parsed?.values && typeof parsed.timestamp === 'number') {\n this.values = parsed.values;\n this.timestamp = parsed.timestamp;\n }\n } catch {\n /* malformed */\n }\n }\n}\n","import { FlagsCache } from './cache';\nimport type {\n EvaluationContext,\n EvaluationResult,\n FlagDeclaration,\n FlagsClient,\n FlagsClientOptions,\n} from './types';\n\nconst DEFAULT_TTL = 30_000;\n\nfunction warn(message: string): void {\n if (typeof console !== 'undefined' && console.warn) {\n console.warn(`[org.inovus.flags] ${message}`);\n }\n}\n\nexport class FlagsClientImpl implements FlagsClient {\n private readonly proxyUrl: string;\n private readonly apiKey: string;\n private readonly ttl: number;\n private readonly cache: FlagsCache;\n private declarations: FlagDeclaration[] = [];\n private context: EvaluationContext | undefined;\n private refreshTimer: ReturnType<typeof setInterval> | undefined;\n\n constructor(options: FlagsClientOptions) {\n this.proxyUrl = options.proxyUrl.replace(/\\/$/, '');\n this.apiKey = options.apiKey;\n this.ttl = options.ttl ?? DEFAULT_TTL;\n const persist = options.persist ?? true;\n this.cache = new FlagsCache(this.ttl, persist);\n }\n\n async init(declarations: FlagDeclaration[], context?: EvaluationContext): Promise<void> {\n this.declarations = declarations;\n this.context = context;\n const ok = await this.fetchAndCache(context);\n if (!ok) {\n const hasCache = declarations.some((d) => this.cache.get(d.flagKey) !== undefined);\n if (!hasCache) {\n warn('init: Worker unreachable and no persisted cache; getX() will use defaults');\n }\n }\n this.startBackgroundRefresh();\n }\n\n async refresh(context?: EvaluationContext): Promise<void> {\n if (context !== undefined) {\n this.context = context;\n }\n const ok = await this.fetchAndCache(this.context);\n if (!ok) {\n warn('refresh: failed; keeping existing cache');\n }\n }\n\n getBoolean(flagKey: string, defaultValue: boolean): boolean {\n return this.read(flagKey, defaultValue, (v): v is boolean => typeof v === 'boolean');\n }\n\n getString(flagKey: string, defaultValue: string): string {\n return this.read(flagKey, defaultValue, (v): v is string => typeof v === 'string');\n }\n\n getNumber(flagKey: string, defaultValue: number): number {\n return this.read(flagKey, defaultValue, (v): v is number => typeof v === 'number');\n }\n\n getObject<T>(flagKey: string, defaultValue: T): T {\n return this.read(flagKey, defaultValue, (v): v is T => v !== null && typeof v === 'object');\n }\n\n destroy(): void {\n if (this.refreshTimer !== undefined) {\n clearInterval(this.refreshTimer);\n this.refreshTimer = undefined;\n }\n }\n\n private read<T>(\n flagKey: string,\n defaultValue: T,\n guard: (value: unknown) => value is T\n ): T {\n const cached = this.cache.get(flagKey);\n if (cached === undefined) {\n warn(`get: flag \"${flagKey}\" not in cache; using defaultValue`);\n return defaultValue;\n }\n if (!guard(cached)) {\n warn(`get: flag \"${flagKey}\" has wrong type in cache; using defaultValue`);\n return defaultValue;\n }\n return cached;\n }\n\n private startBackgroundRefresh(): void {\n this.destroy();\n this.refreshTimer = setInterval(() => {\n void this.refresh();\n }, this.ttl);\n }\n\n private async fetchAndCache(context?: EvaluationContext): Promise<boolean> {\n if (this.declarations.length === 0) return true;\n\n const body = this.declarations.map((d) => ({\n flagKey: d.flagKey,\n type: d.type,\n defaultValue: d.defaultValue,\n ...(context ? { context } : {}),\n }));\n\n try {\n const response = await fetch(`${this.proxyUrl}/evaluate/batch`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-Flags-Api-Key': this.apiKey,\n },\n body: JSON.stringify(body),\n });\n\n if (!response.ok) {\n return false;\n }\n\n const results = (await response.json()) as EvaluationResult[];\n if (!Array.isArray(results)) {\n return false;\n }\n\n const values: Record<string, unknown> = {};\n for (const result of results) {\n if (result && typeof result.flagKey === 'string') {\n values[result.flagKey] = result.value;\n }\n }\n this.cache.set(values);\n return true;\n } catch {\n return false;\n }\n }\n}\n","import { FlagsClientImpl } from './client';\nimport type { FlagsClient, FlagsClientOptions } from './types';\n\nexport function createFlagsClient(options: FlagsClientOptions): FlagsClient {\n return new FlagsClientImpl(options);\n}\n\nexport type {\n CachedFlagStore,\n EvaluationContext,\n EvaluationRequest,\n EvaluationResult,\n FlagDeclaration,\n FlagType,\n FlagsClient,\n FlagsClientOptions,\n} from './types';\n\nexport { FlagsCache } from './cache';\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "org.inovus.flags",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "TypeScript client for cf-flags-proxy with stale-while-revalidate caching",
|
|
5
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=18"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"main": "./dist/index.cjs",
|
|
15
|
+
"module": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.cjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"LICENSE",
|
|
27
|
+
"README.md"
|
|
28
|
+
],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup",
|
|
31
|
+
"prepublishOnly": "npm run build",
|
|
32
|
+
"test": "vitest run --coverage",
|
|
33
|
+
"typecheck": "tsc --noEmit"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@vitest/coverage-v8": "^3.0.9",
|
|
37
|
+
"happy-dom": "^17.4.4",
|
|
38
|
+
"tsup": "^8.4.0",
|
|
39
|
+
"typescript": "^5.8.2",
|
|
40
|
+
"vitest": "^3.0.9"
|
|
41
|
+
}
|
|
42
|
+
}
|