@toyz/loom-flags 0.1.0 → 0.1.1
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 +209 -0
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
# @toyz/loom-flags
|
|
2
|
+
|
|
3
|
+
Decorator-driven feature flags for [Loom](https://github.com/Toyz/loom). Reactive, transport-swappable, real-time.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
npm install @toyz/loom-flags
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
**One dependency:** `@toyz/loom`. That's it.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
### 1. Create a Provider
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { app } from "@toyz/loom";
|
|
19
|
+
import { FlagProvider } from "@toyz/loom-flags";
|
|
20
|
+
|
|
21
|
+
class MyFlagProvider extends FlagProvider {
|
|
22
|
+
isEnabled(flag: string, context?: Record<string, any>): boolean {
|
|
23
|
+
return this.flags.get(flag) ?? false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
getVariant<T = string>(flag: string, fallback: T): T {
|
|
27
|
+
const val = this.variants.get(flag);
|
|
28
|
+
return (val !== undefined ? val : fallback) as T;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const provider = new MyFlagProvider();
|
|
33
|
+
provider.set("dark-mode", true);
|
|
34
|
+
provider.set("beta-export", false);
|
|
35
|
+
|
|
36
|
+
app.use(FlagProvider, provider);
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Use `@flag` on a Class
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { flag } from "@toyz/loom-flags";
|
|
43
|
+
|
|
44
|
+
@component("new-dashboard")
|
|
45
|
+
@flag("new-dashboard")
|
|
46
|
+
class NewDashboard extends LoomElement {
|
|
47
|
+
update() {
|
|
48
|
+
if (!this.flagEnabled) return <div>Feature not available</div>;
|
|
49
|
+
return <div>Welcome to the new dashboard!</div>;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
The `@flag` class decorator injects a reactive `flagEnabled` property. When the flag changes at runtime, `scheduleUpdate()` is called automatically.
|
|
55
|
+
|
|
56
|
+
### 3. Use `@flag` on a Method
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
@component("data-tools")
|
|
60
|
+
class DataTools extends LoomElement {
|
|
61
|
+
@flag("beta-export")
|
|
62
|
+
handleExport() {
|
|
63
|
+
// Only runs when "beta-export" is enabled — no-op otherwise
|
|
64
|
+
downloadCSV(this.data);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 4. Dynamic Context
|
|
70
|
+
|
|
71
|
+
Pass user info to the provider for targeted flag evaluation:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
@flag("premium-widgets", el => ({
|
|
75
|
+
userId: el.user.id,
|
|
76
|
+
plan: el.user.plan,
|
|
77
|
+
}))
|
|
78
|
+
class PremiumWidget extends LoomElement { ... }
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### 5. Declarative with `<loom-flag>`
|
|
82
|
+
|
|
83
|
+
```tsx
|
|
84
|
+
import "@toyz/loom-flags"; // registers <loom-flag>
|
|
85
|
+
|
|
86
|
+
<loom-flag name="beta-feature">
|
|
87
|
+
<new-widget slot="enabled" />
|
|
88
|
+
<span slot="disabled">Coming soon…</span>
|
|
89
|
+
</loom-flag>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Swaps slots reactively when the flag changes — no component code required.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Real-Time Updates
|
|
97
|
+
|
|
98
|
+
Providers can push flag changes at runtime. Every `@flag` and `<loom-flag>` re-evaluates instantly:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// From a WebSocket handler, SSE listener, or polling loop:
|
|
102
|
+
provider.set("dark-mode", false); // toggles all @flag("dark-mode")
|
|
103
|
+
provider.setVariant("checkout", "b"); // updates variant value
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Under the hood, `set()` fires a `FlagChanged` event on the Loom bus. All subscribers react.
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## API
|
|
111
|
+
|
|
112
|
+
### `@flag(name, context?)`
|
|
113
|
+
|
|
114
|
+
Multi-kind decorator. Works on classes and methods.
|
|
115
|
+
|
|
116
|
+
| Target | Behavior |
|
|
117
|
+
|---|---|
|
|
118
|
+
| Class | Injects reactive `flagEnabled` + `flagName` properties |
|
|
119
|
+
| Method | Guards execution — no-op when flag is off |
|
|
120
|
+
|
|
121
|
+
### `FlagProvider`
|
|
122
|
+
|
|
123
|
+
Abstract class — extend and register via DI.
|
|
124
|
+
|
|
125
|
+
```ts
|
|
126
|
+
abstract class FlagProvider {
|
|
127
|
+
abstract isEnabled(flag: string, context?: Record<string, any>): boolean;
|
|
128
|
+
abstract getVariant<T = string>(flag: string, fallback: T): T;
|
|
129
|
+
set(flag: string, enabled: boolean): void;
|
|
130
|
+
setVariant(flag: string, value: string): void;
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### `<loom-flag name="...">`
|
|
135
|
+
|
|
136
|
+
Built-in component for declarative flag gating.
|
|
137
|
+
|
|
138
|
+
| Slot | Shown when |
|
|
139
|
+
|---|---|
|
|
140
|
+
| `enabled` | Flag is on |
|
|
141
|
+
| `disabled` | Flag is off |
|
|
142
|
+
|
|
143
|
+
### `FlagChanged`
|
|
144
|
+
|
|
145
|
+
Bus event dispatched when a flag changes.
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
class FlagChanged extends LoomEvent {
|
|
149
|
+
readonly flag: string;
|
|
150
|
+
readonly enabled: boolean;
|
|
151
|
+
readonly variant?: string;
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Custom Providers
|
|
158
|
+
|
|
159
|
+
Integrate with any flag service:
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
// LaunchDarkly
|
|
163
|
+
class LDProvider extends FlagProvider {
|
|
164
|
+
constructor(private client: LDClient) { super(); }
|
|
165
|
+
|
|
166
|
+
isEnabled(flag: string, context?: Record<string, any>): boolean {
|
|
167
|
+
return this.client.variation(flag, context, false);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
getVariant<T = string>(flag: string, fallback: T): T {
|
|
171
|
+
return this.client.variation(flag, {}, fallback);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
app.use(FlagProvider, new LDProvider(ldClient));
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
One DI swap. Every `@flag` and `<loom-flag>` in the app uses the new provider. No component changes.
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## Testing
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
import { MockFlags } from "@toyz/loom-flags/testing";
|
|
186
|
+
|
|
187
|
+
const flags = new MockFlags();
|
|
188
|
+
app.use(FlagProvider, flags);
|
|
189
|
+
|
|
190
|
+
// Toggle flags
|
|
191
|
+
flags.enable("dark-mode");
|
|
192
|
+
flags.disable("beta-export");
|
|
193
|
+
flags.setVariant("checkout-flow", "variant-b");
|
|
194
|
+
|
|
195
|
+
// Assertions
|
|
196
|
+
flags.assertChecked("dark-mode");
|
|
197
|
+
flags.assertEnabled("dark-mode");
|
|
198
|
+
flags.assertDisabled("beta-export");
|
|
199
|
+
flags.assertNotChecked("unknown-flag");
|
|
200
|
+
|
|
201
|
+
// Reset between tests
|
|
202
|
+
flags.reset();
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
---
|
|
206
|
+
|
|
207
|
+
## License
|
|
208
|
+
|
|
209
|
+
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@toyz/loom-flags",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Feature flags for Loom — decorator-driven with real-time reactive updates",
|
|
6
6
|
"license": "MIT",
|
|
@@ -49,4 +49,4 @@
|
|
|
49
49
|
"reactive",
|
|
50
50
|
"web-components"
|
|
51
51
|
]
|
|
52
|
-
}
|
|
52
|
+
}
|