flagmint-js-sdk 2.2.2 → 2.2.3-rc-2
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 +20 -6
- package/dist/flagmint.cjs.js +297 -42
- package/dist/flagmint.cjs.js.map +1 -1
- package/dist/flagmint.es.js +297 -42
- package/dist/flagmint.es.js.map +1 -1
- package/dist/flagmint.umd.js +297 -42
- package/dist/flagmint.umd.js.map +1 -1
- package/dist/sdk/core/client.d.ts +17 -1
- package/dist/sdk/core/config-sync/index.d.ts +1 -1
- package/dist/sdk/core/config-sync/piiHash.d.ts +46 -0
- package/dist/sdk/core/config-sync/rulesStore.d.ts +7 -1
- package/dist/sdk/core/config-sync/types.d.ts +5 -0
- package/dist/sdk/core/helpers/connectionShare.d.ts +73 -22
- package/package.json +1 -1
package/dist/flagmint.es.js
CHANGED
|
@@ -39091,7 +39091,7 @@ class SseTransport {
|
|
|
39091
39091
|
this.connectPromise = new Promise((resolve, reject) => {
|
|
39092
39092
|
this.connectResolve = resolve;
|
|
39093
39093
|
this.connectReject = reject;
|
|
39094
|
-
const sdkVersion = "2.2.2";
|
|
39094
|
+
const sdkVersion = "2.2.3-rc-2";
|
|
39095
39095
|
const platform2 = typeof window !== "undefined" ? "browser" : "nodejs";
|
|
39096
39096
|
const wrapperName = this.configOptions?.wrapper?.name || "native-js";
|
|
39097
39097
|
const wrapperVersion = this.configOptions?.wrapper?.version || "none";
|
|
@@ -39360,11 +39360,15 @@ class ConnectionShareHub {
|
|
|
39360
39360
|
this.watchId = null;
|
|
39361
39361
|
this.closed = false;
|
|
39362
39362
|
this.latestFlags = null;
|
|
39363
|
+
this.latestRules = null;
|
|
39363
39364
|
this.flagsWaiters = [];
|
|
39365
|
+
this.rulesWaiters = [];
|
|
39364
39366
|
this.flagsTimeouts = [];
|
|
39367
|
+
this.rulesTimeouts = [];
|
|
39365
39368
|
this.contextWaiters = /* @__PURE__ */ new Map();
|
|
39366
|
-
this.
|
|
39367
|
-
this.
|
|
39369
|
+
this.mode = options.mode ?? "evaluated";
|
|
39370
|
+
this.lockKey = this.mode === "rules" ? `flagmint_share_rules_lock:${apiKey}` : `flagmint_share_lock:${apiKey}`;
|
|
39371
|
+
this.channelName = this.mode === "rules" ? `flagmint-share-rules:${apiKey}` : `flagmint-share:${apiKey}`;
|
|
39368
39372
|
this.now = options.now ?? (() => Date.now());
|
|
39369
39373
|
this.storage = options.storage ?? (typeof localStorage !== "undefined" ? localStorage : null);
|
|
39370
39374
|
this.channelFactory = options.channelFactory;
|
|
@@ -39376,6 +39380,10 @@ class ConnectionShareHub {
|
|
|
39376
39380
|
get currentRole() {
|
|
39377
39381
|
return this.role;
|
|
39378
39382
|
}
|
|
39383
|
+
/** `evaluated` (server eval) or `rules` (config sync). */
|
|
39384
|
+
get shareMode() {
|
|
39385
|
+
return this.mode;
|
|
39386
|
+
}
|
|
39379
39387
|
/**
|
|
39380
39388
|
* Join the share group: compete for the lock, subscribe to the channel.
|
|
39381
39389
|
*
|
|
@@ -39419,16 +39427,57 @@ class ConnectionShareHub {
|
|
|
39419
39427
|
setLeaderContextHandler(handler) {
|
|
39420
39428
|
this.leaderContextHandler = handler;
|
|
39421
39429
|
}
|
|
39430
|
+
/**
|
|
39431
|
+
* Rules-mode follower: hydrate local rules from the leader snapshot and
|
|
39432
|
+
* return flags evaluated for **this** document's context.
|
|
39433
|
+
*
|
|
39434
|
+
* @param handler Apply snapshot + local eval → flag map
|
|
39435
|
+
*/
|
|
39436
|
+
setFollowerRulesApply(handler) {
|
|
39437
|
+
this.followerRulesApply = handler;
|
|
39438
|
+
}
|
|
39439
|
+
/**
|
|
39440
|
+
* Rules-mode follower: re-evaluate after local `updateContext` (no RPC).
|
|
39441
|
+
*
|
|
39442
|
+
* @param handler Context → flag map using this tab's rules store
|
|
39443
|
+
*/
|
|
39444
|
+
setFollowerLocalEval(handler) {
|
|
39445
|
+
this.followerLocalEval = handler;
|
|
39446
|
+
}
|
|
39422
39447
|
/**
|
|
39423
39448
|
* Leader-only: publish the current evaluated flags to all followers.
|
|
39424
|
-
* No-op on followers (avoids echo loops).
|
|
39449
|
+
* No-op on followers (avoids echo loops). Evaluated mode only.
|
|
39450
|
+
*
|
|
39451
|
+
* @param flags Evaluated flag map from the leader stream
|
|
39425
39452
|
*/
|
|
39426
39453
|
broadcastFlags(flags) {
|
|
39454
|
+
if (this.mode !== "evaluated")
|
|
39455
|
+
return;
|
|
39427
39456
|
if (this.role !== "leader" || !this.channel)
|
|
39428
39457
|
return;
|
|
39429
39458
|
this.latestFlags = flags;
|
|
39430
39459
|
this.post({ type: "flags", memberId: this.memberId, flags });
|
|
39431
39460
|
}
|
|
39461
|
+
/**
|
|
39462
|
+
* Leader-only (rules mode): publish a verified rules snapshot.
|
|
39463
|
+
* Followers hydrate and evaluate with their own context.
|
|
39464
|
+
*
|
|
39465
|
+
* @param snapshot Rules store snapshot (MAC already verified on the leader)
|
|
39466
|
+
* @param analytics Optional per-flag analytics map from rules
|
|
39467
|
+
*/
|
|
39468
|
+
broadcastRules(snapshot, analytics) {
|
|
39469
|
+
if (this.mode !== "rules")
|
|
39470
|
+
return;
|
|
39471
|
+
if (this.role !== "leader" || !this.channel)
|
|
39472
|
+
return;
|
|
39473
|
+
this.latestRules = { snapshot, analytics };
|
|
39474
|
+
this.post({
|
|
39475
|
+
type: "rules",
|
|
39476
|
+
memberId: this.memberId,
|
|
39477
|
+
snapshot,
|
|
39478
|
+
analytics
|
|
39479
|
+
});
|
|
39480
|
+
}
|
|
39432
39481
|
/** Leader-only: mirror a transport `onError` (auth, quota) to followers. */
|
|
39433
39482
|
broadcastError(error2) {
|
|
39434
39483
|
if (this.role !== "leader" || !this.channel)
|
|
@@ -39442,11 +39491,34 @@ class ConnectionShareHub {
|
|
|
39442
39491
|
});
|
|
39443
39492
|
}
|
|
39444
39493
|
/**
|
|
39445
|
-
* Transport used by follower FlagClients.
|
|
39446
|
-
*
|
|
39494
|
+
* Transport used by follower FlagClients.
|
|
39495
|
+
* - Evaluated: `init` waits for flags; `fetchFlags` RPCs through the leader.
|
|
39496
|
+
* - Rules: `init` waits for a rules snapshot; `fetchFlags` evaluates locally.
|
|
39447
39497
|
* `destroy` is a no-op — {@link ConnectionShareHub.destroy} owns teardown.
|
|
39448
39498
|
*/
|
|
39449
39499
|
createFollowerTransport() {
|
|
39500
|
+
if (this.mode === "rules") {
|
|
39501
|
+
return {
|
|
39502
|
+
init: async () => {
|
|
39503
|
+
const payload = await this.waitForRules();
|
|
39504
|
+
const flags = this.followerRulesApply?.(payload) ?? {};
|
|
39505
|
+
this.followerFlagsCallback?.(flags);
|
|
39506
|
+
},
|
|
39507
|
+
fetchFlags: async (context) => {
|
|
39508
|
+
if (!this.followerLocalEval) {
|
|
39509
|
+
throw new Error("Rules-share follower has no local eval handler.");
|
|
39510
|
+
}
|
|
39511
|
+
return this.followerLocalEval(context);
|
|
39512
|
+
},
|
|
39513
|
+
destroy: () => void 0,
|
|
39514
|
+
onFlagsUpdated: (callback) => {
|
|
39515
|
+
this.followerFlagsCallback = callback;
|
|
39516
|
+
},
|
|
39517
|
+
onError: (callback) => {
|
|
39518
|
+
this.followerErrorCallback = callback;
|
|
39519
|
+
}
|
|
39520
|
+
};
|
|
39521
|
+
}
|
|
39450
39522
|
return {
|
|
39451
39523
|
init: async () => {
|
|
39452
39524
|
const flags = await this.waitForFlags();
|
|
@@ -39489,10 +39561,15 @@ class ConnectionShareHub {
|
|
|
39489
39561
|
setTimeout(() => channel.close(), 0);
|
|
39490
39562
|
}
|
|
39491
39563
|
this.flagsWaiters = [];
|
|
39564
|
+
this.rulesWaiters = [];
|
|
39492
39565
|
this.flagsTimeouts.splice(0).forEach(({ timeoutId, reject }) => {
|
|
39493
39566
|
clearTimeout(timeoutId);
|
|
39494
39567
|
reject(new Error("Connection share hub closed."));
|
|
39495
39568
|
});
|
|
39569
|
+
this.rulesTimeouts.splice(0).forEach(({ timeoutId, reject }) => {
|
|
39570
|
+
clearTimeout(timeoutId);
|
|
39571
|
+
reject(new Error("Connection share hub closed."));
|
|
39572
|
+
});
|
|
39496
39573
|
this.contextWaiters.forEach(({ reject }) => reject(new Error("Connection share hub closed.")));
|
|
39497
39574
|
this.contextWaiters.clear();
|
|
39498
39575
|
}
|
|
@@ -39589,7 +39666,16 @@ class ConnectionShareHub {
|
|
|
39589
39666
|
return;
|
|
39590
39667
|
switch (message.type) {
|
|
39591
39668
|
case "hello":
|
|
39592
|
-
if (this.role
|
|
39669
|
+
if (this.role !== "leader")
|
|
39670
|
+
break;
|
|
39671
|
+
if (this.mode === "rules" && this.latestRules) {
|
|
39672
|
+
this.post({
|
|
39673
|
+
type: "rules",
|
|
39674
|
+
memberId: this.memberId,
|
|
39675
|
+
snapshot: this.latestRules.snapshot,
|
|
39676
|
+
analytics: this.latestRules.analytics
|
|
39677
|
+
});
|
|
39678
|
+
} else if (this.mode === "evaluated" && this.latestFlags) {
|
|
39593
39679
|
this.post({
|
|
39594
39680
|
type: "flags",
|
|
39595
39681
|
memberId: this.memberId,
|
|
@@ -39598,7 +39684,7 @@ class ConnectionShareHub {
|
|
|
39598
39684
|
}
|
|
39599
39685
|
break;
|
|
39600
39686
|
case "flags":
|
|
39601
|
-
if (this.role === "leader")
|
|
39687
|
+
if (this.mode !== "evaluated" || this.role === "leader")
|
|
39602
39688
|
return;
|
|
39603
39689
|
if (!message.flags || typeof message.flags !== "object" || Array.isArray(message.flags)) {
|
|
39604
39690
|
return;
|
|
@@ -39607,6 +39693,22 @@ class ConnectionShareHub {
|
|
|
39607
39693
|
this.flagsWaiters.splice(0).forEach((resolve) => resolve(message.flags));
|
|
39608
39694
|
this.followerFlagsCallback?.(message.flags);
|
|
39609
39695
|
break;
|
|
39696
|
+
case "rules":
|
|
39697
|
+
if (this.mode !== "rules" || this.role === "leader")
|
|
39698
|
+
return;
|
|
39699
|
+
if (!message.snapshot || typeof message.snapshot !== "object")
|
|
39700
|
+
return;
|
|
39701
|
+
{
|
|
39702
|
+
const payload = {
|
|
39703
|
+
snapshot: message.snapshot,
|
|
39704
|
+
analytics: message.analytics
|
|
39705
|
+
};
|
|
39706
|
+
this.latestRules = payload;
|
|
39707
|
+
this.rulesWaiters.splice(0).forEach((resolve) => resolve(payload));
|
|
39708
|
+
const flags = this.followerRulesApply?.(payload) ?? {};
|
|
39709
|
+
this.followerFlagsCallback?.(flags);
|
|
39710
|
+
}
|
|
39711
|
+
break;
|
|
39610
39712
|
case "error":
|
|
39611
39713
|
if (this.role === "leader")
|
|
39612
39714
|
return;
|
|
@@ -39619,7 +39721,7 @@ class ConnectionShareHub {
|
|
|
39619
39721
|
}
|
|
39620
39722
|
break;
|
|
39621
39723
|
case "context-request":
|
|
39622
|
-
if (this.role !== "leader")
|
|
39724
|
+
if (this.mode !== "evaluated" || this.role !== "leader")
|
|
39623
39725
|
return;
|
|
39624
39726
|
void this.handleContextRequest(message);
|
|
39625
39727
|
break;
|
|
@@ -39704,6 +39806,31 @@ class ConnectionShareHub {
|
|
|
39704
39806
|
this.flagsTimeouts.push({ timeoutId, reject });
|
|
39705
39807
|
});
|
|
39706
39808
|
}
|
|
39809
|
+
/**
|
|
39810
|
+
* Block until the leader has sent a `rules` snapshot, or {@link FOLLOWER_WAIT_MS}.
|
|
39811
|
+
*
|
|
39812
|
+
* @returns Latest rules payload from the leader
|
|
39813
|
+
*/
|
|
39814
|
+
waitForRules() {
|
|
39815
|
+
if (this.latestRules)
|
|
39816
|
+
return Promise.resolve(this.latestRules);
|
|
39817
|
+
if (this.closed)
|
|
39818
|
+
return Promise.reject(new Error("Connection share hub closed."));
|
|
39819
|
+
return new Promise((resolve, reject) => {
|
|
39820
|
+
const timeoutId = setTimeout(() => {
|
|
39821
|
+
this.rulesWaiters = this.rulesWaiters.filter((waiter) => waiter !== onRules);
|
|
39822
|
+
this.rulesTimeouts = this.rulesTimeouts.filter((entry) => entry.timeoutId !== timeoutId);
|
|
39823
|
+
reject(new Error("Timed out waiting for shared rules snapshot from the leader."));
|
|
39824
|
+
}, FOLLOWER_WAIT_MS);
|
|
39825
|
+
const onRules = (payload) => {
|
|
39826
|
+
clearTimeout(timeoutId);
|
|
39827
|
+
this.rulesTimeouts = this.rulesTimeouts.filter((entry) => entry.timeoutId !== timeoutId);
|
|
39828
|
+
resolve(payload);
|
|
39829
|
+
};
|
|
39830
|
+
this.rulesWaiters.push(onRules);
|
|
39831
|
+
this.rulesTimeouts.push({ timeoutId, reject });
|
|
39832
|
+
});
|
|
39833
|
+
}
|
|
39707
39834
|
/**
|
|
39708
39835
|
* Follower `fetchFlags`: ask the leader to POST `/context` and wait for
|
|
39709
39836
|
* `context-result`. Leaders run the handler locally.
|
|
@@ -41903,22 +42030,80 @@ function payloadHasExplicitPiiFields(payload) {
|
|
|
41903
42030
|
return false;
|
|
41904
42031
|
return payload.piiSalt !== void 0 || payload.piiAttributes !== void 0 || payload.piiValueEncoding !== void 0;
|
|
41905
42032
|
}
|
|
42033
|
+
function piiAttributeKeysFromPayload(payload) {
|
|
42034
|
+
if (!payload || !Array.isArray(payload.piiAttributes))
|
|
42035
|
+
return [];
|
|
42036
|
+
return payload.piiAttributes.filter((k) => typeof k === "string" && k.length > 0);
|
|
42037
|
+
}
|
|
42038
|
+
function hasMalformedPiiAttributes(payload) {
|
|
42039
|
+
if (!payload || payload.piiAttributes === void 0)
|
|
42040
|
+
return false;
|
|
42041
|
+
if (!Array.isArray(payload.piiAttributes))
|
|
42042
|
+
return true;
|
|
42043
|
+
return payload.piiAttributes.some(
|
|
42044
|
+
(entry) => typeof entry !== "string" || entry.length === 0
|
|
42045
|
+
);
|
|
42046
|
+
}
|
|
42047
|
+
function hasExplicitInvalidPiiEncoding(payload) {
|
|
42048
|
+
if (!payload || payload.piiValueEncoding === void 0)
|
|
42049
|
+
return false;
|
|
42050
|
+
return payload.piiValueEncoding !== PII_VALUE_ENCODING;
|
|
42051
|
+
}
|
|
42052
|
+
function payloadRequiresUsablePiiWire(payload) {
|
|
42053
|
+
if (!payload)
|
|
42054
|
+
return false;
|
|
42055
|
+
if (hasExplicitInvalidPiiEncoding(payload))
|
|
42056
|
+
return true;
|
|
42057
|
+
if (hasMalformedPiiAttributes(payload))
|
|
42058
|
+
return true;
|
|
42059
|
+
if (piiAttributeKeysFromPayload(payload).length > 0)
|
|
42060
|
+
return true;
|
|
42061
|
+
return false;
|
|
42062
|
+
}
|
|
42063
|
+
function isExplicitPiiRequirementCleared(payload) {
|
|
42064
|
+
if (!payload || payload.piiAttributes === void 0)
|
|
42065
|
+
return false;
|
|
42066
|
+
if (!Array.isArray(payload.piiAttributes))
|
|
42067
|
+
return false;
|
|
42068
|
+
if (payload.piiAttributes.length !== 0)
|
|
42069
|
+
return false;
|
|
42070
|
+
if (hasExplicitInvalidPiiEncoding(payload))
|
|
42071
|
+
return false;
|
|
42072
|
+
return true;
|
|
42073
|
+
}
|
|
41906
42074
|
function piiWireOptionsFromPayload(payload) {
|
|
41907
42075
|
if (!payload)
|
|
41908
42076
|
return null;
|
|
42077
|
+
if (hasExplicitInvalidPiiEncoding(payload))
|
|
42078
|
+
return null;
|
|
42079
|
+
if (hasMalformedPiiAttributes(payload))
|
|
42080
|
+
return null;
|
|
41909
42081
|
const salt = typeof payload.piiSalt === "string" ? payload.piiSalt : "";
|
|
41910
42082
|
if (!salt)
|
|
41911
42083
|
return null;
|
|
41912
|
-
const
|
|
41913
|
-
if (encoding !== PII_VALUE_ENCODING)
|
|
41914
|
-
return null;
|
|
41915
|
-
const attrs = Array.isArray(payload.piiAttributes) ? payload.piiAttributes.filter((k) => typeof k === "string") : [];
|
|
42084
|
+
const attrs = piiAttributeKeysFromPayload(payload);
|
|
41916
42085
|
return {
|
|
41917
42086
|
piiAttributes: attrs,
|
|
41918
42087
|
piiSalt: salt,
|
|
41919
42088
|
piiValueEncoding: PII_VALUE_ENCODING
|
|
41920
42089
|
};
|
|
41921
42090
|
}
|
|
42091
|
+
function isPiiWireBroken(payload, resolved, previousPriorHadPiiAttrs = false) {
|
|
42092
|
+
if (hasExplicitInvalidPiiEncoding(payload) || hasMalformedPiiAttributes(payload)) {
|
|
42093
|
+
return true;
|
|
42094
|
+
}
|
|
42095
|
+
if (resolved) {
|
|
42096
|
+
return false;
|
|
42097
|
+
}
|
|
42098
|
+
if (isExplicitPiiRequirementCleared(payload))
|
|
42099
|
+
return false;
|
|
42100
|
+
if (payloadRequiresUsablePiiWire(payload))
|
|
42101
|
+
return true;
|
|
42102
|
+
if (previousPriorHadPiiAttrs && payloadHasExplicitPiiFields(payload)) {
|
|
42103
|
+
return true;
|
|
42104
|
+
}
|
|
42105
|
+
return false;
|
|
42106
|
+
}
|
|
41922
42107
|
function resolvePiiWireForDelta(previous, action) {
|
|
41923
42108
|
if (!payloadHasExplicitPiiFields(action)) {
|
|
41924
42109
|
return previous;
|
|
@@ -41971,7 +42156,8 @@ function createEmptyRulesState() {
|
|
|
41971
42156
|
segments: /* @__PURE__ */ new Map(),
|
|
41972
42157
|
ready: false,
|
|
41973
42158
|
needsFullConfig: true,
|
|
41974
|
-
piiWire: null
|
|
42159
|
+
piiWire: null,
|
|
42160
|
+
piiWireBroken: false
|
|
41975
42161
|
};
|
|
41976
42162
|
}
|
|
41977
42163
|
function cloneState(state) {
|
|
@@ -41986,7 +42172,8 @@ function cloneState(state) {
|
|
|
41986
42172
|
piiAttributes: [...state.piiWire.piiAttributes],
|
|
41987
42173
|
piiSalt: state.piiWire.piiSalt,
|
|
41988
42174
|
piiValueEncoding: state.piiWire.piiValueEncoding
|
|
41989
|
-
} : null
|
|
42175
|
+
} : null,
|
|
42176
|
+
piiWireBroken: state.piiWireBroken
|
|
41990
42177
|
};
|
|
41991
42178
|
}
|
|
41992
42179
|
function segmentsFromRecord(record) {
|
|
@@ -42013,14 +42200,18 @@ function hydrateFromFull(action) {
|
|
|
42013
42200
|
if (flag?.key)
|
|
42014
42201
|
flags.set(flag.key, flag);
|
|
42015
42202
|
}
|
|
42203
|
+
const piiWire = piiWireOptionsFromPayload(action);
|
|
42204
|
+
const piiWireBroken = isPiiWireBroken(action, piiWire, false);
|
|
42016
42205
|
return {
|
|
42017
42206
|
version: action.version,
|
|
42018
42207
|
expiresAt: action.expiresAt,
|
|
42019
42208
|
flags,
|
|
42020
42209
|
segments: segmentsFromRecord(action.segments),
|
|
42021
|
-
|
|
42022
|
-
|
|
42023
|
-
|
|
42210
|
+
// Usable rules, but PII metadata must be valid when hashing is required.
|
|
42211
|
+
ready: !piiWireBroken,
|
|
42212
|
+
needsFullConfig: piiWireBroken,
|
|
42213
|
+
piiWire,
|
|
42214
|
+
piiWireBroken
|
|
42024
42215
|
};
|
|
42025
42216
|
}
|
|
42026
42217
|
function applyPatch(state, action) {
|
|
@@ -42033,14 +42224,19 @@ function applyPatch(state, action) {
|
|
|
42033
42224
|
flags.set(flag.key, flag);
|
|
42034
42225
|
}
|
|
42035
42226
|
const nextPii = resolvePiiWireForDelta(state.piiWire, action);
|
|
42227
|
+
const priorHadPiiAttrs = (state.piiWire?.piiAttributes.length ?? 0) > 0 || state.piiWireBroken;
|
|
42228
|
+
const computedBroken = isPiiWireBroken(action, nextPii, priorHadPiiAttrs);
|
|
42229
|
+
const recoveredWithRequiredAttrs = !!nextPii && nextPii.piiAttributes.length > 0;
|
|
42230
|
+
const piiWireBroken = isExplicitPiiRequirementCleared(action) ? false : recoveredWithRequiredAttrs ? false : computedBroken || state.piiWireBroken;
|
|
42036
42231
|
return {
|
|
42037
42232
|
version: action.toVersion,
|
|
42038
42233
|
expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : state.expiresAt,
|
|
42039
42234
|
flags,
|
|
42040
42235
|
segments: applySegmentUpserts(state.segments, action.segments),
|
|
42041
|
-
ready:
|
|
42042
|
-
needsFullConfig:
|
|
42043
|
-
piiWire: nextPii
|
|
42236
|
+
ready: !piiWireBroken,
|
|
42237
|
+
needsFullConfig: piiWireBroken,
|
|
42238
|
+
piiWire: nextPii,
|
|
42239
|
+
piiWireBroken
|
|
42044
42240
|
};
|
|
42045
42241
|
}
|
|
42046
42242
|
function reduceRules(state, action, now = Date.now()) {
|
|
@@ -42060,8 +42256,8 @@ function reduceRules(state, action, now = Date.now()) {
|
|
|
42060
42256
|
...cloneState(state),
|
|
42061
42257
|
version: state.version,
|
|
42062
42258
|
expiresAt: action.expiresAt,
|
|
42063
|
-
ready:
|
|
42064
|
-
needsFullConfig: state.flags.size === 0 || versionMismatch ? true : state.needsFullConfig
|
|
42259
|
+
ready: !state.piiWireBroken,
|
|
42260
|
+
needsFullConfig: state.piiWireBroken || state.flags.size === 0 || versionMismatch ? true : state.needsFullConfig
|
|
42065
42261
|
}
|
|
42066
42262
|
};
|
|
42067
42263
|
}
|
|
@@ -42117,8 +42313,8 @@ function reduceRules(state, action, now = Date.now()) {
|
|
|
42117
42313
|
...current,
|
|
42118
42314
|
version: action.toVersion,
|
|
42119
42315
|
expiresAt: typeof action.expiresAt === "number" ? action.expiresAt : current.expiresAt,
|
|
42120
|
-
ready:
|
|
42121
|
-
needsFullConfig:
|
|
42316
|
+
ready: !current.piiWireBroken,
|
|
42317
|
+
needsFullConfig: current.piiWireBroken
|
|
42122
42318
|
}
|
|
42123
42319
|
};
|
|
42124
42320
|
}
|
|
@@ -42141,9 +42337,12 @@ class RulesStore {
|
|
|
42141
42337
|
return this.state;
|
|
42142
42338
|
}
|
|
42143
42339
|
/**
|
|
42144
|
-
* Fail-closed readiness: lease not expired
|
|
42340
|
+
* Fail-closed readiness: lease not expired, store marked ready, and PII
|
|
42341
|
+
* wire metadata usable when hashing is required.
|
|
42145
42342
|
*/
|
|
42146
42343
|
isReady(now = Date.now()) {
|
|
42344
|
+
if (this.state.piiWireBroken)
|
|
42345
|
+
return false;
|
|
42147
42346
|
if (!this.state.ready)
|
|
42148
42347
|
return false;
|
|
42149
42348
|
if (!this.state.expiresAt)
|
|
@@ -42227,7 +42426,9 @@ class RulesStore {
|
|
|
42227
42426
|
piiAttributes: [...pii.piiAttributes],
|
|
42228
42427
|
piiSalt: pii.piiSalt,
|
|
42229
42428
|
piiValueEncoding: pii.piiValueEncoding
|
|
42230
|
-
} : {}
|
|
42429
|
+
} : {},
|
|
42430
|
+
// Persist fail-closed even when piiWire is null (unusable salt/encoding).
|
|
42431
|
+
...this.state.piiWireBroken ? { piiWireBroken: true } : {}
|
|
42231
42432
|
};
|
|
42232
42433
|
}
|
|
42233
42434
|
hydrateFromSnapshot(snapshot, now = Date.now()) {
|
|
@@ -42237,14 +42438,17 @@ class RulesStore {
|
|
|
42237
42438
|
flags.set(flag.key, flag);
|
|
42238
42439
|
}
|
|
42239
42440
|
const expired = isConfigPayloadExpired(snapshot.expiresAt, now);
|
|
42441
|
+
const piiWire = piiWireOptionsFromPayload(snapshot);
|
|
42442
|
+
const piiWireBroken = snapshot.piiWireBroken === true || isPiiWireBroken(snapshot, piiWire, false);
|
|
42240
42443
|
this.state = {
|
|
42241
42444
|
version: snapshot.version,
|
|
42242
42445
|
expiresAt: snapshot.expiresAt,
|
|
42243
42446
|
flags,
|
|
42244
42447
|
segments: segmentsFromRecord(snapshot.segments),
|
|
42245
|
-
ready: !expired && flags.size > 0,
|
|
42246
|
-
needsFullConfig: expired || flags.size === 0,
|
|
42247
|
-
piiWire
|
|
42448
|
+
ready: !expired && flags.size > 0 && !piiWireBroken,
|
|
42449
|
+
needsFullConfig: expired || flags.size === 0 || piiWireBroken,
|
|
42450
|
+
piiWire,
|
|
42451
|
+
piiWireBroken
|
|
42248
42452
|
};
|
|
42249
42453
|
}
|
|
42250
42454
|
}
|
|
@@ -43164,11 +43368,11 @@ class FlagClient {
|
|
|
43164
43368
|
this.env = options.env;
|
|
43165
43369
|
this.enableFlagmint = options.enableFlagmint ?? true;
|
|
43166
43370
|
this.configSync = options.configSync ?? false;
|
|
43167
|
-
|
|
43371
|
+
logger.setup({ debugLog: options.debugLog });
|
|
43372
|
+
this.shareConnection = options.shareConnection ?? isConnectionSharingAvailable();
|
|
43168
43373
|
if (this.configSync) {
|
|
43169
43374
|
this.rulesStore = new RulesStore();
|
|
43170
43375
|
}
|
|
43171
|
-
logger.setup({ debugLog: options.debugLog });
|
|
43172
43376
|
if (this.previewMode && this.rawFlags && Object.keys(this.rawFlags).length > 0) {
|
|
43173
43377
|
this.flags = this.evaluateLocally(this.rawFlags, this.context);
|
|
43174
43378
|
this.readyPromise = Promise.resolve();
|
|
@@ -43232,7 +43436,7 @@ class FlagClient {
|
|
|
43232
43436
|
return;
|
|
43233
43437
|
}
|
|
43234
43438
|
await this.setupTransport(options);
|
|
43235
|
-
this.
|
|
43439
|
+
this.broadcastShareSnapshot();
|
|
43236
43440
|
this.isInitialized = true;
|
|
43237
43441
|
this.resolveReady();
|
|
43238
43442
|
} catch (err) {
|
|
@@ -43272,19 +43476,26 @@ class FlagClient {
|
|
|
43272
43476
|
* @returns true when this document is a follower and should skip opening a stream.
|
|
43273
43477
|
*/
|
|
43274
43478
|
async joinConnectionShare(options) {
|
|
43275
|
-
if (!this.shareConnection
|
|
43479
|
+
if (!this.shareConnection) {
|
|
43480
|
+
return false;
|
|
43481
|
+
}
|
|
43482
|
+
if (options.transport && !options.share) {
|
|
43276
43483
|
return false;
|
|
43277
43484
|
}
|
|
43278
43485
|
if (!options.share && !isConnectionSharingAvailable()) {
|
|
43279
43486
|
return false;
|
|
43280
43487
|
}
|
|
43281
|
-
|
|
43488
|
+
const shareMode = this.configSync ? "rules" : "evaluated";
|
|
43489
|
+
this.shareHub = new ConnectionShareHub(this.apiKey, {
|
|
43490
|
+
...options.share,
|
|
43491
|
+
mode: shareMode
|
|
43492
|
+
});
|
|
43282
43493
|
this.shareHub.onPromote(async () => {
|
|
43283
43494
|
logger.log("[FlagClient] Share leader departed. Promoting this document to hold the stream.");
|
|
43284
43495
|
try {
|
|
43285
43496
|
await this.setupTransport(options);
|
|
43286
43497
|
this.attachShareLeader();
|
|
43287
|
-
this.
|
|
43498
|
+
this.broadcastShareSnapshot();
|
|
43288
43499
|
} catch (err) {
|
|
43289
43500
|
const error2 = err instanceof Error ? err : new Error(String(err));
|
|
43290
43501
|
logger.error("[FlagClient] Promotion to share leader failed:", error2);
|
|
@@ -43293,7 +43504,12 @@ class FlagClient {
|
|
|
43293
43504
|
});
|
|
43294
43505
|
const role = await this.shareHub.join();
|
|
43295
43506
|
if (role === "follower") {
|
|
43296
|
-
logger.log(
|
|
43507
|
+
logger.log(
|
|
43508
|
+
shareMode === "rules" ? "[FlagClient] Following shared config-sync rules from another same-origin document." : "[FlagClient] Following an existing Flagmint stream in another same-origin document."
|
|
43509
|
+
);
|
|
43510
|
+
if (shareMode === "rules") {
|
|
43511
|
+
this.attachRulesShareFollower();
|
|
43512
|
+
}
|
|
43297
43513
|
this.transport = this.shareHub.createFollowerTransport();
|
|
43298
43514
|
this.bindTransport(this.transport);
|
|
43299
43515
|
await this.transport.init();
|
|
@@ -43302,8 +43518,15 @@ class FlagClient {
|
|
|
43302
43518
|
this.attachShareLeader();
|
|
43303
43519
|
return false;
|
|
43304
43520
|
}
|
|
43521
|
+
/**
|
|
43522
|
+
* Leader: wire context RPC for evaluated share only.
|
|
43523
|
+
* Rules share never forwards follower context onto the stream.
|
|
43524
|
+
*/
|
|
43305
43525
|
attachShareLeader() {
|
|
43306
|
-
this.shareHub
|
|
43526
|
+
if (!this.shareHub || this.shareHub.shareMode === "rules") {
|
|
43527
|
+
return;
|
|
43528
|
+
}
|
|
43529
|
+
this.shareHub.setLeaderContextHandler(async (context, options) => {
|
|
43307
43530
|
const mergedContext = toJsonCloneable({ ...this.context, ...context });
|
|
43308
43531
|
if (options?.persist === true) {
|
|
43309
43532
|
this.context = mergedContext;
|
|
@@ -43312,9 +43535,6 @@ class FlagClient {
|
|
|
43312
43535
|
}
|
|
43313
43536
|
}
|
|
43314
43537
|
if (!this.transport || typeof this.transport.fetchFlags !== "function") {
|
|
43315
|
-
if (this.configSync && this.rulesStore) {
|
|
43316
|
-
return this.evaluateFromRulesStore(mergedContext);
|
|
43317
|
-
}
|
|
43318
43538
|
return this.flags;
|
|
43319
43539
|
}
|
|
43320
43540
|
return this.transport.fetchFlags(mergedContext, {
|
|
@@ -43322,6 +43542,41 @@ class FlagClient {
|
|
|
43322
43542
|
});
|
|
43323
43543
|
});
|
|
43324
43544
|
}
|
|
43545
|
+
/**
|
|
43546
|
+
* Follower hooks for rules-only share: hydrate rules locally and evaluate
|
|
43547
|
+
* with this document's context (never the leader's).
|
|
43548
|
+
*/
|
|
43549
|
+
attachRulesShareFollower() {
|
|
43550
|
+
if (!this.shareHub || !this.rulesStore)
|
|
43551
|
+
return;
|
|
43552
|
+
this.shareHub.setFollowerRulesApply((payload) => {
|
|
43553
|
+
this.rulesStore.hydrateFromSnapshot(payload.snapshot);
|
|
43554
|
+
if (payload.analytics) {
|
|
43555
|
+
this.updateAnalytics(payload.analytics);
|
|
43556
|
+
}
|
|
43557
|
+
const flags = this.evaluateFromRulesStore();
|
|
43558
|
+
this.updateFlags(flags, true);
|
|
43559
|
+
return flags;
|
|
43560
|
+
});
|
|
43561
|
+
this.shareHub.setFollowerLocalEval((context) => {
|
|
43562
|
+
return this.evaluateFromRulesStore(context);
|
|
43563
|
+
});
|
|
43564
|
+
}
|
|
43565
|
+
/**
|
|
43566
|
+
* Leader: push the right snapshot type to followers after flags/rules change.
|
|
43567
|
+
*/
|
|
43568
|
+
broadcastShareSnapshot() {
|
|
43569
|
+
if (!this.shareHub || this.shareHub.currentRole !== "leader")
|
|
43570
|
+
return;
|
|
43571
|
+
if (this.shareHub.shareMode === "rules" && this.rulesStore) {
|
|
43572
|
+
const snapshot = this.rulesStore.toSnapshot();
|
|
43573
|
+
if (snapshot.flags.length === 0)
|
|
43574
|
+
return;
|
|
43575
|
+
this.shareHub.broadcastRules(snapshot, this.analyticsMapFromRulesStore());
|
|
43576
|
+
return;
|
|
43577
|
+
}
|
|
43578
|
+
this.shareHub.broadcastFlags(this.flags);
|
|
43579
|
+
}
|
|
43325
43580
|
/**
|
|
43326
43581
|
* Creates and initializes the transport layer used to receive feature flags
|
|
43327
43582
|
* updates from the Flagmint platform.
|
|
@@ -43466,7 +43721,7 @@ class FlagClient {
|
|
|
43466
43721
|
}
|
|
43467
43722
|
this.notifySubscribers();
|
|
43468
43723
|
if (!fromShare) {
|
|
43469
|
-
this.
|
|
43724
|
+
this.broadcastShareSnapshot();
|
|
43470
43725
|
}
|
|
43471
43726
|
}
|
|
43472
43727
|
/**
|