@ethlete/query 6.0.0-next.1 → 6.0.0-next.3
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/CHANGELOG.md +16 -0
- package/fesm2022/ethlete-query.mjs +109 -35
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/package.json +1 -1
- package/types/ethlete-query.d.ts +17 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# @ethlete/query
|
|
2
2
|
|
|
3
|
+
## 6.0.0-next.3
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#2926](https://github.com/ethlete-io/ethdk/pull/2926) [`801d112`](https://github.com/ethlete-io/ethdk/commit/801d1122b55d24762b9e656d3a8ed70c561ec06f) Thanks [@github-actions](https://github.com/apps/github-actions)! - Fix query form emitting twice if a sort field gets cleared
|
|
8
|
+
|
|
9
|
+
## 6.0.0-next.2
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [`252fb8f`](https://github.com/ethlete-io/ethdk/commit/252fb8fe498a129b2f2dbf8f196aec4e1f2327a3) Thanks [@TomTomB](https://github.com/TomTomB)! - Report `leaderStatusChange` and `leaderInstanceCountChange` events in bearer auth tracking feature
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- [#2923](https://github.com/ethlete-io/ethdk/pull/2923) [`62bd4ae`](https://github.com/ethlete-io/ethdk/commit/62bd4ae092ad931fc8706ba7518cd26e17ef82a9) Thanks [@github-actions](https://github.com/apps/github-actions)! - Remove useless queryKey field from auth provider tracking events
|
|
18
|
+
|
|
3
19
|
## 6.0.0-next.1
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
|
@@ -5165,6 +5165,9 @@ const setupLeaderElection = () => {
|
|
|
5165
5165
|
const storageKey = 'ethlete-auth-leader-heartbeat';
|
|
5166
5166
|
const tabId = `tab-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
|
5167
5167
|
const isLeader = signal(false, ...(ngDevMode ? [{ debugName: "isLeader" }] : []));
|
|
5168
|
+
const instanceCount = signal(1, ...(ngDevMode ? [{ debugName: "instanceCount" }] : []));
|
|
5169
|
+
const knownTabs = new Map();
|
|
5170
|
+
knownTabs.set(tabId, Date.now());
|
|
5168
5171
|
let channel = null;
|
|
5169
5172
|
let heartbeatIntervalId = null;
|
|
5170
5173
|
let leaderCheckIntervalId = null;
|
|
@@ -5172,6 +5175,7 @@ const setupLeaderElection = () => {
|
|
|
5172
5175
|
isLeader.set(true);
|
|
5173
5176
|
return {
|
|
5174
5177
|
isLeader: isLeader.asReadonly(),
|
|
5178
|
+
instanceCount: instanceCount.asReadonly(),
|
|
5175
5179
|
becomeLeader: () => {
|
|
5176
5180
|
/* no-op */
|
|
5177
5181
|
},
|
|
@@ -5181,6 +5185,7 @@ const setupLeaderElection = () => {
|
|
|
5181
5185
|
};
|
|
5182
5186
|
}
|
|
5183
5187
|
channel = new BroadcastChannel(channelName);
|
|
5188
|
+
channel.postMessage({ type: 'instance-register', tabId });
|
|
5184
5189
|
const updateLeaderHeartbeat = (newTabId = tabId) => {
|
|
5185
5190
|
try {
|
|
5186
5191
|
localStorage.setItem(storageKey, JSON.stringify({ tabId: newTabId, timestamp: Date.now() }));
|
|
@@ -5227,48 +5232,60 @@ const setupLeaderElection = () => {
|
|
|
5227
5232
|
const heartbeat = getLeaderHeartbeat();
|
|
5228
5233
|
const now = Date.now();
|
|
5229
5234
|
if (!heartbeat || now - heartbeat.timestamp > leaderTimeout) {
|
|
5230
|
-
// No leader or leader is dead
|
|
5231
5235
|
if (!isLeader()) {
|
|
5232
5236
|
becomeLeader();
|
|
5233
5237
|
}
|
|
5234
5238
|
}
|
|
5235
5239
|
else if (heartbeat.tabId === tabId) {
|
|
5236
|
-
// We are the leader, update heartbeat
|
|
5237
5240
|
if (!isLeader()) {
|
|
5238
5241
|
isLeader.set(true);
|
|
5239
5242
|
}
|
|
5240
5243
|
updateLeaderHeartbeat();
|
|
5241
5244
|
}
|
|
5242
5245
|
else {
|
|
5243
|
-
// Another tab is leader
|
|
5244
5246
|
if (isLeader()) {
|
|
5245
5247
|
isLeader.set(false);
|
|
5246
5248
|
}
|
|
5247
5249
|
}
|
|
5248
5250
|
};
|
|
5249
|
-
// Handle messages from other tabs
|
|
5250
5251
|
channel.onmessage = (event) => {
|
|
5251
5252
|
const message = event.data;
|
|
5252
5253
|
if (message.type === 'heartbeat') {
|
|
5254
|
+
if (message.tabId !== tabId) {
|
|
5255
|
+
knownTabs.set(message.tabId, Date.now());
|
|
5256
|
+
}
|
|
5253
5257
|
if (message.tabId !== tabId && isLeader()) {
|
|
5254
|
-
// Another tab claims to be leader, resolve conflict
|
|
5255
5258
|
const heartbeat = getLeaderHeartbeat();
|
|
5256
5259
|
if (heartbeat && heartbeat.tabId === message.tabId) {
|
|
5257
|
-
// Other tab is legitimate leader
|
|
5258
5260
|
releaseLeadership();
|
|
5259
5261
|
}
|
|
5260
5262
|
}
|
|
5261
5263
|
}
|
|
5264
|
+
else if (message.type === 'tab-heartbeat') {
|
|
5265
|
+
if (message.tabId !== tabId) {
|
|
5266
|
+
knownTabs.set(message.tabId, Date.now());
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5269
|
+
else if (message.type === 'instance-register') {
|
|
5270
|
+
if (message.tabId !== tabId) {
|
|
5271
|
+
knownTabs.set(message.tabId, Date.now());
|
|
5272
|
+
instanceCount.set(knownTabs.size);
|
|
5273
|
+
channel?.postMessage({ type: 'tab-heartbeat', tabId });
|
|
5274
|
+
}
|
|
5275
|
+
}
|
|
5276
|
+
else if (message.type === 'instance-unregister') {
|
|
5277
|
+
if (message.tabId !== tabId) {
|
|
5278
|
+
knownTabs.delete(message.tabId);
|
|
5279
|
+
instanceCount.set(knownTabs.size);
|
|
5280
|
+
}
|
|
5281
|
+
}
|
|
5262
5282
|
else if (message.type === 'leader-claim') {
|
|
5263
5283
|
if (message.tabId !== tabId && isLeader()) {
|
|
5264
|
-
// Conflict: both tabs think they're leader
|
|
5265
|
-
// Let the tab with earlier heartbeat win
|
|
5266
5284
|
const heartbeat = getLeaderHeartbeat();
|
|
5267
5285
|
if (heartbeat && heartbeat.tabId === message.tabId) {
|
|
5268
5286
|
releaseLeadership();
|
|
5269
5287
|
}
|
|
5270
5288
|
else {
|
|
5271
|
-
// We were leader first, send heartbeat to assert dominance
|
|
5272
5289
|
const assertMessage = {
|
|
5273
5290
|
type: 'heartbeat',
|
|
5274
5291
|
tabId,
|
|
@@ -5279,35 +5296,40 @@ const setupLeaderElection = () => {
|
|
|
5279
5296
|
}
|
|
5280
5297
|
else if (message.type === 'leader-release') {
|
|
5281
5298
|
if (message.tabId !== tabId) {
|
|
5282
|
-
// Leader released, try to become new leader
|
|
5283
5299
|
setTimeout(() => checkLeaderStatus(), 100);
|
|
5284
5300
|
}
|
|
5285
5301
|
}
|
|
5286
5302
|
};
|
|
5287
|
-
// Try to become leader on startup
|
|
5288
5303
|
checkLeaderStatus();
|
|
5289
|
-
// Send heartbeat periodically if we're the leader
|
|
5290
5304
|
heartbeatIntervalId = setInterval(() => {
|
|
5305
|
+
knownTabs.set(tabId, Date.now());
|
|
5291
5306
|
if (isLeader()) {
|
|
5292
5307
|
updateLeaderHeartbeat();
|
|
5293
|
-
|
|
5294
|
-
|
|
5295
|
-
|
|
5296
|
-
};
|
|
5297
|
-
|
|
5308
|
+
channel?.postMessage({ type: 'heartbeat', tabId });
|
|
5309
|
+
}
|
|
5310
|
+
else {
|
|
5311
|
+
channel?.postMessage({ type: 'tab-heartbeat', tabId });
|
|
5312
|
+
}
|
|
5313
|
+
const now = Date.now();
|
|
5314
|
+
let changed = false;
|
|
5315
|
+
for (const [id, lastSeen] of knownTabs) {
|
|
5316
|
+
if (id !== tabId && now - lastSeen > leaderTimeout) {
|
|
5317
|
+
knownTabs.delete(id);
|
|
5318
|
+
changed = true;
|
|
5319
|
+
}
|
|
5320
|
+
}
|
|
5321
|
+
if (changed) {
|
|
5322
|
+
instanceCount.set(knownTabs.size);
|
|
5298
5323
|
}
|
|
5299
5324
|
}, heartbeatInterval);
|
|
5300
|
-
// Check leader status periodically
|
|
5301
5325
|
leaderCheckIntervalId = setInterval(() => {
|
|
5302
5326
|
checkLeaderStatus();
|
|
5303
5327
|
}, heartbeatInterval);
|
|
5304
|
-
// Handle tab visibility changes
|
|
5305
5328
|
const handleVisibilityChange = () => {
|
|
5306
5329
|
if (document.hidden && isLeader()) {
|
|
5307
5330
|
// Tab is hidden but we're leader - keep leadership but other tabs will take over if we die
|
|
5308
5331
|
}
|
|
5309
5332
|
else if (!document.hidden && !isLeader()) {
|
|
5310
|
-
// Tab became visible and we're not leader - check if we should become leader
|
|
5311
5333
|
checkLeaderStatus();
|
|
5312
5334
|
}
|
|
5313
5335
|
};
|
|
@@ -5319,10 +5341,10 @@ const setupLeaderElection = () => {
|
|
|
5319
5341
|
if (leaderCheckIntervalId) {
|
|
5320
5342
|
clearInterval(leaderCheckIntervalId);
|
|
5321
5343
|
}
|
|
5344
|
+
channel?.postMessage({ type: 'instance-unregister', tabId });
|
|
5322
5345
|
releaseLeadership();
|
|
5323
5346
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
|
5324
5347
|
channel?.close();
|
|
5325
|
-
// Clean up storage if we were the leader
|
|
5326
5348
|
const heartbeat = getLeaderHeartbeat();
|
|
5327
5349
|
if (heartbeat?.tabId === tabId) {
|
|
5328
5350
|
try {
|
|
@@ -5336,6 +5358,7 @@ const setupLeaderElection = () => {
|
|
|
5336
5358
|
destroyRef.onDestroy(cleanup);
|
|
5337
5359
|
return {
|
|
5338
5360
|
isLeader: isLeader.asReadonly(),
|
|
5361
|
+
instanceCount: instanceCount.asReadonly(),
|
|
5339
5362
|
becomeLeader,
|
|
5340
5363
|
cleanup,
|
|
5341
5364
|
};
|
|
@@ -5602,10 +5625,15 @@ const createLeaderElection = (config) => {
|
|
|
5602
5625
|
const leaderElectionEnabled = multiTabSyncEnabled &&
|
|
5603
5626
|
(typeof multiTabSyncConfig === 'object' ? (multiTabSyncConfig?.leaderElection ?? true) : true);
|
|
5604
5627
|
const leaderElection = leaderElectionEnabled ? setupLeaderElection() : null;
|
|
5605
|
-
|
|
5628
|
+
const isLeaderFn = () => (leaderElectionEnabled ? (leaderElection?.isLeader() ?? true) : true);
|
|
5629
|
+
const leaderElectionContext = leaderElection
|
|
5630
|
+
? { isLeader: leaderElection.isLeader, instanceCount: leaderElection.instanceCount }
|
|
5631
|
+
: undefined;
|
|
5632
|
+
return { isLeaderFn, leaderElectionContext };
|
|
5606
5633
|
};
|
|
5607
5634
|
const createBearerAuthProviderImpl = (config) => {
|
|
5608
5635
|
const injector = inject(Injector);
|
|
5636
|
+
const destroyRef = inject(DestroyRef);
|
|
5609
5637
|
const queryClient = config.queryClientRef[1]();
|
|
5610
5638
|
const accessToken = signal(null, ...(ngDevMode ? [{ debugName: "accessToken" }] : []));
|
|
5611
5639
|
const refreshToken = signal(null, ...(ngDevMode ? [{ debugName: "refreshToken" }] : []));
|
|
@@ -5645,7 +5673,7 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
5645
5673
|
bearerDecryptFn: config.bearerDecryptFn,
|
|
5646
5674
|
queryClient,
|
|
5647
5675
|
repository: queryClient.repository,
|
|
5648
|
-
isLeader,
|
|
5676
|
+
isLeader: isLeader.isLeaderFn,
|
|
5649
5677
|
afterTokenRefresh$,
|
|
5650
5678
|
queries: queries,
|
|
5651
5679
|
};
|
|
@@ -5658,8 +5686,10 @@ const createBearerAuthProviderImpl = (config) => {
|
|
|
5658
5686
|
bearerData,
|
|
5659
5687
|
logout,
|
|
5660
5688
|
injector,
|
|
5689
|
+
destroyRef,
|
|
5661
5690
|
setTokens,
|
|
5662
|
-
isLeader,
|
|
5691
|
+
isLeader: isLeader.isLeaderFn,
|
|
5692
|
+
leaderElection: isLeader.leaderElectionContext,
|
|
5663
5693
|
afterTokenRefresh$,
|
|
5664
5694
|
queries: queries,
|
|
5665
5695
|
};
|
|
@@ -6074,7 +6104,7 @@ const createTrackingFeature = (context, config) => {
|
|
|
6074
6104
|
}
|
|
6075
6105
|
}
|
|
6076
6106
|
};
|
|
6077
|
-
const
|
|
6107
|
+
const fireHandlers = (event, data) => {
|
|
6078
6108
|
const eventHandlers = handlers.get(event);
|
|
6079
6109
|
if (eventHandlers) {
|
|
6080
6110
|
eventHandlers.forEach((handler) => {
|
|
@@ -6087,6 +6117,27 @@ const createTrackingFeature = (context, config) => {
|
|
|
6087
6117
|
});
|
|
6088
6118
|
}
|
|
6089
6119
|
};
|
|
6120
|
+
const forwardingChannel = context.leaderElection && typeof BroadcastChannel !== 'undefined'
|
|
6121
|
+
? new BroadcastChannel('ethlete-auth-tracking')
|
|
6122
|
+
: null;
|
|
6123
|
+
if (forwardingChannel) {
|
|
6124
|
+
forwardingChannel.onmessage = (messageEvent) => {
|
|
6125
|
+
if (context.isLeader()) {
|
|
6126
|
+
fireHandlers(messageEvent.data.event, messageEvent.data.data);
|
|
6127
|
+
}
|
|
6128
|
+
};
|
|
6129
|
+
context.destroyRef.onDestroy(() => forwardingChannel.close());
|
|
6130
|
+
}
|
|
6131
|
+
const emit = (event, data) => {
|
|
6132
|
+
if (forwardingChannel && !context.isLeader()) {
|
|
6133
|
+
forwardingChannel.postMessage({ event, data });
|
|
6134
|
+
return;
|
|
6135
|
+
}
|
|
6136
|
+
fireHandlers(event, data);
|
|
6137
|
+
};
|
|
6138
|
+
const emitDirect = (event, data) => {
|
|
6139
|
+
fireHandlers(event, data);
|
|
6140
|
+
};
|
|
6090
6141
|
const trackedQueries = new Map();
|
|
6091
6142
|
Object.entries(context.queries).forEach(([key, queryEntry]) => {
|
|
6092
6143
|
const query = queryEntry; // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
@@ -6103,17 +6154,11 @@ const createTrackingFeature = (context, config) => {
|
|
|
6103
6154
|
const prevState = trackedQueries.get(key);
|
|
6104
6155
|
if (response && !loading && !error && prevState?.lastResult !== 'success') {
|
|
6105
6156
|
trackedQueries.set(key, { loading: false, lastResult: 'success' });
|
|
6106
|
-
emit(`${key}Success`, {
|
|
6107
|
-
queryKey: key,
|
|
6108
|
-
snapshot,
|
|
6109
|
-
});
|
|
6157
|
+
emit(`${key}Success`, { snapshot });
|
|
6110
6158
|
}
|
|
6111
6159
|
if (error && !loading && prevState?.lastResult !== 'error') {
|
|
6112
6160
|
trackedQueries.set(key, { loading: false, lastResult: 'error' });
|
|
6113
|
-
emit(`${key}Failure`, {
|
|
6114
|
-
queryKey: key,
|
|
6115
|
-
error,
|
|
6116
|
-
});
|
|
6161
|
+
emit(`${key}Failure`, { error });
|
|
6117
6162
|
}
|
|
6118
6163
|
if (loading) {
|
|
6119
6164
|
trackedQueries.set(key, { loading: true, lastResult: null });
|
|
@@ -6143,6 +6188,17 @@ const createTrackingFeature = (context, config) => {
|
|
|
6143
6188
|
}
|
|
6144
6189
|
});
|
|
6145
6190
|
}
|
|
6191
|
+
if (context.leaderElection) {
|
|
6192
|
+
const { isLeader: isLeaderSignal, instanceCount: instanceCountSignal } = context.leaderElection;
|
|
6193
|
+
effect(() => {
|
|
6194
|
+
emitDirect('leaderStatusChange', { isLeader: isLeaderSignal() });
|
|
6195
|
+
});
|
|
6196
|
+
effect(() => {
|
|
6197
|
+
emitDirect('leaderInstanceCountChange', {
|
|
6198
|
+
count: instanceCountSignal(),
|
|
6199
|
+
});
|
|
6200
|
+
});
|
|
6201
|
+
}
|
|
6146
6202
|
return {
|
|
6147
6203
|
on,
|
|
6148
6204
|
off,
|
|
@@ -6966,8 +7022,11 @@ class QueryForm {
|
|
|
6966
7022
|
continue;
|
|
6967
7023
|
const valueGotRemoved = value === ET_PROPERTY_REMOVED;
|
|
6968
7024
|
if (valueGotRemoved) {
|
|
6969
|
-
|
|
6970
|
-
|
|
7025
|
+
const defaultValue = this._getDefaultValue(key);
|
|
7026
|
+
if (!equal(field.control.value, defaultValue)) {
|
|
7027
|
+
field.control.setValue(defaultValue, { emitEvent: false });
|
|
7028
|
+
didValueChanges = true;
|
|
7029
|
+
}
|
|
6971
7030
|
continue;
|
|
6972
7031
|
}
|
|
6973
7032
|
let deserializedValue = value;
|
|
@@ -7139,6 +7198,21 @@ class QueryForm {
|
|
|
7139
7198
|
});
|
|
7140
7199
|
}
|
|
7141
7200
|
_handleFormChange(forceOverwrite = false) {
|
|
7201
|
+
// Normalize values that have a valueToQueryParamTransformFn returning null/undefined to their
|
|
7202
|
+
// field default before capturing the new value. This prevents intermediate "empty object" states
|
|
7203
|
+
// (e.g. Sort emitting { active: '', direction: '' } when cleared) from being
|
|
7204
|
+
// emitted as distinct form values.
|
|
7205
|
+
for (const [key, field] of Object.entries(this._fields)) {
|
|
7206
|
+
if (!field.data.valueToQueryParamTransformFn)
|
|
7207
|
+
continue;
|
|
7208
|
+
const rawValue = this._formValue[key];
|
|
7209
|
+
if (this._isDefaultValue(key, rawValue))
|
|
7210
|
+
continue;
|
|
7211
|
+
const serialized = field.data.valueToQueryParamTransformFn(rawValue);
|
|
7212
|
+
if (serialized !== null && serialized !== undefined)
|
|
7213
|
+
continue;
|
|
7214
|
+
this.form.controls[key]?.setValue(this._getDefaultValue(key), { emitEvent: false });
|
|
7215
|
+
}
|
|
7142
7216
|
const currentVal = clone(this._currentFormValue$.value);
|
|
7143
7217
|
const newVal = clone(this._formValue);
|
|
7144
7218
|
if (equal(currentVal, newVal)) {
|