@flagtide/angular 0.0.0-stage → 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 +21 -0
- package/README.md +85 -2
- package/fesm2022/flagtide-angular-overrides.mjs +309 -0
- package/fesm2022/flagtide-angular-overrides.mjs.map +1 -0
- package/fesm2022/flagtide-angular.mjs +258 -0
- package/fesm2022/flagtide-angular.mjs.map +1 -0
- package/package.json +48 -3
- package/types/flagtide-angular-overrides.d.ts +36 -0
- package/types/flagtide-angular.d.ts +163 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adrian Turbiński
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,86 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @flagtide/angular
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Signals-first feature flags for Angular. Flags are evaluated in the browser with the same algorithm as the flagtide server and update when the server pushes a change. It works zoneless and with server side rendering.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @flagtide/angular @flagtide/core
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { ApplicationConfig } from '@angular/core';
|
|
11
|
+
import { provideFlagtide } from '@flagtide/angular';
|
|
12
|
+
|
|
13
|
+
export const appConfig: ApplicationConfig = {
|
|
14
|
+
providers: [
|
|
15
|
+
provideFlagtide({
|
|
16
|
+
sdkKey: 'fws_...',
|
|
17
|
+
streamUrl: 'wss://flags.example.com/sdk/v1/stream',
|
|
18
|
+
context: { key: 'user-42', attributes: { plan: 'pro' } },
|
|
19
|
+
}),
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Reading flags
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { Component } from '@angular/core';
|
|
28
|
+
import { injectFlag } from '@flagtide/angular';
|
|
29
|
+
|
|
30
|
+
@Component({
|
|
31
|
+
selector: 'app-header',
|
|
32
|
+
template: `
|
|
33
|
+
@if (showBanner()) {
|
|
34
|
+
<p>{{ bannerText() }}</p>
|
|
35
|
+
}
|
|
36
|
+
`,
|
|
37
|
+
})
|
|
38
|
+
export class HeaderComponent {
|
|
39
|
+
readonly showBanner = injectFlag('header-banner', false);
|
|
40
|
+
readonly bannerText = injectFlag('banner-text', 'Welcome');
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`injectFlag(key, fallback)` returns a `Signal`. The fallback is used while the flag is unknown and when its type differs from the type of the fallback. Boolean, string, number and JSON flags are supported.
|
|
45
|
+
|
|
46
|
+
## Template and router
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<app-promo *flagtideFlag="'promo'; else plain" />
|
|
50
|
+
<ng-template #plain><app-standard /></ng-template>
|
|
51
|
+
<span *flagtideFlag="'checkout-label'; equals: 'Buy now'">Buy now</span>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
import { flagtideGuard } from '@flagtide/angular';
|
|
56
|
+
|
|
57
|
+
export const routes = [
|
|
58
|
+
{
|
|
59
|
+
path: 'recommendations',
|
|
60
|
+
canMatch: [flagtideGuard('beta-recommendations', { redirectTo: '/' })],
|
|
61
|
+
loadComponent: () => import('./recommendations').then((m) => m.Recommendations),
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Connection status
|
|
67
|
+
|
|
68
|
+
Inject `FlagtideStatus` and read `status()`: `connecting`, `live`, `stale` or `offline`. `stale` means flags are served from storage or after a dropped connection and cannot be confirmed current.
|
|
69
|
+
|
|
70
|
+
## Server side rendering
|
|
71
|
+
|
|
72
|
+
On the server the SDK opens no socket and touches no browser storage. It fetches the snapshot over HTTP before the first render and passes it to the browser through `TransferState`, so the first client render matches the server render. Set `snapshotUrl` when the server reaches the API under another address than the browser, and give `context` a key that is the same on both sides if you use percentage rollouts.
|
|
73
|
+
|
|
74
|
+
## Dev overrides panel
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
import { FlagtideOverridesPanel } from '@flagtide/angular/overrides';
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Add `<flagtide-overrides-panel />` once, for example in development builds. It lists every flag, lets you override values in this browser and reports them with the reason `OVERRIDE`.
|
|
81
|
+
|
|
82
|
+
## Requirements
|
|
83
|
+
|
|
84
|
+
Angular 22, `@flagtide/core` and RxJS 7.8 as peer dependencies.
|
|
85
|
+
|
|
86
|
+
MIT licensed.
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { ChangeDetectionStrategy, Component, computed, inject, signal } from "@angular/core";
|
|
3
|
+
import { Flagtide } from "@flagtide/angular";
|
|
4
|
+
import { flagTypeOf } from "@flagtide/core";
|
|
5
|
+
const SAMPLE_FALLBACK = {
|
|
6
|
+
boolean: false,
|
|
7
|
+
string: "",
|
|
8
|
+
number: 0,
|
|
9
|
+
json: null
|
|
10
|
+
};
|
|
11
|
+
function describeValue(value) {
|
|
12
|
+
return typeof value === "string" ? value : JSON.stringify(value);
|
|
13
|
+
}
|
|
14
|
+
var FlagtideOverridesPanel = class FlagtideOverridesPanel {
|
|
15
|
+
open = signal(false, ...ngDevMode ? [{ debugName: "open" }] : /* istanbul ignore next */ []);
|
|
16
|
+
invalidJson = signal(null, ...ngDevMode ? [{ debugName: "invalidJson" }] : /* istanbul ignore next */ []);
|
|
17
|
+
flagtide = inject(Flagtide);
|
|
18
|
+
status = this.flagtide.status;
|
|
19
|
+
rows = computed(() => {
|
|
20
|
+
this.flagtide.revision();
|
|
21
|
+
const client = this.flagtide.client;
|
|
22
|
+
return client.flagKeys().map((key) => {
|
|
23
|
+
const type = client.flagType(key) ?? "json";
|
|
24
|
+
const resolution = client.resolve(key, SAMPLE_FALLBACK[type]);
|
|
25
|
+
return {
|
|
26
|
+
key,
|
|
27
|
+
type,
|
|
28
|
+
value: resolution.value,
|
|
29
|
+
text: describeValue(resolution.value),
|
|
30
|
+
reason: resolution.reason,
|
|
31
|
+
overridden: client.overrides.get(key) !== void 0
|
|
32
|
+
};
|
|
33
|
+
}).sort((left, right) => left.key.localeCompare(right.key));
|
|
34
|
+
}, ...ngDevMode ? [{ debugName: "rows" }] : /* istanbul ignore next */ []);
|
|
35
|
+
overriddenCount = computed(() => this.rows().filter((row) => row.overridden).length, ...ngDevMode ? [{ debugName: "overriddenCount" }] : /* istanbul ignore next */ []);
|
|
36
|
+
setBoolean(key, event) {
|
|
37
|
+
this.apply(key, event.target.checked);
|
|
38
|
+
}
|
|
39
|
+
setString(key, event) {
|
|
40
|
+
this.apply(key, event.target.value);
|
|
41
|
+
}
|
|
42
|
+
setNumber(key, event) {
|
|
43
|
+
const raw = event.target.value.trim();
|
|
44
|
+
const parsed = Number(raw);
|
|
45
|
+
if (raw !== "" && Number.isFinite(parsed)) this.apply(key, parsed);
|
|
46
|
+
}
|
|
47
|
+
setJson(key, event) {
|
|
48
|
+
let parsed;
|
|
49
|
+
try {
|
|
50
|
+
parsed = JSON.parse(event.target.value);
|
|
51
|
+
} catch {
|
|
52
|
+
this.invalidJson.set(key);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!this.apply(key, parsed)) this.invalidJson.set(key);
|
|
56
|
+
}
|
|
57
|
+
clear(key) {
|
|
58
|
+
this.flagtide.client.overrides.clear(key);
|
|
59
|
+
}
|
|
60
|
+
clearAll() {
|
|
61
|
+
this.flagtide.client.overrides.clearAll();
|
|
62
|
+
}
|
|
63
|
+
apply(key, value) {
|
|
64
|
+
this.invalidJson.set(null);
|
|
65
|
+
if (flagTypeOf(value) !== this.flagtide.client.flagType(key)) return false;
|
|
66
|
+
this.flagtide.client.overrides.set(key, value);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
70
|
+
minVersion: "12.0.0",
|
|
71
|
+
version: "22.2.1",
|
|
72
|
+
ngImport: i0,
|
|
73
|
+
type: FlagtideOverridesPanel,
|
|
74
|
+
deps: [],
|
|
75
|
+
target: i0.ɵɵFactoryTarget.Component
|
|
76
|
+
});
|
|
77
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({
|
|
78
|
+
minVersion: "17.0.0",
|
|
79
|
+
version: "22.2.1",
|
|
80
|
+
type: FlagtideOverridesPanel,
|
|
81
|
+
isStandalone: true,
|
|
82
|
+
selector: "flagtide-overrides-panel",
|
|
83
|
+
host: { classAttribute: "fw-host" },
|
|
84
|
+
ngImport: i0,
|
|
85
|
+
template: `
|
|
86
|
+
<section class="fw-panel" aria-label="Flag overrides">
|
|
87
|
+
<button
|
|
88
|
+
type="button"
|
|
89
|
+
class="fw-toggle"
|
|
90
|
+
[attr.aria-expanded]="open()"
|
|
91
|
+
aria-controls="fw-overrides-body"
|
|
92
|
+
(click)="open.set(!open())"
|
|
93
|
+
>
|
|
94
|
+
<span>Flags</span>
|
|
95
|
+
<span class="fw-count">{{ rows().length }}</span>
|
|
96
|
+
@if (overriddenCount() > 0) {
|
|
97
|
+
<span class="fw-count fw-count-active">{{ overriddenCount() }} overridden</span>
|
|
98
|
+
}
|
|
99
|
+
<span class="fw-status" [attr.data-status]="status()">Connection: {{ status() }}</span>
|
|
100
|
+
</button>
|
|
101
|
+
@if (open()) {
|
|
102
|
+
<div id="fw-overrides-body" class="fw-body">
|
|
103
|
+
@if (rows().length === 0) {
|
|
104
|
+
<p class="fw-empty">No flags received yet.</p>
|
|
105
|
+
} @else {
|
|
106
|
+
<ul class="fw-list">
|
|
107
|
+
@for (row of rows(); track row.key) {
|
|
108
|
+
<li class="fw-row" [class.fw-overridden]="row.overridden">
|
|
109
|
+
<div class="fw-meta">
|
|
110
|
+
<code class="fw-key">{{ row.key }}</code>
|
|
111
|
+
<span class="fw-type">{{ row.type }}</span>
|
|
112
|
+
<span class="fw-reason">{{ row.reason }}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<div class="fw-control">
|
|
115
|
+
@switch (row.type) {
|
|
116
|
+
@case ('boolean') {
|
|
117
|
+
<label class="fw-switch">
|
|
118
|
+
<input
|
|
119
|
+
type="checkbox"
|
|
120
|
+
role="switch"
|
|
121
|
+
[checked]="row.value === true"
|
|
122
|
+
[attr.aria-label]="'Override ' + row.key"
|
|
123
|
+
(change)="setBoolean(row.key, $event)"
|
|
124
|
+
/>
|
|
125
|
+
<span>{{ row.value === true ? 'on' : 'off' }}</span>
|
|
126
|
+
</label>
|
|
127
|
+
}
|
|
128
|
+
@case ('string') {
|
|
129
|
+
<input
|
|
130
|
+
class="fw-input"
|
|
131
|
+
type="text"
|
|
132
|
+
[value]="row.text"
|
|
133
|
+
[attr.aria-label]="'Override ' + row.key"
|
|
134
|
+
(change)="setString(row.key, $event)"
|
|
135
|
+
/>
|
|
136
|
+
}
|
|
137
|
+
@case ('number') {
|
|
138
|
+
<input
|
|
139
|
+
class="fw-input"
|
|
140
|
+
type="number"
|
|
141
|
+
step="any"
|
|
142
|
+
[value]="row.text"
|
|
143
|
+
[attr.aria-label]="'Override ' + row.key"
|
|
144
|
+
(change)="setNumber(row.key, $event)"
|
|
145
|
+
/>
|
|
146
|
+
}
|
|
147
|
+
@default {
|
|
148
|
+
<textarea
|
|
149
|
+
class="fw-input fw-json"
|
|
150
|
+
rows="3"
|
|
151
|
+
spellcheck="false"
|
|
152
|
+
[value]="row.text"
|
|
153
|
+
[attr.aria-label]="'Override ' + row.key + ' as JSON'"
|
|
154
|
+
[attr.aria-invalid]="invalidJson() === row.key"
|
|
155
|
+
(change)="setJson(row.key, $event)"
|
|
156
|
+
></textarea>
|
|
157
|
+
@if (invalidJson() === row.key) {
|
|
158
|
+
<span class="fw-error" role="alert"
|
|
159
|
+
>Not applied. Enter valid JSON: an object, an array or null.</span
|
|
160
|
+
>
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
@if (row.overridden) {
|
|
165
|
+
<button type="button" class="fw-reset" (click)="clear(row.key)">
|
|
166
|
+
Reset<span class="fw-sr"> {{ row.key }}</span>
|
|
167
|
+
</button>
|
|
168
|
+
}
|
|
169
|
+
</div>
|
|
170
|
+
</li>
|
|
171
|
+
}
|
|
172
|
+
</ul>
|
|
173
|
+
<button
|
|
174
|
+
type="button"
|
|
175
|
+
class="fw-reset-all"
|
|
176
|
+
[disabled]="overriddenCount() === 0"
|
|
177
|
+
(click)="clearAll()"
|
|
178
|
+
>
|
|
179
|
+
Reset all overrides
|
|
180
|
+
</button>
|
|
181
|
+
}
|
|
182
|
+
</div>
|
|
183
|
+
}
|
|
184
|
+
</section>
|
|
185
|
+
`,
|
|
186
|
+
isInline: true,
|
|
187
|
+
styles: [":host{position:fixed;right:16px;bottom:16px;z-index:2147483000;font:14px/1.4 system-ui,sans-serif;color:#f3f5f8}.fw-panel{width:min(420px,calc(100vw - 32px));background:#1b1f27;border:1px solid #4a5263;border-radius:10px;box-shadow:0 8px 28px #00000059}.fw-toggle{display:flex;flex-wrap:wrap;gap:8px;align-items:center;width:100%;padding:10px 12px;font:inherit;color:inherit;text-align:left;background:transparent;border:0;cursor:pointer}.fw-count{padding:0 8px;background:#3a4254;border-radius:10px}.fw-count-active{color:#1b1f27;background:#ffd24a}.fw-status{margin-left:auto}.fw-status:before{display:inline-block;width:8px;height:8px;margin-right:6px;content:\"\";background:#9aa3b5;border-radius:50%}.fw-status[data-status=live]:before{background:#46d37c}.fw-status[data-status=stale]:before{background:#ffd24a;border-radius:2px}.fw-status[data-status=offline]:before{background:#ff7a7a;border-radius:0}.fw-body{max-height:min(60vh,520px);padding:0 12px 12px;overflow:auto;border-top:1px solid #4a5263}.fw-list{padding:0;margin:8px 0;list-style:none}.fw-row{padding:8px 0;border-bottom:1px solid #333a49}.fw-overridden{border-left:3px solid #ffd24a;padding-left:8px}.fw-meta{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}.fw-key{font-weight:600}.fw-type,.fw-reason{color:#b6bdcc;font-size:12px}.fw-control{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:6px}.fw-switch{display:inline-flex;gap:8px;align-items:center}.fw-input{flex:1 1 160px;min-width:0;padding:6px 8px;font:inherit;color:#f3f5f8;background:#11141a;border:1px solid #6b7488;border-radius:6px}.fw-json{font-family:ui-monospace,monospace}.fw-error{flex-basis:100%;color:#ffb3b3}button{font:inherit}.fw-reset,.fw-reset-all{padding:4px 10px;color:#f3f5f8;background:#2c3342;border:1px solid #6b7488;border-radius:6px;cursor:pointer}.fw-reset-all:disabled{color:#8b94a8;cursor:default}.fw-empty{margin:12px 0 0;color:#b6bdcc}.fw-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:3px solid #ffd24a;outline-offset:2px}\n"],
|
|
188
|
+
changeDetection: i0.ChangeDetectionStrategy.OnPush
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
192
|
+
minVersion: "12.0.0",
|
|
193
|
+
version: "22.2.1",
|
|
194
|
+
ngImport: i0,
|
|
195
|
+
type: FlagtideOverridesPanel,
|
|
196
|
+
decorators: [{
|
|
197
|
+
type: Component,
|
|
198
|
+
args: [{
|
|
199
|
+
selector: "flagtide-overrides-panel",
|
|
200
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
201
|
+
host: { class: "fw-host" },
|
|
202
|
+
template: `
|
|
203
|
+
<section class="fw-panel" aria-label="Flag overrides">
|
|
204
|
+
<button
|
|
205
|
+
type="button"
|
|
206
|
+
class="fw-toggle"
|
|
207
|
+
[attr.aria-expanded]="open()"
|
|
208
|
+
aria-controls="fw-overrides-body"
|
|
209
|
+
(click)="open.set(!open())"
|
|
210
|
+
>
|
|
211
|
+
<span>Flags</span>
|
|
212
|
+
<span class="fw-count">{{ rows().length }}</span>
|
|
213
|
+
@if (overriddenCount() > 0) {
|
|
214
|
+
<span class="fw-count fw-count-active">{{ overriddenCount() }} overridden</span>
|
|
215
|
+
}
|
|
216
|
+
<span class="fw-status" [attr.data-status]="status()">Connection: {{ status() }}</span>
|
|
217
|
+
</button>
|
|
218
|
+
@if (open()) {
|
|
219
|
+
<div id="fw-overrides-body" class="fw-body">
|
|
220
|
+
@if (rows().length === 0) {
|
|
221
|
+
<p class="fw-empty">No flags received yet.</p>
|
|
222
|
+
} @else {
|
|
223
|
+
<ul class="fw-list">
|
|
224
|
+
@for (row of rows(); track row.key) {
|
|
225
|
+
<li class="fw-row" [class.fw-overridden]="row.overridden">
|
|
226
|
+
<div class="fw-meta">
|
|
227
|
+
<code class="fw-key">{{ row.key }}</code>
|
|
228
|
+
<span class="fw-type">{{ row.type }}</span>
|
|
229
|
+
<span class="fw-reason">{{ row.reason }}</span>
|
|
230
|
+
</div>
|
|
231
|
+
<div class="fw-control">
|
|
232
|
+
@switch (row.type) {
|
|
233
|
+
@case ('boolean') {
|
|
234
|
+
<label class="fw-switch">
|
|
235
|
+
<input
|
|
236
|
+
type="checkbox"
|
|
237
|
+
role="switch"
|
|
238
|
+
[checked]="row.value === true"
|
|
239
|
+
[attr.aria-label]="'Override ' + row.key"
|
|
240
|
+
(change)="setBoolean(row.key, $event)"
|
|
241
|
+
/>
|
|
242
|
+
<span>{{ row.value === true ? 'on' : 'off' }}</span>
|
|
243
|
+
</label>
|
|
244
|
+
}
|
|
245
|
+
@case ('string') {
|
|
246
|
+
<input
|
|
247
|
+
class="fw-input"
|
|
248
|
+
type="text"
|
|
249
|
+
[value]="row.text"
|
|
250
|
+
[attr.aria-label]="'Override ' + row.key"
|
|
251
|
+
(change)="setString(row.key, $event)"
|
|
252
|
+
/>
|
|
253
|
+
}
|
|
254
|
+
@case ('number') {
|
|
255
|
+
<input
|
|
256
|
+
class="fw-input"
|
|
257
|
+
type="number"
|
|
258
|
+
step="any"
|
|
259
|
+
[value]="row.text"
|
|
260
|
+
[attr.aria-label]="'Override ' + row.key"
|
|
261
|
+
(change)="setNumber(row.key, $event)"
|
|
262
|
+
/>
|
|
263
|
+
}
|
|
264
|
+
@default {
|
|
265
|
+
<textarea
|
|
266
|
+
class="fw-input fw-json"
|
|
267
|
+
rows="3"
|
|
268
|
+
spellcheck="false"
|
|
269
|
+
[value]="row.text"
|
|
270
|
+
[attr.aria-label]="'Override ' + row.key + ' as JSON'"
|
|
271
|
+
[attr.aria-invalid]="invalidJson() === row.key"
|
|
272
|
+
(change)="setJson(row.key, $event)"
|
|
273
|
+
></textarea>
|
|
274
|
+
@if (invalidJson() === row.key) {
|
|
275
|
+
<span class="fw-error" role="alert"
|
|
276
|
+
>Not applied. Enter valid JSON: an object, an array or null.</span
|
|
277
|
+
>
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
@if (row.overridden) {
|
|
282
|
+
<button type="button" class="fw-reset" (click)="clear(row.key)">
|
|
283
|
+
Reset<span class="fw-sr"> {{ row.key }}</span>
|
|
284
|
+
</button>
|
|
285
|
+
}
|
|
286
|
+
</div>
|
|
287
|
+
</li>
|
|
288
|
+
}
|
|
289
|
+
</ul>
|
|
290
|
+
<button
|
|
291
|
+
type="button"
|
|
292
|
+
class="fw-reset-all"
|
|
293
|
+
[disabled]="overriddenCount() === 0"
|
|
294
|
+
(click)="clearAll()"
|
|
295
|
+
>
|
|
296
|
+
Reset all overrides
|
|
297
|
+
</button>
|
|
298
|
+
}
|
|
299
|
+
</div>
|
|
300
|
+
}
|
|
301
|
+
</section>
|
|
302
|
+
`,
|
|
303
|
+
styles: [":host{position:fixed;right:16px;bottom:16px;z-index:2147483000;font:14px/1.4 system-ui,sans-serif;color:#f3f5f8}.fw-panel{width:min(420px,calc(100vw - 32px));background:#1b1f27;border:1px solid #4a5263;border-radius:10px;box-shadow:0 8px 28px #00000059}.fw-toggle{display:flex;flex-wrap:wrap;gap:8px;align-items:center;width:100%;padding:10px 12px;font:inherit;color:inherit;text-align:left;background:transparent;border:0;cursor:pointer}.fw-count{padding:0 8px;background:#3a4254;border-radius:10px}.fw-count-active{color:#1b1f27;background:#ffd24a}.fw-status{margin-left:auto}.fw-status:before{display:inline-block;width:8px;height:8px;margin-right:6px;content:\"\";background:#9aa3b5;border-radius:50%}.fw-status[data-status=live]:before{background:#46d37c}.fw-status[data-status=stale]:before{background:#ffd24a;border-radius:2px}.fw-status[data-status=offline]:before{background:#ff7a7a;border-radius:0}.fw-body{max-height:min(60vh,520px);padding:0 12px 12px;overflow:auto;border-top:1px solid #4a5263}.fw-list{padding:0;margin:8px 0;list-style:none}.fw-row{padding:8px 0;border-bottom:1px solid #333a49}.fw-overridden{border-left:3px solid #ffd24a;padding-left:8px}.fw-meta{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}.fw-key{font-weight:600}.fw-type,.fw-reason{color:#b6bdcc;font-size:12px}.fw-control{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin-top:6px}.fw-switch{display:inline-flex;gap:8px;align-items:center}.fw-input{flex:1 1 160px;min-width:0;padding:6px 8px;font:inherit;color:#f3f5f8;background:#11141a;border:1px solid #6b7488;border-radius:6px}.fw-json{font-family:ui-monospace,monospace}.fw-error{flex-basis:100%;color:#ffb3b3}button{font:inherit}.fw-reset,.fw-reset-all{padding:4px 10px;color:#f3f5f8;background:#2c3342;border:1px solid #6b7488;border-radius:6px;cursor:pointer}.fw-reset-all:disabled{color:#8b94a8;cursor:default}.fw-empty{margin:12px 0 0;color:#b6bdcc}.fw-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}button:focus-visible,input:focus-visible,textarea:focus-visible{outline:3px solid #ffd24a;outline-offset:2px}\n"]
|
|
304
|
+
}]
|
|
305
|
+
}]
|
|
306
|
+
});
|
|
307
|
+
export { FlagtideOverridesPanel };
|
|
308
|
+
|
|
309
|
+
//# sourceMappingURL=flagtide-angular-overrides.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flagtide-angular-overrides.mjs","names":[],"sources":["../../../../libs/angular/overrides/src/overrides-panel.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, computed, inject, signal } from '@angular/core';\nimport { Flagtide } from '@flagtide/angular';\nimport { flagTypeOf } from '@flagtide/core';\nimport type { FlagType, JsonValue, ResolutionReason } from '@flagtide/core';\n\ninterface OverrideRow {\n readonly key: string;\n readonly type: FlagType;\n readonly value: JsonValue;\n readonly text: string;\n readonly reason: ResolutionReason;\n readonly overridden: boolean;\n}\n\nconst SAMPLE_FALLBACK: Record<FlagType, JsonValue> = { boolean: false, string: '', number: 0, json: null };\n\nfunction describeValue(value: JsonValue): string {\n return typeof value === 'string' ? value : JSON.stringify(value);\n}\n\n/**\n * A development panel that lists every flag the client knows and lets you override its value in this browser.\n * Overrides win over server values, are reported with the reason `OVERRIDE` and are kept in browser storage.\n * Place it once, near the root of the application, for example behind a development check.\n *\n * ```html\n * <flagtide-overrides-panel />\n * ```\n */\n@Component({\n selector: 'flagtide-overrides-panel',\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: { class: 'fw-host' },\n template: `\n <section class=\"fw-panel\" aria-label=\"Flag overrides\">\n <button\n type=\"button\"\n class=\"fw-toggle\"\n [attr.aria-expanded]=\"open()\"\n aria-controls=\"fw-overrides-body\"\n (click)=\"open.set(!open())\"\n >\n <span>Flags</span>\n <span class=\"fw-count\">{{ rows().length }}</span>\n @if (overriddenCount() > 0) {\n <span class=\"fw-count fw-count-active\">{{ overriddenCount() }} overridden</span>\n }\n <span class=\"fw-status\" [attr.data-status]=\"status()\">Connection: {{ status() }}</span>\n </button>\n @if (open()) {\n <div id=\"fw-overrides-body\" class=\"fw-body\">\n @if (rows().length === 0) {\n <p class=\"fw-empty\">No flags received yet.</p>\n } @else {\n <ul class=\"fw-list\">\n @for (row of rows(); track row.key) {\n <li class=\"fw-row\" [class.fw-overridden]=\"row.overridden\">\n <div class=\"fw-meta\">\n <code class=\"fw-key\">{{ row.key }}</code>\n <span class=\"fw-type\">{{ row.type }}</span>\n <span class=\"fw-reason\">{{ row.reason }}</span>\n </div>\n <div class=\"fw-control\">\n @switch (row.type) {\n @case ('boolean') {\n <label class=\"fw-switch\">\n <input\n type=\"checkbox\"\n role=\"switch\"\n [checked]=\"row.value === true\"\n [attr.aria-label]=\"'Override ' + row.key\"\n (change)=\"setBoolean(row.key, $event)\"\n />\n <span>{{ row.value === true ? 'on' : 'off' }}</span>\n </label>\n }\n @case ('string') {\n <input\n class=\"fw-input\"\n type=\"text\"\n [value]=\"row.text\"\n [attr.aria-label]=\"'Override ' + row.key\"\n (change)=\"setString(row.key, $event)\"\n />\n }\n @case ('number') {\n <input\n class=\"fw-input\"\n type=\"number\"\n step=\"any\"\n [value]=\"row.text\"\n [attr.aria-label]=\"'Override ' + row.key\"\n (change)=\"setNumber(row.key, $event)\"\n />\n }\n @default {\n <textarea\n class=\"fw-input fw-json\"\n rows=\"3\"\n spellcheck=\"false\"\n [value]=\"row.text\"\n [attr.aria-label]=\"'Override ' + row.key + ' as JSON'\"\n [attr.aria-invalid]=\"invalidJson() === row.key\"\n (change)=\"setJson(row.key, $event)\"\n ></textarea>\n @if (invalidJson() === row.key) {\n <span class=\"fw-error\" role=\"alert\"\n >Not applied. Enter valid JSON: an object, an array or null.</span\n >\n }\n }\n }\n @if (row.overridden) {\n <button type=\"button\" class=\"fw-reset\" (click)=\"clear(row.key)\">\n Reset<span class=\"fw-sr\"> {{ row.key }}</span>\n </button>\n }\n </div>\n </li>\n }\n </ul>\n <button\n type=\"button\"\n class=\"fw-reset-all\"\n [disabled]=\"overriddenCount() === 0\"\n (click)=\"clearAll()\"\n >\n Reset all overrides\n </button>\n }\n </div>\n }\n </section>\n `,\n styles: `\n :host {\n position: fixed;\n right: 16px;\n bottom: 16px;\n z-index: 2147483000;\n font:\n 14px/1.4 system-ui,\n sans-serif;\n color: #f3f5f8;\n }\n .fw-panel {\n width: min(420px, calc(100vw - 32px));\n background: #1b1f27;\n border: 1px solid #4a5263;\n border-radius: 10px;\n box-shadow: 0 8px 28px rgb(0 0 0 / 35%);\n }\n .fw-toggle {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n align-items: center;\n width: 100%;\n padding: 10px 12px;\n font: inherit;\n color: inherit;\n text-align: left;\n background: transparent;\n border: 0;\n cursor: pointer;\n }\n .fw-count {\n padding: 0 8px;\n background: #3a4254;\n border-radius: 10px;\n }\n .fw-count-active {\n color: #1b1f27;\n background: #ffd24a;\n }\n .fw-status {\n margin-left: auto;\n }\n .fw-status::before {\n display: inline-block;\n width: 8px;\n height: 8px;\n margin-right: 6px;\n content: '';\n background: #9aa3b5;\n border-radius: 50%;\n }\n .fw-status[data-status='live']::before {\n background: #46d37c;\n }\n .fw-status[data-status='stale']::before {\n background: #ffd24a;\n border-radius: 2px;\n }\n .fw-status[data-status='offline']::before {\n background: #ff7a7a;\n border-radius: 0;\n }\n .fw-body {\n max-height: min(60vh, 520px);\n padding: 0 12px 12px;\n overflow: auto;\n border-top: 1px solid #4a5263;\n }\n .fw-list {\n padding: 0;\n margin: 8px 0;\n list-style: none;\n }\n .fw-row {\n padding: 8px 0;\n border-bottom: 1px solid #333a49;\n }\n .fw-overridden {\n border-left: 3px solid #ffd24a;\n padding-left: 8px;\n }\n .fw-meta {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n align-items: baseline;\n }\n .fw-key {\n font-weight: 600;\n }\n .fw-type,\n .fw-reason {\n color: #b6bdcc;\n font-size: 12px;\n }\n .fw-control {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n align-items: center;\n margin-top: 6px;\n }\n .fw-switch {\n display: inline-flex;\n gap: 8px;\n align-items: center;\n }\n .fw-input {\n flex: 1 1 160px;\n min-width: 0;\n padding: 6px 8px;\n font: inherit;\n color: #f3f5f8;\n background: #11141a;\n border: 1px solid #6b7488;\n border-radius: 6px;\n }\n .fw-json {\n font-family: ui-monospace, monospace;\n }\n .fw-error {\n flex-basis: 100%;\n color: #ffb3b3;\n }\n button {\n font: inherit;\n }\n .fw-reset,\n .fw-reset-all {\n padding: 4px 10px;\n color: #f3f5f8;\n background: #2c3342;\n border: 1px solid #6b7488;\n border-radius: 6px;\n cursor: pointer;\n }\n .fw-reset-all:disabled {\n color: #8b94a8;\n cursor: default;\n }\n .fw-empty {\n margin: 12px 0 0;\n color: #b6bdcc;\n }\n .fw-sr {\n position: absolute;\n width: 1px;\n height: 1px;\n overflow: hidden;\n clip-path: inset(50%);\n white-space: nowrap;\n }\n button:focus-visible,\n input:focus-visible,\n textarea:focus-visible {\n outline: 3px solid #ffd24a;\n outline-offset: 2px;\n }\n `,\n})\nexport class FlagtideOverridesPanel {\n protected readonly open = signal(false);\n protected readonly invalidJson = signal<string | null>(null);\n\n private readonly flagtide = inject(Flagtide);\n\n protected readonly status = this.flagtide.status;\n\n protected readonly rows = computed<readonly OverrideRow[]>(() => {\n this.flagtide.revision();\n const client = this.flagtide.client;\n return client\n .flagKeys()\n .map((key) => {\n const type = client.flagType(key) ?? 'json';\n const resolution = client.resolve(key, SAMPLE_FALLBACK[type]);\n return {\n key,\n type,\n value: resolution.value,\n text: describeValue(resolution.value),\n reason: resolution.reason,\n overridden: client.overrides.get(key) !== undefined,\n };\n })\n .sort((left, right) => left.key.localeCompare(right.key));\n });\n\n protected readonly overriddenCount = computed(() => this.rows().filter((row) => row.overridden).length);\n\n protected setBoolean(key: string, event: Event): void {\n this.apply(key, (event.target as HTMLInputElement).checked);\n }\n\n protected setString(key: string, event: Event): void {\n this.apply(key, (event.target as HTMLInputElement).value);\n }\n\n protected setNumber(key: string, event: Event): void {\n const raw = (event.target as HTMLInputElement).value.trim();\n const parsed = Number(raw);\n if (raw !== '' && Number.isFinite(parsed)) {\n this.apply(key, parsed);\n }\n }\n\n protected setJson(key: string, event: Event): void {\n let parsed: JsonValue;\n try {\n parsed = JSON.parse((event.target as HTMLTextAreaElement).value) as JsonValue;\n } catch {\n this.invalidJson.set(key);\n return;\n }\n if (!this.apply(key, parsed)) {\n this.invalidJson.set(key);\n }\n }\n\n protected clear(key: string): void {\n this.flagtide.client.overrides.clear(key);\n }\n\n protected clearAll(): void {\n this.flagtide.client.overrides.clearAll();\n }\n\n private apply(key: string, value: JsonValue): boolean {\n this.invalidJson.set(null);\n if (flagTypeOf(value) !== this.flagtide.client.flagType(key)) {\n return false;\n }\n this.flagtide.client.overrides.set(key, value);\n return true;\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { DestroyRef, Directive, Injectable, InjectionToken, PLATFORM_ID, TemplateRef, TransferState, ViewContainerRef, assertInInjectionContext, computed, effect, inject, input, makeEnvironmentProviders, makeStateKey, provideAppInitializer, signal, untracked } from "@angular/core";
|
|
3
|
+
import { createFlagtideClient, createMemoryStore, fetchSnapshot, jsonEquals } from "@flagtide/core";
|
|
4
|
+
import { isPlatformServer } from "@angular/common";
|
|
5
|
+
import { Router } from "@angular/router";
|
|
6
|
+
import { map, of, race, take, timer } from "rxjs";
|
|
7
|
+
const FLAGTIDE_CONFIG = new InjectionToken("FLAGTIDE_CONFIG");
|
|
8
|
+
const STREAM_PATH = /\/sdk\/v1\/stream\/?$/u;
|
|
9
|
+
function deriveSnapshotUrl(streamUrl) {
|
|
10
|
+
return streamUrl.replace(/^ws/u, "http").replace(STREAM_PATH, "");
|
|
11
|
+
}
|
|
12
|
+
const SNAPSHOT_KEY = makeStateKey("flagtide:snapshot");
|
|
13
|
+
var Flagtide = class Flagtide {
|
|
14
|
+
client;
|
|
15
|
+
revision;
|
|
16
|
+
status;
|
|
17
|
+
config = inject(FLAGTIDE_CONFIG);
|
|
18
|
+
transferState = inject(TransferState);
|
|
19
|
+
onServer = isPlatformServer(inject(PLATFORM_ID));
|
|
20
|
+
constructor() {
|
|
21
|
+
const transferred = this.transferState.get(SNAPSHOT_KEY, null);
|
|
22
|
+
this.client = createFlagtideClient({
|
|
23
|
+
streamUrl: this.config.streamUrl,
|
|
24
|
+
sdkKey: this.config.sdkKey,
|
|
25
|
+
context: typeof this.config.context === "function" ? this.config.context() : this.config.context,
|
|
26
|
+
store: this.onServer ? createMemoryStore() : this.config.storage,
|
|
27
|
+
initialSnapshot: transferred ?? void 0,
|
|
28
|
+
timing: this.config.timing,
|
|
29
|
+
socketFactory: this.config.socketFactory
|
|
30
|
+
});
|
|
31
|
+
const revision = signal(0, ...ngDevMode ? [{ debugName: "revision" }] : /* istanbul ignore next */ []);
|
|
32
|
+
const status = signal(this.client.status, ...ngDevMode ? [{ debugName: "status" }] : /* istanbul ignore next */ []);
|
|
33
|
+
this.revision = revision.asReadonly();
|
|
34
|
+
this.status = status.asReadonly();
|
|
35
|
+
const subscriptions = [this.client.changes$.subscribe(() => revision.update((count) => count + 1)), this.client.status$.subscribe((value) => status.set(value))];
|
|
36
|
+
inject(DestroyRef).onDestroy(() => {
|
|
37
|
+
subscriptions.forEach((subscription) => subscription.unsubscribe());
|
|
38
|
+
this.client.stop();
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
setContext(context) {
|
|
42
|
+
this.client.setContext(context);
|
|
43
|
+
}
|
|
44
|
+
async initialize() {
|
|
45
|
+
if (!this.onServer) {
|
|
46
|
+
this.client.start();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const snapshot = await fetchSnapshot(this.config.snapshotUrl ?? deriveSnapshotUrl(this.config.streamUrl), this.config.sdkKey);
|
|
51
|
+
this.client.hydrate(snapshot);
|
|
52
|
+
this.transferState.set(SNAPSHOT_KEY, snapshot);
|
|
53
|
+
} catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
58
|
+
minVersion: "12.0.0",
|
|
59
|
+
version: "22.2.1",
|
|
60
|
+
ngImport: i0,
|
|
61
|
+
type: Flagtide,
|
|
62
|
+
deps: [],
|
|
63
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
64
|
+
});
|
|
65
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
66
|
+
minVersion: "12.0.0",
|
|
67
|
+
version: "22.2.1",
|
|
68
|
+
ngImport: i0,
|
|
69
|
+
type: Flagtide
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
73
|
+
minVersion: "12.0.0",
|
|
74
|
+
version: "22.2.1",
|
|
75
|
+
ngImport: i0,
|
|
76
|
+
type: Flagtide,
|
|
77
|
+
decorators: [{ type: Injectable }],
|
|
78
|
+
ctorParameters: () => []
|
|
79
|
+
});
|
|
80
|
+
var FlagtideFlagDirective = class FlagtideFlagDirective {
|
|
81
|
+
flagtideFlag = input.required(...ngDevMode ? [{ debugName: "flagtideFlag" }] : /* istanbul ignore next */ []);
|
|
82
|
+
flagtideFlagEquals = input(true, ...ngDevMode ? [{ debugName: "flagtideFlagEquals" }] : /* istanbul ignore next */ []);
|
|
83
|
+
flagtideFlagElse = input(null, ...ngDevMode ? [{ debugName: "flagtideFlagElse" }] : /* istanbul ignore next */ []);
|
|
84
|
+
flagtide = inject(Flagtide);
|
|
85
|
+
container = inject(ViewContainerRef);
|
|
86
|
+
template = inject(TemplateRef);
|
|
87
|
+
rendered = null;
|
|
88
|
+
outcome = computed(() => {
|
|
89
|
+
this.flagtide.revision();
|
|
90
|
+
const expected = this.flagtideFlagEquals();
|
|
91
|
+
const resolution = this.flagtide.client.resolve(this.flagtideFlag(), expected);
|
|
92
|
+
return {
|
|
93
|
+
matches: resolution.reason !== "FLAG_NOT_FOUND" && resolution.reason !== "TYPE_MISMATCH" && jsonEquals(resolution.value, expected),
|
|
94
|
+
value: resolution.value
|
|
95
|
+
};
|
|
96
|
+
}, {
|
|
97
|
+
...ngDevMode ? { debugName: "outcome" } : /* istanbul ignore next */ {},
|
|
98
|
+
equal: (left, right) => left.matches === right.matches && jsonEquals(left.value, right.value)
|
|
99
|
+
});
|
|
100
|
+
constructor() {
|
|
101
|
+
effect(() => {
|
|
102
|
+
const { matches, value } = this.outcome();
|
|
103
|
+
const elseTemplate = this.flagtideFlagElse();
|
|
104
|
+
untracked(() => this.render(matches ? "then" : "else", matches ? this.template : elseTemplate, value));
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
render(kind, template, value) {
|
|
108
|
+
if (this.rendered === kind && kind === "then") return;
|
|
109
|
+
this.container.clear();
|
|
110
|
+
this.rendered = null;
|
|
111
|
+
if (template !== null) {
|
|
112
|
+
this.container.createEmbeddedView(template, {
|
|
113
|
+
$implicit: value,
|
|
114
|
+
flagtideFlag: value
|
|
115
|
+
});
|
|
116
|
+
this.rendered = kind;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
static ngTemplateContextGuard(directive, context) {
|
|
120
|
+
return directive !== null && context !== null;
|
|
121
|
+
}
|
|
122
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
123
|
+
minVersion: "12.0.0",
|
|
124
|
+
version: "22.2.1",
|
|
125
|
+
ngImport: i0,
|
|
126
|
+
type: FlagtideFlagDirective,
|
|
127
|
+
deps: [],
|
|
128
|
+
target: i0.ɵɵFactoryTarget.Directive
|
|
129
|
+
});
|
|
130
|
+
static ɵdir = i0.ɵɵngDeclareDirective({
|
|
131
|
+
minVersion: "17.1.0",
|
|
132
|
+
version: "22.2.1",
|
|
133
|
+
type: FlagtideFlagDirective,
|
|
134
|
+
isStandalone: true,
|
|
135
|
+
selector: "[flagtideFlag]",
|
|
136
|
+
inputs: {
|
|
137
|
+
flagtideFlag: {
|
|
138
|
+
classPropertyName: "flagtideFlag",
|
|
139
|
+
publicName: "flagtideFlag",
|
|
140
|
+
isSignal: true,
|
|
141
|
+
isRequired: true,
|
|
142
|
+
transformFunction: null
|
|
143
|
+
},
|
|
144
|
+
flagtideFlagEquals: {
|
|
145
|
+
classPropertyName: "flagtideFlagEquals",
|
|
146
|
+
publicName: "flagtideFlagEquals",
|
|
147
|
+
isSignal: true,
|
|
148
|
+
isRequired: false,
|
|
149
|
+
transformFunction: null
|
|
150
|
+
},
|
|
151
|
+
flagtideFlagElse: {
|
|
152
|
+
classPropertyName: "flagtideFlagElse",
|
|
153
|
+
publicName: "flagtideFlagElse",
|
|
154
|
+
isSignal: true,
|
|
155
|
+
isRequired: false,
|
|
156
|
+
transformFunction: null
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
ngImport: i0
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
163
|
+
minVersion: "12.0.0",
|
|
164
|
+
version: "22.2.1",
|
|
165
|
+
ngImport: i0,
|
|
166
|
+
type: FlagtideFlagDirective,
|
|
167
|
+
decorators: [{
|
|
168
|
+
type: Directive,
|
|
169
|
+
args: [{ selector: "[flagtideFlag]" }]
|
|
170
|
+
}],
|
|
171
|
+
ctorParameters: () => [],
|
|
172
|
+
propDecorators: {
|
|
173
|
+
flagtideFlag: [{
|
|
174
|
+
type: i0.Input,
|
|
175
|
+
args: [{
|
|
176
|
+
isSignal: true,
|
|
177
|
+
alias: "flagtideFlag",
|
|
178
|
+
required: true
|
|
179
|
+
}]
|
|
180
|
+
}],
|
|
181
|
+
flagtideFlagEquals: [{
|
|
182
|
+
type: i0.Input,
|
|
183
|
+
args: [{
|
|
184
|
+
isSignal: true,
|
|
185
|
+
alias: "flagtideFlagEquals",
|
|
186
|
+
required: false
|
|
187
|
+
}]
|
|
188
|
+
}],
|
|
189
|
+
flagtideFlagElse: [{
|
|
190
|
+
type: i0.Input,
|
|
191
|
+
args: [{
|
|
192
|
+
isSignal: true,
|
|
193
|
+
alias: "flagtideFlagElse",
|
|
194
|
+
required: false
|
|
195
|
+
}]
|
|
196
|
+
}]
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
const DEFAULT_WAIT_MS = 2e3;
|
|
200
|
+
function flagtideGuard(key, options = {}) {
|
|
201
|
+
return () => {
|
|
202
|
+
const flagtide = inject(Flagtide);
|
|
203
|
+
const router = inject(Router);
|
|
204
|
+
const ready = !isPlatformServer(inject(PLATFORM_ID)) && flagtide.client.version === null ? race(flagtide.client.changes$.pipe(take(1)), timer(options.waitMs ?? DEFAULT_WAIT_MS)) : of(true);
|
|
205
|
+
const expected = options.equals ?? true;
|
|
206
|
+
return ready.pipe(map(() => {
|
|
207
|
+
const resolution = flagtide.client.resolve(key, expected);
|
|
208
|
+
if (resolution.reason !== "FLAG_NOT_FOUND" && resolution.reason !== "TYPE_MISMATCH" && jsonEquals(resolution.value, expected)) return true;
|
|
209
|
+
return options.redirectTo === void 0 ? false : router.parseUrl(options.redirectTo);
|
|
210
|
+
}));
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
var FlagtideStatus = class FlagtideStatus {
|
|
214
|
+
status = inject(Flagtide).status;
|
|
215
|
+
static ɵfac = i0.ɵɵngDeclareFactory({
|
|
216
|
+
minVersion: "12.0.0",
|
|
217
|
+
version: "22.2.1",
|
|
218
|
+
ngImport: i0,
|
|
219
|
+
type: FlagtideStatus,
|
|
220
|
+
deps: [],
|
|
221
|
+
target: i0.ɵɵFactoryTarget.Injectable
|
|
222
|
+
});
|
|
223
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({
|
|
224
|
+
minVersion: "12.0.0",
|
|
225
|
+
version: "22.2.1",
|
|
226
|
+
ngImport: i0,
|
|
227
|
+
type: FlagtideStatus
|
|
228
|
+
});
|
|
229
|
+
};
|
|
230
|
+
i0.ɵɵngDeclareClassMetadata({
|
|
231
|
+
minVersion: "12.0.0",
|
|
232
|
+
version: "22.2.1",
|
|
233
|
+
ngImport: i0,
|
|
234
|
+
type: FlagtideStatus,
|
|
235
|
+
decorators: [{ type: Injectable }]
|
|
236
|
+
});
|
|
237
|
+
function injectFlag(key, fallback) {
|
|
238
|
+
assertInInjectionContext(injectFlag);
|
|
239
|
+
const flagtide = inject(Flagtide);
|
|
240
|
+
return computed(() => {
|
|
241
|
+
flagtide.revision();
|
|
242
|
+
return flagtide.client.value(key, fallback);
|
|
243
|
+
}, { equal: jsonEquals });
|
|
244
|
+
}
|
|
245
|
+
function provideFlagtide(config) {
|
|
246
|
+
return makeEnvironmentProviders([
|
|
247
|
+
{
|
|
248
|
+
provide: FLAGTIDE_CONFIG,
|
|
249
|
+
useValue: config
|
|
250
|
+
},
|
|
251
|
+
Flagtide,
|
|
252
|
+
FlagtideStatus,
|
|
253
|
+
provideAppInitializer(() => inject(Flagtide).initialize())
|
|
254
|
+
]);
|
|
255
|
+
}
|
|
256
|
+
export { Flagtide, FlagtideFlagDirective, FlagtideStatus, flagtideGuard, injectFlag, provideFlagtide };
|
|
257
|
+
|
|
258
|
+
//# sourceMappingURL=flagtide-angular.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flagtide-angular.mjs","names":[],"sources":["../../../../libs/angular/src/lib/config.ts","../../../../libs/angular/src/lib/flagtide.ts","../../../../libs/angular/src/lib/flag.directive.ts","../../../../libs/angular/src/lib/flag.guard.ts","../../../../libs/angular/src/lib/flagtide-status.ts","../../../../libs/angular/src/lib/inject-flag.ts","../../../../libs/angular/src/lib/provide-flagtide.ts"],"sourcesContent":["import { InjectionToken } from '@angular/core';\nimport type { ConnectionTiming, EvaluationContext, KeyValueStore, SocketFactory } from '@flagtide/core';\n\n/** Settings for {@link provideFlagtide}. */\nexport interface FlagtideConfig {\n /** The read-only SDK key of one environment. */\n readonly sdkKey: string;\n /** The stream endpoint the browser connects to, for example `ws://localhost:18081/sdk/v1/stream`. */\n readonly streamUrl: string;\n /**\n * The HTTP address the server side render fetches the snapshot from, without a trailing slash. Needed when\n * the server reaches the API under another address than the browser does, for example inside a container\n * network. Defaults to the stream URL with the scheme changed to `http` and the path removed.\n */\n readonly snapshotUrl?: string;\n /**\n * Who flags are evaluated for. A function runs in an injection context, so it can read a cookie or a request.\n * With server side rendering, use a context key that is the same on the server and in the browser, otherwise\n * a percentage rollout can render differently on both sides. Without a context the browser uses an anonymous\n * id kept in storage.\n */\n readonly context?: EvaluationContext | (() => EvaluationContext);\n /** Where the last snapshot and the overrides are kept in the browser. Defaults to `localStorage`. */\n readonly storage?: KeyValueStore;\n /** Backoff and heartbeat limits. The defaults are the constants of the stream protocol. */\n readonly timing?: Partial<ConnectionTiming>;\n /** Replaces how sockets are opened. Meant for tests and for runtimes without a global `WebSocket`. */\n readonly socketFactory?: SocketFactory;\n}\n\nexport const FLAGTIDE_CONFIG = new InjectionToken<FlagtideConfig>('FLAGTIDE_CONFIG');\n\nconst STREAM_PATH = /\\/sdk\\/v1\\/stream\\/?$/u;\n\nexport function deriveSnapshotUrl(streamUrl: string): string {\n return streamUrl.replace(/^ws/u, 'http').replace(STREAM_PATH, '');\n}\n","import { isPlatformServer } from '@angular/common';\nimport {\n DestroyRef,\n Injectable,\n PLATFORM_ID,\n TransferState,\n inject,\n makeStateKey,\n signal,\n} from '@angular/core';\nimport type { Signal } from '@angular/core';\nimport { createFlagtideClient, createMemoryStore, fetchSnapshot } from '@flagtide/core';\nimport type { ConnectionStatus, EvaluationContext, FlagSnapshot, FlagtideClient } from '@flagtide/core';\nimport { FLAGTIDE_CONFIG, deriveSnapshotUrl } from './config';\n\nconst SNAPSHOT_KEY = makeStateKey<FlagSnapshot>('flagtide:snapshot');\n\n/**\n * The service behind `injectFlag`, the directive and the guard. It owns the {@link FlagtideClient} and exposes\n * what changes as signals. Injecting it directly is for tooling such as the overrides panel.\n */\n@Injectable()\nexport class Flagtide {\n /** The framework-agnostic client. */\n readonly client: FlagtideClient;\n /** Counts every change that can alter a flag value: a snapshot, a delta, an override or a new context. */\n readonly revision: Signal<number>;\n /** The connection status of the stream. */\n readonly status: Signal<ConnectionStatus>;\n\n private readonly config = inject(FLAGTIDE_CONFIG);\n private readonly transferState = inject(TransferState);\n private readonly onServer = isPlatformServer(inject(PLATFORM_ID));\n\n constructor() {\n const transferred = this.transferState.get(SNAPSHOT_KEY, null);\n this.client = createFlagtideClient({\n streamUrl: this.config.streamUrl,\n sdkKey: this.config.sdkKey,\n context: typeof this.config.context === 'function' ? this.config.context() : this.config.context,\n store: this.onServer ? createMemoryStore() : this.config.storage,\n initialSnapshot: transferred ?? undefined,\n timing: this.config.timing,\n socketFactory: this.config.socketFactory,\n });\n const revision = signal(0);\n const status = signal<ConnectionStatus>(this.client.status);\n this.revision = revision.asReadonly();\n this.status = status.asReadonly();\n const subscriptions = [\n this.client.changes$.subscribe(() => revision.update((count) => count + 1)),\n this.client.status$.subscribe((value) => status.set(value)),\n ];\n inject(DestroyRef).onDestroy(() => {\n subscriptions.forEach((subscription) => subscription.unsubscribe());\n this.client.stop();\n });\n }\n\n /** Changes who flags are evaluated for. */\n setContext(context: EvaluationContext): void {\n this.client.setContext(context);\n }\n\n /**\n * Prepares the client. On the server it fetches the snapshot over HTTP and puts it into `TransferState`. In the\n * browser it opens the stream. A server that cannot be reached leaves the client empty, so flags return their\n * fallbacks instead of failing the render.\n */\n async initialize(): Promise<void> {\n if (!this.onServer) {\n this.client.start();\n return;\n }\n try {\n const snapshot = await fetchSnapshot(\n this.config.snapshotUrl ?? deriveSnapshotUrl(this.config.streamUrl),\n this.config.sdkKey,\n );\n this.client.hydrate(snapshot);\n this.transferState.set(SNAPSHOT_KEY, snapshot);\n } catch {\n return;\n }\n }\n}\n","import {\n Directive,\n TemplateRef,\n ViewContainerRef,\n computed,\n effect,\n inject,\n input,\n untracked,\n} from '@angular/core';\nimport { jsonEquals } from '@flagtide/core';\nimport type { JsonValue } from '@flagtide/core';\nimport { Flagtide } from './flagtide';\n\n/** What a template of `*flagtideFlag` can read: the current value of the flag. */\nexport interface FlagtideFlagContext {\n $implicit: JsonValue;\n flagtideFlag: JsonValue;\n}\n\ntype Rendered = 'then' | 'else' | null;\n\n/**\n * Renders its template while a flag has the expected value, and the optional `else` template otherwise.\n *\n * ```html\n * <app-promo *flagtideFlag=\"'promo-banner'; else plain\" />\n * <ng-template #plain>...</ng-template>\n * <p *flagtideFlag=\"'checkout-label'; equals: 'Buy now'\">...</p>\n * ```\n *\n * `equals` defaults to `true`. A flag that is unknown or of another type never matches.\n */\n@Directive({ selector: '[flagtideFlag]' })\nexport class FlagtideFlagDirective {\n readonly flagtideFlag = input.required<string>();\n readonly flagtideFlagEquals = input<JsonValue>(true);\n readonly flagtideFlagElse = input<TemplateRef<FlagtideFlagContext> | null>(null);\n\n private readonly flagtide = inject(Flagtide);\n private readonly container = inject(ViewContainerRef);\n private readonly template = inject<TemplateRef<FlagtideFlagContext>>(TemplateRef);\n private rendered: Rendered = null;\n\n private readonly outcome = computed(\n () => {\n this.flagtide.revision();\n const expected = this.flagtideFlagEquals();\n const resolution = this.flagtide.client.resolve(this.flagtideFlag(), expected);\n const known = resolution.reason !== 'FLAG_NOT_FOUND' && resolution.reason !== 'TYPE_MISMATCH';\n return { matches: known && jsonEquals(resolution.value, expected), value: resolution.value };\n },\n { equal: (left, right) => left.matches === right.matches && jsonEquals(left.value, right.value) },\n );\n\n constructor() {\n effect(() => {\n const { matches, value } = this.outcome();\n const elseTemplate = this.flagtideFlagElse();\n untracked(() => this.render(matches ? 'then' : 'else', matches ? this.template : elseTemplate, value));\n });\n }\n\n private render(\n kind: Exclude<Rendered, null>,\n template: TemplateRef<FlagtideFlagContext> | null,\n value: JsonValue,\n ): void {\n if (this.rendered === kind && kind === 'then') {\n return;\n }\n this.container.clear();\n this.rendered = null;\n if (template !== null) {\n this.container.createEmbeddedView(template, { $implicit: value, flagtideFlag: value });\n this.rendered = kind;\n }\n }\n\n static ngTemplateContextGuard(\n directive: FlagtideFlagDirective,\n context: unknown,\n ): context is FlagtideFlagContext {\n return directive !== null && context !== null;\n }\n}\n","import { isPlatformServer } from '@angular/common';\nimport { PLATFORM_ID, inject } from '@angular/core';\nimport { Router } from '@angular/router';\nimport type { CanMatchFn, UrlTree } from '@angular/router';\nimport { jsonEquals } from '@flagtide/core';\nimport type { JsonValue } from '@flagtide/core';\nimport { map, of, race, take, timer } from 'rxjs';\nimport type { Observable } from 'rxjs';\nimport { Flagtide } from './flagtide';\n\n/** Options of {@link flagtideGuard}. */\nexport interface FlagtideGuardOptions {\n /** The value that lets the route match. Defaults to `true`. */\n readonly equals?: JsonValue;\n /** Where to send the user when the flag does not match, as a URL. Without it the route simply does not match. */\n readonly redirectTo?: string;\n /**\n * How long to wait for the first flags when the browser has none yet, in milliseconds. Defaults to 2000. After\n * that the guard decides with the fallback, which never matches an unknown flag.\n */\n readonly waitMs?: number;\n}\n\nconst DEFAULT_WAIT_MS = 2000;\n\n/**\n * A `canMatch` guard that lets a route match only while a flag has the expected value. The flag is read when\n * the navigation happens.\n *\n * ```ts\n * { path: 'beta', canMatch: [flagtideGuard('beta-recommendations', { redirectTo: '/' })], loadComponent: ... }\n * ```\n */\nexport function flagtideGuard(key: string, options: FlagtideGuardOptions = {}): CanMatchFn {\n return (): Observable<boolean | UrlTree> => {\n const flagtide = inject(Flagtide);\n const router = inject(Router);\n const waits = !isPlatformServer(inject(PLATFORM_ID)) && flagtide.client.version === null;\n const ready: Observable<unknown> = waits\n ? race(flagtide.client.changes$.pipe(take(1)), timer(options.waitMs ?? DEFAULT_WAIT_MS))\n : of(true);\n const expected = options.equals ?? true;\n return ready.pipe(\n map(() => {\n const resolution = flagtide.client.resolve(key, expected);\n const known = resolution.reason !== 'FLAG_NOT_FOUND' && resolution.reason !== 'TYPE_MISMATCH';\n if (known && jsonEquals(resolution.value, expected)) {\n return true;\n }\n return options.redirectTo === undefined ? false : router.parseUrl(options.redirectTo);\n }),\n );\n };\n}\n","import { Injectable, inject } from '@angular/core';\nimport type { Signal } from '@angular/core';\nimport type { ConnectionStatus } from '@flagtide/core';\nimport { Flagtide } from './flagtide';\n\n/**\n * The connection status as a signal.\n *\n * - `connecting`: the first attempt is in progress and nothing is confirmed yet\n * - `live`: the stream is open and the data is confirmed current\n * - `stale`: flags are served but cannot be confirmed current, for example from storage or after a dropped connection\n * - `offline`: the browser has no network or the server has been unreachable for too long\n */\n@Injectable()\nexport class FlagtideStatus {\n readonly status: Signal<ConnectionStatus> = inject(Flagtide).status;\n}\n","import { assertInInjectionContext, computed, inject } from '@angular/core';\nimport type { Signal } from '@angular/core';\nimport { jsonEquals } from '@flagtide/core';\nimport type { JsonValue } from '@flagtide/core';\nimport { Flagtide } from './flagtide';\n\n/**\n * A signal with the value of a flag. It is evaluated locally and updates when the server pushes a change, when\n * an override is set and when the context changes. The fallback is returned while the flag is unknown and when\n * its type differs from the type of the fallback.\n *\n * Call it in an injection context, for example a field initializer of a component.\n *\n * ```ts\n * readonly showBanner = injectFlag('header-banner', false);\n * ```\n */\nexport function injectFlag(key: string, fallback: boolean): Signal<boolean>;\nexport function injectFlag(key: string, fallback: string): Signal<string>;\nexport function injectFlag(key: string, fallback: number): Signal<number>;\nexport function injectFlag<T extends JsonValue>(key: string, fallback: T): Signal<T>;\nexport function injectFlag<T extends JsonValue>(key: string, fallback: T): Signal<T> {\n assertInInjectionContext(injectFlag);\n const flagtide = inject(Flagtide);\n return computed(\n () => {\n flagtide.revision();\n return flagtide.client.value(key, fallback);\n },\n { equal: jsonEquals },\n );\n}\n","import { makeEnvironmentProviders, provideAppInitializer, inject } from '@angular/core';\nimport type { EnvironmentProviders } from '@angular/core';\nimport { FLAGTIDE_CONFIG } from './config';\nimport type { FlagtideConfig } from './config';\nimport { Flagtide } from './flagtide';\nimport { FlagtideStatus } from './flagtide-status';\n\n/**\n * Registers the SDK. Add it to `ApplicationConfig.providers`.\n *\n * In the browser it opens the stream and starts from the last stored snapshot, or from the snapshot the server\n * rendered. On the server it opens no socket, touches no browser storage, fetches the snapshot over HTTP before\n * the first render and hands it to the browser through `TransferState`.\n */\nexport function provideFlagtide(config: FlagtideConfig): EnvironmentProviders {\n return makeEnvironmentProviders([\n { provide: FLAGTIDE_CONFIG, useValue: config },\n Flagtide,\n FlagtideStatus,\n provideAppInitializer(() => inject(Flagtide).initialize()),\n ]);\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,51 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flagtide/angular",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Signals-first Angular SDK for flagtide: injectFlag, a structural directive, a canMatch guard, a connection status signal and a dev overrides panel. Zoneless and SSR ready.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Adrian Turbiński",
|
|
7
|
+
"homepage": "https://github.com/adiyy2001/flagtide/tree/main/libs/angular#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/adiyy2001/flagtide.git",
|
|
11
|
+
"directory": "libs/angular"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/adiyy2001/flagtide/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"angular",
|
|
18
|
+
"feature-flags",
|
|
19
|
+
"signals",
|
|
20
|
+
"zoneless",
|
|
21
|
+
"ssr",
|
|
22
|
+
"websocket"
|
|
23
|
+
],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"@angular/common": "^22.0.0",
|
|
27
|
+
"@angular/core": "^22.0.0",
|
|
28
|
+
"@angular/router": "^22.0.0",
|
|
29
|
+
"@flagtide/core": "^1.0.0",
|
|
30
|
+
"rxjs": "^7.8.0"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"tslib": "^2.3.0"
|
|
34
|
+
},
|
|
35
|
+
"module": "fesm2022/flagtide-angular.mjs",
|
|
36
|
+
"typings": "types/flagtide-angular.d.ts",
|
|
37
|
+
"exports": {
|
|
38
|
+
"./package.json": {
|
|
39
|
+
"default": "./package.json"
|
|
40
|
+
},
|
|
41
|
+
".": {
|
|
42
|
+
"types": "./types/flagtide-angular.d.ts",
|
|
43
|
+
"default": "./fesm2022/flagtide-angular.mjs"
|
|
44
|
+
},
|
|
45
|
+
"./overrides": {
|
|
46
|
+
"types": "./types/flagtide-angular-overrides.d.ts",
|
|
47
|
+
"default": "./fesm2022/flagtide-angular-overrides.mjs"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"type": "module"
|
|
6
51
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { FlagType, JsonValue, ResolutionReason } from "@flagtide/core";
|
|
2
|
+
import * as i0 from "@angular/core";
|
|
3
|
+
interface OverrideRow {
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly type: FlagType;
|
|
6
|
+
readonly value: JsonValue;
|
|
7
|
+
readonly text: string;
|
|
8
|
+
readonly reason: ResolutionReason;
|
|
9
|
+
readonly overridden: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* A development panel that lists every flag the client knows and lets you override its value in this browser.
|
|
13
|
+
* Overrides win over server values, are reported with the reason `OVERRIDE` and are kept in browser storage.
|
|
14
|
+
* Place it once, near the root of the application, for example behind a development check.
|
|
15
|
+
*
|
|
16
|
+
* ```html
|
|
17
|
+
* <flagtide-overrides-panel />
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare class FlagtideOverridesPanel {
|
|
21
|
+
protected readonly open: import("@angular/core").WritableSignal<boolean>;
|
|
22
|
+
protected readonly invalidJson: import("@angular/core").WritableSignal<string | null>;
|
|
23
|
+
private readonly flagtide;
|
|
24
|
+
protected readonly status: import("@angular/core").Signal<import("@flagtide/core").ConnectionStatus>;
|
|
25
|
+
protected readonly rows: import("@angular/core").Signal<readonly OverrideRow[]>;
|
|
26
|
+
protected readonly overriddenCount: import("@angular/core").Signal<number>;
|
|
27
|
+
protected setBoolean(key: string, event: Event): void;
|
|
28
|
+
protected setString(key: string, event: Event): void;
|
|
29
|
+
protected setNumber(key: string, event: Event): void;
|
|
30
|
+
protected setJson(key: string, event: Event): void;
|
|
31
|
+
protected clear(key: string): void;
|
|
32
|
+
protected clearAll(): void;
|
|
33
|
+
private apply;
|
|
34
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<FlagtideOverridesPanel, never>;
|
|
35
|
+
static ɵcmp: i0.ɵɵComponentDeclaration<FlagtideOverridesPanel, "flagtide-overrides-panel", never, {}, {}, never, never, true, never>;
|
|
36
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as i0 from "@angular/core";
|
|
2
|
+
import { EnvironmentProviders, InjectionToken, Signal, TemplateRef } from "@angular/core";
|
|
3
|
+
import { ConnectionStatus, ConnectionStatus as ConnectionStatus$1, ConnectionTiming, EvaluationContext, EvaluationContext as EvaluationContext$1, FlagtideClient, JsonValue, JsonValue as JsonValue$1, KeyValueStore, SocketFactory } from "@flagtide/core";
|
|
4
|
+
import { CanMatchFn } from "@angular/router";
|
|
5
|
+
/** Settings for {@link provideFlagtide}. */
|
|
6
|
+
interface FlagtideConfig {
|
|
7
|
+
/** The read-only SDK key of one environment. */
|
|
8
|
+
readonly sdkKey: string;
|
|
9
|
+
/** The stream endpoint the browser connects to, for example `ws://localhost:18081/sdk/v1/stream`. */
|
|
10
|
+
readonly streamUrl: string;
|
|
11
|
+
/**
|
|
12
|
+
* The HTTP address the server side render fetches the snapshot from, without a trailing slash. Needed when
|
|
13
|
+
* the server reaches the API under another address than the browser does, for example inside a container
|
|
14
|
+
* network. Defaults to the stream URL with the scheme changed to `http` and the path removed.
|
|
15
|
+
*/
|
|
16
|
+
readonly snapshotUrl?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Who flags are evaluated for. A function runs in an injection context, so it can read a cookie or a request.
|
|
19
|
+
* With server side rendering, use a context key that is the same on the server and in the browser, otherwise
|
|
20
|
+
* a percentage rollout can render differently on both sides. Without a context the browser uses an anonymous
|
|
21
|
+
* id kept in storage.
|
|
22
|
+
*/
|
|
23
|
+
readonly context?: EvaluationContext$1 | (() => EvaluationContext$1);
|
|
24
|
+
/** Where the last snapshot and the overrides are kept in the browser. Defaults to `localStorage`. */
|
|
25
|
+
readonly storage?: KeyValueStore;
|
|
26
|
+
/** Backoff and heartbeat limits. The defaults are the constants of the stream protocol. */
|
|
27
|
+
readonly timing?: Partial<ConnectionTiming>;
|
|
28
|
+
/** Replaces how sockets are opened. Meant for tests and for runtimes without a global `WebSocket`. */
|
|
29
|
+
readonly socketFactory?: SocketFactory;
|
|
30
|
+
}
|
|
31
|
+
declare const FLAGTIDE_CONFIG: InjectionToken<FlagtideConfig>;
|
|
32
|
+
declare function deriveSnapshotUrl(streamUrl: string): string;
|
|
33
|
+
/** What a template of `*flagtideFlag` can read: the current value of the flag. */
|
|
34
|
+
interface FlagtideFlagContext {
|
|
35
|
+
$implicit: JsonValue$1;
|
|
36
|
+
flagtideFlag: JsonValue$1;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Renders its template while a flag has the expected value, and the optional `else` template otherwise.
|
|
40
|
+
*
|
|
41
|
+
* ```html
|
|
42
|
+
* <app-promo *flagtideFlag="'promo-banner'; else plain" />
|
|
43
|
+
* <ng-template #plain>...</ng-template>
|
|
44
|
+
* <p *flagtideFlag="'checkout-label'; equals: 'Buy now'">...</p>
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* `equals` defaults to `true`. A flag that is unknown or of another type never matches.
|
|
48
|
+
*/
|
|
49
|
+
export declare class FlagtideFlagDirective {
|
|
50
|
+
readonly flagtideFlag: import("@angular/core").InputSignal<string>;
|
|
51
|
+
readonly flagtideFlagEquals: import("@angular/core").InputSignal<JsonValue$1>;
|
|
52
|
+
readonly flagtideFlagElse: import("@angular/core").InputSignal<TemplateRef<FlagtideFlagContext> | null>;
|
|
53
|
+
private readonly flagtide;
|
|
54
|
+
private readonly container;
|
|
55
|
+
private readonly template;
|
|
56
|
+
private rendered;
|
|
57
|
+
private readonly outcome;
|
|
58
|
+
constructor();
|
|
59
|
+
private render;
|
|
60
|
+
static ngTemplateContextGuard(directive: FlagtideFlagDirective, context: unknown): context is FlagtideFlagContext;
|
|
61
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<FlagtideFlagDirective, never>;
|
|
62
|
+
static ɵdir: i0.ɵɵDirectiveDeclaration<FlagtideFlagDirective, "[flagtideFlag]", never, {
|
|
63
|
+
"flagtideFlag": {
|
|
64
|
+
"alias": "flagtideFlag";
|
|
65
|
+
"required": true;
|
|
66
|
+
"isSignal": true;
|
|
67
|
+
};
|
|
68
|
+
"flagtideFlagEquals": {
|
|
69
|
+
"alias": "flagtideFlagEquals";
|
|
70
|
+
"required": false;
|
|
71
|
+
"isSignal": true;
|
|
72
|
+
};
|
|
73
|
+
"flagtideFlagElse": {
|
|
74
|
+
"alias": "flagtideFlagElse";
|
|
75
|
+
"required": false;
|
|
76
|
+
"isSignal": true;
|
|
77
|
+
};
|
|
78
|
+
}, {}, never, never, true, never>;
|
|
79
|
+
}
|
|
80
|
+
/** Options of {@link flagtideGuard}. */
|
|
81
|
+
interface FlagtideGuardOptions {
|
|
82
|
+
/** The value that lets the route match. Defaults to `true`. */
|
|
83
|
+
readonly equals?: JsonValue$1;
|
|
84
|
+
/** Where to send the user when the flag does not match, as a URL. Without it the route simply does not match. */
|
|
85
|
+
readonly redirectTo?: string;
|
|
86
|
+
/**
|
|
87
|
+
* How long to wait for the first flags when the browser has none yet, in milliseconds. Defaults to 2000. After
|
|
88
|
+
* that the guard decides with the fallback, which never matches an unknown flag.
|
|
89
|
+
*/
|
|
90
|
+
readonly waitMs?: number;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* A `canMatch` guard that lets a route match only while a flag has the expected value. The flag is read when
|
|
94
|
+
* the navigation happens.
|
|
95
|
+
*
|
|
96
|
+
* ```ts
|
|
97
|
+
* { path: 'beta', canMatch: [flagtideGuard('beta-recommendations', { redirectTo: '/' })], loadComponent: ... }
|
|
98
|
+
* ```
|
|
99
|
+
*/
|
|
100
|
+
export declare function flagtideGuard(key: string, options?: FlagtideGuardOptions): CanMatchFn;
|
|
101
|
+
/**
|
|
102
|
+
* The service behind `injectFlag`, the directive and the guard. It owns the {@link FlagtideClient} and exposes
|
|
103
|
+
* what changes as signals. Injecting it directly is for tooling such as the overrides panel.
|
|
104
|
+
*/
|
|
105
|
+
export declare class Flagtide {
|
|
106
|
+
/** The framework-agnostic client. */
|
|
107
|
+
readonly client: FlagtideClient;
|
|
108
|
+
/** Counts every change that can alter a flag value: a snapshot, a delta, an override or a new context. */
|
|
109
|
+
readonly revision: Signal<number>;
|
|
110
|
+
/** The connection status of the stream. */
|
|
111
|
+
readonly status: Signal<ConnectionStatus$1>;
|
|
112
|
+
private readonly config;
|
|
113
|
+
private readonly transferState;
|
|
114
|
+
private readonly onServer;
|
|
115
|
+
constructor();
|
|
116
|
+
/** Changes who flags are evaluated for. */
|
|
117
|
+
setContext(context: EvaluationContext$1): void;
|
|
118
|
+
/**
|
|
119
|
+
* Prepares the client. On the server it fetches the snapshot over HTTP and puts it into `TransferState`. In the
|
|
120
|
+
* browser it opens the stream. A server that cannot be reached leaves the client empty, so flags return their
|
|
121
|
+
* fallbacks instead of failing the render.
|
|
122
|
+
*/
|
|
123
|
+
initialize(): Promise<void>;
|
|
124
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<Flagtide, never>;
|
|
125
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* The connection status as a signal.
|
|
129
|
+
*
|
|
130
|
+
* - `connecting`: the first attempt is in progress and nothing is confirmed yet
|
|
131
|
+
* - `live`: the stream is open and the data is confirmed current
|
|
132
|
+
* - `stale`: flags are served but cannot be confirmed current, for example from storage or after a dropped connection
|
|
133
|
+
* - `offline`: the browser has no network or the server has been unreachable for too long
|
|
134
|
+
*/
|
|
135
|
+
export declare class FlagtideStatus {
|
|
136
|
+
readonly status: Signal<ConnectionStatus$1>;
|
|
137
|
+
static ɵfac: i0.ɵɵFactoryDeclaration<FlagtideStatus, never>;
|
|
138
|
+
static ɵprov: i0.ɵɵInjectableDeclaration<any>;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* A signal with the value of a flag. It is evaluated locally and updates when the server pushes a change, when
|
|
142
|
+
* an override is set and when the context changes. The fallback is returned while the flag is unknown and when
|
|
143
|
+
* its type differs from the type of the fallback.
|
|
144
|
+
*
|
|
145
|
+
* Call it in an injection context, for example a field initializer of a component.
|
|
146
|
+
*
|
|
147
|
+
* ```ts
|
|
148
|
+
* readonly showBanner = injectFlag('header-banner', false);
|
|
149
|
+
* ```
|
|
150
|
+
*/
|
|
151
|
+
export declare function injectFlag(key: string, fallback: boolean): Signal<boolean>;
|
|
152
|
+
export declare function injectFlag(key: string, fallback: string): Signal<string>;
|
|
153
|
+
export declare function injectFlag(key: string, fallback: number): Signal<number>;
|
|
154
|
+
export declare function injectFlag<T extends JsonValue$1>(key: string, fallback: T): Signal<T>;
|
|
155
|
+
/**
|
|
156
|
+
* Registers the SDK. Add it to `ApplicationConfig.providers`.
|
|
157
|
+
*
|
|
158
|
+
* In the browser it opens the stream and starts from the last stored snapshot, or from the snapshot the server
|
|
159
|
+
* rendered. On the server it opens no socket, touches no browser storage, fetches the snapshot over HTTP before
|
|
160
|
+
* the first render and hands it to the browser through `TransferState`.
|
|
161
|
+
*/
|
|
162
|
+
export declare function provideFlagtide(config: FlagtideConfig): EnvironmentProviders;
|
|
163
|
+
export type { ConnectionStatus, EvaluationContext, FlagtideConfig, FlagtideFlagContext, FlagtideGuardOptions, JsonValue };
|