@push.rocks/smartstate 2.0.31 → 2.1.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/dist_ts/00_commitinfo_data.js +3 -3
- package/dist_ts/index.d.ts +2 -0
- package/dist_ts/index.js +3 -1
- package/dist_ts/smartstate.classes.computed.d.ts +7 -0
- package/dist_ts/smartstate.classes.computed.js +10 -0
- package/dist_ts/smartstate.classes.smartstate.d.ts +19 -10
- package/dist_ts/smartstate.classes.smartstate.js +44 -17
- package/dist_ts/smartstate.classes.statepart.d.ts +26 -9
- package/dist_ts/smartstate.classes.statepart.js +120 -24
- package/dist_ts/smartstate.contextprovider.d.ts +16 -0
- package/dist_ts/smartstate.contextprovider.js +44 -0
- package/npmextra.json +8 -2
- package/package.json +9 -4
- package/readme.hints.md +47 -30
- package/readme.md +218 -213
- package/ts/00_commitinfo_data.ts +2 -2
- package/ts/index.ts +2 -0
- package/ts/smartstate.classes.computed.ts +16 -0
- package/ts/smartstate.classes.smartstate.ts +51 -17
- package/ts/smartstate.classes.statepart.ts +140 -23
- package/ts/smartstate.contextprovider.ts +61 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as plugins from './smartstate.plugins.js';
|
|
2
2
|
import { StatePart } from './smartstate.classes.statepart.js';
|
|
3
|
+
import { computed } from './smartstate.classes.computed.js';
|
|
3
4
|
|
|
4
5
|
export type TInitMode = 'soft' | 'mandatory' | 'force' | 'persistent';
|
|
5
6
|
|
|
@@ -11,17 +12,57 @@ export class Smartstate<StatePartNameType extends string> {
|
|
|
11
12
|
|
|
12
13
|
private pendingStatePartCreation: Map<string, Promise<StatePart<StatePartNameType, any>>> = new Map();
|
|
13
14
|
|
|
15
|
+
// Batch support
|
|
16
|
+
private batchDepth = 0;
|
|
17
|
+
private pendingNotifications = new Set<StatePart<any, any>>();
|
|
18
|
+
|
|
14
19
|
constructor() {}
|
|
15
20
|
|
|
21
|
+
/**
|
|
22
|
+
* whether state changes are currently being batched
|
|
23
|
+
*/
|
|
24
|
+
public get isBatching(): boolean {
|
|
25
|
+
return this.batchDepth > 0;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* registers a state part for deferred notification during a batch
|
|
30
|
+
*/
|
|
31
|
+
public registerPendingNotification(statePart: StatePart<any, any>): void {
|
|
32
|
+
this.pendingNotifications.add(statePart);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* batches multiple state updates so subscribers are only notified once all updates complete
|
|
37
|
+
*/
|
|
38
|
+
public async batch(updateFn: () => Promise<void> | void): Promise<void> {
|
|
39
|
+
this.batchDepth++;
|
|
40
|
+
try {
|
|
41
|
+
await updateFn();
|
|
42
|
+
} finally {
|
|
43
|
+
this.batchDepth--;
|
|
44
|
+
if (this.batchDepth === 0) {
|
|
45
|
+
const pending = [...this.pendingNotifications];
|
|
46
|
+
this.pendingNotifications.clear();
|
|
47
|
+
for (const sp of pending) {
|
|
48
|
+
await sp.notifyChange();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* creates a computed observable derived from multiple state parts
|
|
56
|
+
*/
|
|
57
|
+
public computed<TResult>(
|
|
58
|
+
sources: StatePart<StatePartNameType, any>[],
|
|
59
|
+
computeFn: (...states: any[]) => TResult,
|
|
60
|
+
): plugins.smartrx.rxjs.Observable<TResult> {
|
|
61
|
+
return computed(sources, computeFn);
|
|
62
|
+
}
|
|
63
|
+
|
|
16
64
|
/**
|
|
17
65
|
* Allows getting and initializing a new statepart
|
|
18
|
-
* initMode === 'soft' (default) - returns existing statepart if exists, creates new if not
|
|
19
|
-
* initMode === 'mandatory' - requires statepart to not exist, fails if it does
|
|
20
|
-
* initMode === 'force' - always creates new statepart, overwriting any existing
|
|
21
|
-
* initMode === 'persistent' - like 'soft' but with webstore persistence
|
|
22
|
-
* @param statePartNameArg
|
|
23
|
-
* @param initialArg
|
|
24
|
-
* @param initMode
|
|
25
66
|
*/
|
|
26
67
|
public async getStatePart<PayloadType>(
|
|
27
68
|
statePartNameArg: StatePartNameType,
|
|
@@ -43,16 +84,13 @@ export class Smartstate<StatePartNameType extends string> {
|
|
|
43
84
|
`State part '${statePartNameArg}' already exists, but initMode is 'mandatory'`
|
|
44
85
|
);
|
|
45
86
|
case 'force':
|
|
46
|
-
|
|
47
|
-
break; // Fall through to creation
|
|
87
|
+
break;
|
|
48
88
|
case 'soft':
|
|
49
89
|
case 'persistent':
|
|
50
90
|
default:
|
|
51
|
-
// Return existing state part
|
|
52
91
|
return existingStatePart as StatePart<StatePartNameType, PayloadType>;
|
|
53
92
|
}
|
|
54
93
|
} else {
|
|
55
|
-
// State part doesn't exist
|
|
56
94
|
if (!initialArg) {
|
|
57
95
|
throw new Error(
|
|
58
96
|
`State part '${statePartNameArg}' does not exist and no initial state provided`
|
|
@@ -73,9 +111,6 @@ export class Smartstate<StatePartNameType extends string> {
|
|
|
73
111
|
|
|
74
112
|
/**
|
|
75
113
|
* Creates a statepart
|
|
76
|
-
* @param statePartName
|
|
77
|
-
* @param initialPayloadArg
|
|
78
|
-
* @param initMode
|
|
79
114
|
*/
|
|
80
115
|
private async createStatePart<PayloadType>(
|
|
81
116
|
statePartName: StatePartNameType,
|
|
@@ -91,21 +126,20 @@ export class Smartstate<StatePartNameType extends string> {
|
|
|
91
126
|
}
|
|
92
127
|
: null
|
|
93
128
|
);
|
|
129
|
+
newState.smartstateRef = this;
|
|
94
130
|
await newState.init();
|
|
95
131
|
const currentState = newState.getState();
|
|
96
132
|
|
|
97
133
|
if (initMode === 'persistent' && currentState !== undefined) {
|
|
98
|
-
// Persisted state exists - merge with defaults, persisted values take precedence
|
|
99
134
|
await newState.setState({
|
|
100
135
|
...initialPayloadArg,
|
|
101
136
|
...currentState,
|
|
102
137
|
});
|
|
103
138
|
} else {
|
|
104
|
-
// No persisted state or non-persistent mode
|
|
105
139
|
await newState.setState(initialPayloadArg);
|
|
106
140
|
}
|
|
107
141
|
|
|
108
142
|
this.statePartMap[statePartName] = newState;
|
|
109
143
|
return newState;
|
|
110
144
|
}
|
|
111
|
-
}
|
|
145
|
+
}
|
|
@@ -1,21 +1,54 @@
|
|
|
1
1
|
import * as plugins from './smartstate.plugins.js';
|
|
2
|
+
import { Observable, shareReplay, takeUntil } from 'rxjs';
|
|
2
3
|
import { StateAction, type IActionDef } from './smartstate.classes.stateaction.js';
|
|
4
|
+
import type { Smartstate } from './smartstate.classes.smartstate.js';
|
|
5
|
+
|
|
6
|
+
export type TMiddleware<TPayload> = (
|
|
7
|
+
newState: TPayload,
|
|
8
|
+
oldState: TPayload | undefined,
|
|
9
|
+
) => TPayload | Promise<TPayload>;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* creates an Observable that emits once when the given AbortSignal fires
|
|
13
|
+
*/
|
|
14
|
+
function fromAbortSignal(signal: AbortSignal): Observable<void> {
|
|
15
|
+
return new Observable<void>((subscriber) => {
|
|
16
|
+
if (signal.aborted) {
|
|
17
|
+
subscriber.next();
|
|
18
|
+
subscriber.complete();
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const handler = () => {
|
|
22
|
+
subscriber.next();
|
|
23
|
+
subscriber.complete();
|
|
24
|
+
};
|
|
25
|
+
signal.addEventListener('abort', handler);
|
|
26
|
+
return () => signal.removeEventListener('abort', handler);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
3
29
|
|
|
4
30
|
export class StatePart<TStatePartName, TStatePayload> {
|
|
5
31
|
public name: TStatePartName;
|
|
6
32
|
public state = new plugins.smartrx.rxjs.Subject<TStatePayload>();
|
|
7
33
|
public stateStore: TStatePayload | undefined;
|
|
34
|
+
public smartstateRef?: Smartstate<any>;
|
|
8
35
|
private cumulativeDeferred = plugins.smartpromise.cumulativeDefer();
|
|
9
36
|
|
|
10
37
|
private pendingCumulativeNotification: ReturnType<typeof setTimeout> | null = null;
|
|
38
|
+
private pendingBatchNotification = false;
|
|
11
39
|
|
|
12
40
|
private webStoreOptions: plugins.webstore.IWebStoreOptions;
|
|
13
|
-
private webStore: plugins.webstore.WebStore<TStatePayload> | null = null;
|
|
41
|
+
private webStore: plugins.webstore.WebStore<TStatePayload> | null = null;
|
|
42
|
+
|
|
43
|
+
private middlewares: TMiddleware<TStatePayload>[] = [];
|
|
44
|
+
|
|
45
|
+
// Selector memoization
|
|
46
|
+
private selectorCache = new WeakMap<Function, plugins.smartrx.rxjs.Observable<any>>();
|
|
47
|
+
private defaultSelectObservable: plugins.smartrx.rxjs.Observable<TStatePayload> | null = null;
|
|
14
48
|
|
|
15
49
|
constructor(nameArg: TStatePartName, webStoreOptionsArg?: plugins.webstore.IWebStoreOptions) {
|
|
16
50
|
this.name = nameArg;
|
|
17
51
|
|
|
18
|
-
// Initialize WebStore if webStoreOptions are provided
|
|
19
52
|
if (webStoreOptionsArg) {
|
|
20
53
|
this.webStoreOptions = webStoreOptionsArg;
|
|
21
54
|
}
|
|
@@ -43,23 +76,43 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|
|
43
76
|
return this.stateStore;
|
|
44
77
|
}
|
|
45
78
|
|
|
79
|
+
/**
|
|
80
|
+
* adds a middleware that intercepts setState calls.
|
|
81
|
+
* middleware can transform the state or throw to reject it.
|
|
82
|
+
* returns a removal function.
|
|
83
|
+
*/
|
|
84
|
+
public addMiddleware(middleware: TMiddleware<TStatePayload>): () => void {
|
|
85
|
+
this.middlewares.push(middleware);
|
|
86
|
+
return () => {
|
|
87
|
+
const idx = this.middlewares.indexOf(middleware);
|
|
88
|
+
if (idx !== -1) {
|
|
89
|
+
this.middlewares.splice(idx, 1);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
46
94
|
/**
|
|
47
95
|
* sets the stateStore to the new state
|
|
48
|
-
* @param newStateArg
|
|
49
96
|
*/
|
|
50
97
|
public async setState(newStateArg: TStatePayload) {
|
|
98
|
+
// Run middleware chain
|
|
99
|
+
let processedState = newStateArg;
|
|
100
|
+
for (const mw of this.middlewares) {
|
|
101
|
+
processedState = await mw(processedState, this.stateStore);
|
|
102
|
+
}
|
|
103
|
+
|
|
51
104
|
// Validate state structure
|
|
52
|
-
if (!this.validateState(
|
|
105
|
+
if (!this.validateState(processedState)) {
|
|
53
106
|
throw new Error(`Invalid state structure for state part '${this.name}'`);
|
|
54
107
|
}
|
|
55
108
|
|
|
56
|
-
// Save to WebStore first to ensure atomicity
|
|
109
|
+
// Save to WebStore first to ensure atomicity
|
|
57
110
|
if (this.webStore) {
|
|
58
|
-
await this.webStore.set(String(this.name),
|
|
111
|
+
await this.webStore.set(String(this.name), processedState);
|
|
59
112
|
}
|
|
60
113
|
|
|
61
114
|
// Update in-memory state after successful persistence
|
|
62
|
-
this.stateStore =
|
|
115
|
+
this.stateStore = processedState;
|
|
63
116
|
await this.notifyChange();
|
|
64
117
|
|
|
65
118
|
return this.stateStore;
|
|
@@ -67,11 +120,8 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|
|
67
120
|
|
|
68
121
|
/**
|
|
69
122
|
* Validates state structure - can be overridden for custom validation
|
|
70
|
-
* @param stateArg
|
|
71
123
|
*/
|
|
72
124
|
protected validateState(stateArg: any): stateArg is TStatePayload {
|
|
73
|
-
// Basic validation - ensure state is not null/undefined
|
|
74
|
-
// Subclasses can override for more specific validation
|
|
75
125
|
return stateArg !== null && stateArg !== undefined;
|
|
76
126
|
}
|
|
77
127
|
|
|
@@ -82,6 +132,14 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|
|
82
132
|
if (!this.stateStore) {
|
|
83
133
|
return;
|
|
84
134
|
}
|
|
135
|
+
|
|
136
|
+
// If inside a batch, defer the notification
|
|
137
|
+
if (this.smartstateRef?.isBatching) {
|
|
138
|
+
this.pendingBatchNotification = true;
|
|
139
|
+
this.smartstateRef.registerPendingNotification(this);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
85
143
|
const createStateHash = async (stateArg: any) => {
|
|
86
144
|
return await plugins.smarthashWeb.sha256FromString(plugins.smartjson.stableOneWayStringify(stateArg));
|
|
87
145
|
};
|
|
@@ -99,10 +157,9 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|
|
99
157
|
private lastStateNotificationPayloadHash: any;
|
|
100
158
|
|
|
101
159
|
/**
|
|
102
|
-
* creates a cumulative notification by adding a change notification at the end of the call stack
|
|
160
|
+
* creates a cumulative notification by adding a change notification at the end of the call stack
|
|
103
161
|
*/
|
|
104
162
|
public notifyChangeCumulative() {
|
|
105
|
-
// Debounce: clear any pending notification
|
|
106
163
|
if (this.pendingCumulativeNotification) {
|
|
107
164
|
clearTimeout(this.pendingCumulativeNotification);
|
|
108
165
|
}
|
|
@@ -116,27 +173,56 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|
|
116
173
|
}
|
|
117
174
|
|
|
118
175
|
/**
|
|
119
|
-
* selects a state or a substate
|
|
176
|
+
* selects a state or a substate.
|
|
177
|
+
* supports an optional AbortSignal for automatic unsubscription.
|
|
178
|
+
* memoizes observables by selector function reference.
|
|
120
179
|
*/
|
|
121
180
|
public select<T = TStatePayload>(
|
|
122
|
-
selectorFn?: (state: TStatePayload) => T
|
|
181
|
+
selectorFn?: (state: TStatePayload) => T,
|
|
182
|
+
options?: { signal?: AbortSignal }
|
|
123
183
|
): plugins.smartrx.rxjs.Observable<T> {
|
|
124
|
-
|
|
125
|
-
|
|
184
|
+
const hasSignal = options?.signal != null;
|
|
185
|
+
|
|
186
|
+
// Check memoization cache (only for non-signal selects)
|
|
187
|
+
if (!hasSignal) {
|
|
188
|
+
if (!selectorFn) {
|
|
189
|
+
if (this.defaultSelectObservable) {
|
|
190
|
+
return this.defaultSelectObservable as unknown as plugins.smartrx.rxjs.Observable<T>;
|
|
191
|
+
}
|
|
192
|
+
} else if (this.selectorCache.has(selectorFn)) {
|
|
193
|
+
return this.selectorCache.get(selectorFn)!;
|
|
194
|
+
}
|
|
126
195
|
}
|
|
127
|
-
|
|
196
|
+
|
|
197
|
+
const effectiveSelectorFn = selectorFn || ((state: TStatePayload) => <T>(<any>state));
|
|
198
|
+
|
|
199
|
+
let mapped = this.state.pipe(
|
|
128
200
|
plugins.smartrx.rxjs.ops.startWith(this.getState()),
|
|
129
201
|
plugins.smartrx.rxjs.ops.filter((stateArg): stateArg is TStatePayload => stateArg !== undefined),
|
|
130
202
|
plugins.smartrx.rxjs.ops.map((stateArg) => {
|
|
131
203
|
try {
|
|
132
|
-
return
|
|
204
|
+
return effectiveSelectorFn(stateArg);
|
|
133
205
|
} catch (e) {
|
|
134
206
|
console.error(`Selector error in state part '${this.name}':`, e);
|
|
135
207
|
return undefined;
|
|
136
208
|
}
|
|
137
209
|
})
|
|
138
210
|
);
|
|
139
|
-
|
|
211
|
+
|
|
212
|
+
if (hasSignal) {
|
|
213
|
+
mapped = mapped.pipe(takeUntil(fromAbortSignal(options.signal)));
|
|
214
|
+
return mapped;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Apply shareReplay for caching and store in memo cache
|
|
218
|
+
const shared = mapped.pipe(shareReplay({ bufferSize: 1, refCount: true }));
|
|
219
|
+
if (!selectorFn) {
|
|
220
|
+
this.defaultSelectObservable = shared as unknown as plugins.smartrx.rxjs.Observable<TStatePayload>;
|
|
221
|
+
} else {
|
|
222
|
+
this.selectorCache.set(selectorFn, shared);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return shared;
|
|
140
226
|
}
|
|
141
227
|
|
|
142
228
|
/**
|
|
@@ -159,18 +245,32 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|
|
159
245
|
}
|
|
160
246
|
|
|
161
247
|
/**
|
|
162
|
-
* waits until a certain part of the state becomes available
|
|
163
|
-
*
|
|
164
|
-
* @param timeoutMs - optional timeout in milliseconds to prevent indefinite waiting
|
|
248
|
+
* waits until a certain part of the state becomes available.
|
|
249
|
+
* supports optional timeout and AbortSignal.
|
|
165
250
|
*/
|
|
166
251
|
public async waitUntilPresent<T = TStatePayload>(
|
|
167
252
|
selectorFn?: (state: TStatePayload) => T,
|
|
168
|
-
timeoutMs?: number
|
|
253
|
+
optionsOrTimeout?: number | { timeoutMs?: number; signal?: AbortSignal }
|
|
169
254
|
): Promise<T> {
|
|
255
|
+
// Parse backward-compatible args
|
|
256
|
+
let timeoutMs: number | undefined;
|
|
257
|
+
let signal: AbortSignal | undefined;
|
|
258
|
+
if (typeof optionsOrTimeout === 'number') {
|
|
259
|
+
timeoutMs = optionsOrTimeout;
|
|
260
|
+
} else if (optionsOrTimeout) {
|
|
261
|
+
timeoutMs = optionsOrTimeout.timeoutMs;
|
|
262
|
+
signal = optionsOrTimeout.signal;
|
|
263
|
+
}
|
|
264
|
+
|
|
170
265
|
const done = plugins.smartpromise.defer<T>();
|
|
171
266
|
const selectedObservable = this.select(selectorFn);
|
|
172
267
|
let resolved = false;
|
|
173
268
|
|
|
269
|
+
// Check if already aborted
|
|
270
|
+
if (signal?.aborted) {
|
|
271
|
+
throw new Error('Aborted');
|
|
272
|
+
}
|
|
273
|
+
|
|
174
274
|
const subscription = selectedObservable.subscribe((value) => {
|
|
175
275
|
if (value && !resolved) {
|
|
176
276
|
resolved = true;
|
|
@@ -189,12 +289,29 @@ export class StatePart<TStatePartName, TStatePayload> {
|
|
|
189
289
|
}, timeoutMs);
|
|
190
290
|
}
|
|
191
291
|
|
|
292
|
+
// Handle abort signal
|
|
293
|
+
const abortHandler = signal ? () => {
|
|
294
|
+
if (!resolved) {
|
|
295
|
+
resolved = true;
|
|
296
|
+
subscription.unsubscribe();
|
|
297
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
298
|
+
done.reject(new Error('Aborted'));
|
|
299
|
+
}
|
|
300
|
+
} : undefined;
|
|
301
|
+
|
|
302
|
+
if (signal && abortHandler) {
|
|
303
|
+
signal.addEventListener('abort', abortHandler);
|
|
304
|
+
}
|
|
305
|
+
|
|
192
306
|
try {
|
|
193
307
|
const result = await done.promise;
|
|
194
308
|
return result;
|
|
195
309
|
} finally {
|
|
196
310
|
subscription.unsubscribe();
|
|
197
311
|
if (timeoutId) clearTimeout(timeoutId);
|
|
312
|
+
if (signal && abortHandler) {
|
|
313
|
+
signal.removeEventListener('abort', abortHandler);
|
|
314
|
+
}
|
|
198
315
|
}
|
|
199
316
|
}
|
|
200
317
|
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { StatePart } from './smartstate.classes.statepart.js';
|
|
2
|
+
|
|
3
|
+
export interface IContextProviderOptions<TPayload> {
|
|
4
|
+
/** the context key (compared by strict equality) */
|
|
5
|
+
context: unknown;
|
|
6
|
+
/** the state part to provide */
|
|
7
|
+
statePart: StatePart<any, TPayload>;
|
|
8
|
+
/** optional selector to provide a derived value instead of the full state */
|
|
9
|
+
selectorFn?: (state: TPayload) => any;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* attaches a Context Protocol provider to an HTML element.
|
|
14
|
+
* listens for `context-request` events and responds with the state part's value.
|
|
15
|
+
* if subscribe=true, retains the callback and invokes it on every state change.
|
|
16
|
+
* returns a cleanup function that removes the listener and unsubscribes.
|
|
17
|
+
*/
|
|
18
|
+
export function attachContextProvider<TPayload>(
|
|
19
|
+
element: HTMLElement,
|
|
20
|
+
options: IContextProviderOptions<TPayload>,
|
|
21
|
+
): () => void {
|
|
22
|
+
const { context, statePart, selectorFn } = options;
|
|
23
|
+
const subscribers = new Set<(value: any, unsubscribe?: () => void) => void>();
|
|
24
|
+
|
|
25
|
+
const subscription = statePart.select(selectorFn).subscribe((value) => {
|
|
26
|
+
for (const cb of subscribers) {
|
|
27
|
+
cb(value);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const getValue = (): any => {
|
|
32
|
+
const state = statePart.getState();
|
|
33
|
+
if (state === undefined) return undefined;
|
|
34
|
+
return selectorFn ? selectorFn(state) : state;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handler = (event: Event) => {
|
|
38
|
+
const e = event as CustomEvent;
|
|
39
|
+
const detail = e.detail;
|
|
40
|
+
if (!detail || detail.context !== context) return;
|
|
41
|
+
|
|
42
|
+
e.stopPropagation();
|
|
43
|
+
|
|
44
|
+
if (detail.subscribe) {
|
|
45
|
+
const cb = detail.callback;
|
|
46
|
+
subscribers.add(cb);
|
|
47
|
+
const unsubscribe = () => subscribers.delete(cb);
|
|
48
|
+
cb(getValue(), unsubscribe);
|
|
49
|
+
} else {
|
|
50
|
+
detail.callback(getValue());
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
element.addEventListener('context-request', handler);
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
element.removeEventListener('context-request', handler);
|
|
58
|
+
subscription.unsubscribe();
|
|
59
|
+
subscribers.clear();
|
|
60
|
+
};
|
|
61
|
+
}
|