@ultimat3/flags 2.0.0 → 3.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.
Files changed (2) hide show
  1. package/package.json +2 -2
  2. package/src/registry.ts +12 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/flags",
3
- "version": "2.0.0",
3
+ "version": "3.0.0",
4
4
  "description": "Feature flags: permanent switches, and temporary ones that cannot be forgotten",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,6 +31,6 @@
31
31
  "test": "bun test"
32
32
  },
33
33
  "dependencies": {
34
- "@ultimat3/core": "2.0.0"
34
+ "@ultimat3/core": "3.0.0"
35
35
  }
36
36
  }
package/src/registry.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  import { flagDuplicate, flagUnknown } from './errors';
6
6
  import type { Flag, FlagDef } from './flag';
7
7
  import { toFlag, withTargeting } from './flag';
8
- import type { FlagTargeting } from './targeting';
8
+ import { assertTargeting, type FlagTargeting } from './targeting';
9
9
 
10
10
  const flags = new Map<string, Flag>();
11
11
 
@@ -54,11 +54,16 @@ export interface SnapshotResult {
54
54
  * kill switch that refuses to land because the payload also mentioned tomorrow's flag is a kill
55
55
  * switch that does not work on the day it is needed. A bad *targeting* still throws — landing a
56
56
  * `rollout: 0.5` would silently switch a feature off for everyone.
57
+ *
58
+ * **Two passes, because the throw above is only worth anything if nothing landed.** Validating and
59
+ * writing in one loop retargeted every key ahead of the bad one and then threw, so the caller — a
60
+ * poller, a job or a realtime channel — saw a failure and had no record that half the fleet's
61
+ * flags had already moved. Nothing here awaits, so no other code observes the gap between passes.
57
62
  */
58
63
  export function applyFlagSnapshot(
59
64
  snapshot: Readonly<Record<string, FlagTargeting>>,
60
65
  ): SnapshotResult {
61
- const applied: string[] = [];
66
+ const declared: [string, Flag, FlagTargeting][] = [];
62
67
  const unknown: string[] = [];
63
68
  for (const [key, targeting] of Object.entries(snapshot)) {
64
69
  const flag = flags.get(key);
@@ -66,6 +71,11 @@ export function applyFlagSnapshot(
66
71
  unknown.push(key);
67
72
  continue;
68
73
  }
74
+ assertTargeting(key, targeting);
75
+ declared.push([key, flag, targeting]);
76
+ }
77
+ const applied: string[] = [];
78
+ for (const [key, flag, targeting] of declared) {
69
79
  flags.set(key, withTargeting(flag, targeting));
70
80
  applied.push(key);
71
81
  }