@pixelume/sdk 0.5.0 → 0.6.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.
@@ -0,0 +1,93 @@
1
+ import type { ResolvedAnalyticsConfig } from '../../core/config.js';
2
+ import type { PixelumeBrowserAdapter } from '../../core/types.js';
3
+ import type { PixelumeAuthListener, PixelumeAuthState } from '../auth/AuthModule.js';
4
+ import { type AnalyticsProvider } from './AnalyticsProvider.js';
5
+ /** Flat values accepted as event properties. Other values are dropped (see `onIssue`). */
6
+ export type PixelumeAnalyticsProperties = Readonly<Record<string, string | number | boolean | null | undefined>>;
7
+ /**
8
+ * - `unknown`: the player has not decided (or the choice is not remembered). Nothing is sent.
9
+ * - `granted`: events are sent.
10
+ * - `denied`: nothing is sent and the analytics identifiers were removed from the browser.
11
+ */
12
+ export type PixelumeAnalyticsConsent = 'unknown' | 'granted' | 'denied';
13
+ /**
14
+ * - `disabled`: not configured (or the client was destroyed): every call is a no-op.
15
+ * - `waiting_consent`: configured, consent not granted. Events are discarded, not queued.
16
+ * - `loading`: consent granted, provider loading; events wait in a small in-memory queue.
17
+ * - `ready`: events are being delivered.
18
+ * - `unavailable`: the provider could not load here (no browser, blocked script/network). Events
19
+ * are discarded; a later `setConsent(true)` tries again.
20
+ */
21
+ export type PixelumeAnalyticsStatus = 'disabled' | 'waiting_consent' | 'loading' | 'ready' | 'unavailable';
22
+ /**
23
+ * Gameplay analytics, decoupled from the provider (PostHog today). Every method is synchronous,
24
+ * never throws and never waits for the network: a failing provider cannot stop the game.
25
+ *
26
+ * A **game session** is one continuous play period defined by the game (e.g. from "Play" until the
27
+ * player returns to the menu, or one match); it is unrelated to the Pixelume login session.
28
+ */
29
+ export interface PixelumeAnalyticsApi {
30
+ readonly consent: PixelumeAnalyticsConsent;
31
+ readonly status: PixelumeAnalyticsStatus;
32
+ /** Id of the current game session (`session_id` of every event), or `null` outside one. */
33
+ readonly sessionId: string | null;
34
+ /** `true` when events are (or will shortly be) delivered: configured, consented, available. */
35
+ isEnabled(): boolean;
36
+ /**
37
+ * Records the player's explicit decision. `false` discards pending events, stops delivery and
38
+ * removes the analytics identifiers from the browser; `true` after `false` starts a new
39
+ * anonymous identity. Repeating the current value does nothing.
40
+ */
41
+ setConsent(granted: boolean): void;
42
+ /** Custom event: snake_case name (1–64 chars) not in the built-in catalog. */
43
+ track(event: string, properties?: PixelumeAnalyticsProperties): void;
44
+ /** Starts a game session (`game_session_started`). Does nothing while one is active. */
45
+ startSession(properties?: PixelumeAnalyticsProperties): void;
46
+ /**
47
+ * Ends the current game session (`game_session_ended` with `duration_ms` and
48
+ * `visible_duration_ms`). Does nothing without an active session. Closing the tab does **not**
49
+ * end the session: only the game knows when play really ended.
50
+ */
51
+ endSession(properties?: PixelumeAnalyticsProperties): void;
52
+ levelStarted(level: string | number, properties?: PixelumeAnalyticsProperties): void;
53
+ /** `level_completed`; carries `duration_ms` when the matching `levelStarted` was seen. */
54
+ levelCompleted(level: string | number, properties?: PixelumeAnalyticsProperties): void;
55
+ /** `level_failed` with an optional short `reason` (e.g. `'out_of_moves'`). */
56
+ levelFailed(level: string | number, reason?: string, properties?: PixelumeAnalyticsProperties): void;
57
+ }
58
+ /** Subset of `pixelume.auth` used to follow the player's identity. */
59
+ export interface AnalyticsAuthSource {
60
+ readonly state: PixelumeAuthState;
61
+ subscribe(listener: PixelumeAuthListener): () => void;
62
+ }
63
+ /** Visibility signals; tests inject a double. */
64
+ export interface VisibilitySource {
65
+ isVisible(): boolean;
66
+ subscribe(onChange: () => void): () => void;
67
+ }
68
+ export interface AnalyticsModuleOptions {
69
+ readonly config: ResolvedAnalyticsConfig | null;
70
+ readonly auth: AnalyticsAuthSource;
71
+ readonly browser?: PixelumeBrowserAdapter | undefined;
72
+ /** Test seams. */
73
+ readonly createProvider?: (config: ResolvedAnalyticsConfig) => AnalyticsProvider;
74
+ readonly visibility?: VisibilitySource | null;
75
+ readonly now?: () => number;
76
+ }
77
+ /** Events kept while the provider loads after consent; older ones are kept, newer dropped. */
78
+ export declare const MAX_PENDING_EVENTS = 100;
79
+ export declare class AnalyticsModule implements PixelumeAnalyticsApi {
80
+ #private;
81
+ constructor(options: AnalyticsModuleOptions);
82
+ get consent(): PixelumeAnalyticsConsent;
83
+ get status(): PixelumeAnalyticsStatus;
84
+ get sessionId(): string | null;
85
+ isEnabled(): boolean;
86
+ setConsent(granted: boolean): void;
87
+ track(event: string, properties?: PixelumeAnalyticsProperties): void;
88
+ startSession(properties?: PixelumeAnalyticsProperties): void;
89
+ endSession(properties?: PixelumeAnalyticsProperties): void;
90
+ levelStarted(level: string | number, properties?: PixelumeAnalyticsProperties): void;
91
+ levelCompleted(level: string | number, properties?: PixelumeAnalyticsProperties): void;
92
+ levelFailed(level: string | number, reason?: string, properties?: PixelumeAnalyticsProperties): void;
93
+ }
@@ -0,0 +1,461 @@
1
+ import { SDK_VERSION } from '../../version.js';
2
+ import { NoopAnalyticsProvider, } from './AnalyticsProvider.js';
3
+ import { ANALYTICS_EVENTS, isCatalogEvent, isValidEventName, normalizeLevel, normalizeReason, sanitizeProperties, } from './events.js';
4
+ import { PostHogAnalyticsProvider } from './PostHogAnalyticsProvider.js';
5
+ /** Events kept while the provider loads after consent; older ones are kept, newer dropped. */
6
+ export const MAX_PENDING_EVENTS = 100;
7
+ const MAX_TRACKED_LEVELS = 50;
8
+ const CONSENT_VERSION = 1;
9
+ /** Anonymous reasons meaning "the account is no longer the one playing". */
10
+ const SEPARATING_REASONS = new Set(['logged_out', 'expired', 'session_ended']);
11
+ /** Keys set by the SDK for an event, protected even when the SDK does not set them this time. */
12
+ const NO_PROTECTED_KEYS = new Set();
13
+ const SESSION_END_KEYS = new Set(['duration_ms', 'visible_duration_ms']);
14
+ const LEVEL_KEYS = new Set(['level', 'duration_ms']);
15
+ const LEVEL_FAILED_KEYS = new Set(['level', 'duration_ms', 'reason']);
16
+ export class AnalyticsModule {
17
+ #config;
18
+ #provider;
19
+ #browser;
20
+ #visibility;
21
+ #now;
22
+ #unsubscribeAuth;
23
+ #consent = 'unknown';
24
+ #status;
25
+ #pending = [];
26
+ #identity = null;
27
+ #session = null;
28
+ #loading = false;
29
+ #loaded = false;
30
+ /** Consent was revoked while the provider was loading: wipe it as soon as it is ready. */
31
+ #revokedWhileLoading = false;
32
+ #destroyed = false;
33
+ constructor(options) {
34
+ this.#config = options.config;
35
+ this.#browser = options.browser;
36
+ this.#visibility =
37
+ options.visibility === undefined ? browserVisibilitySource() : options.visibility;
38
+ this.#now = options.now ?? (() => Date.now());
39
+ const config = this.#config;
40
+ if (config === null) {
41
+ this.#provider = new NoopAnalyticsProvider();
42
+ this.#status = 'disabled';
43
+ this.#unsubscribeAuth = () => undefined;
44
+ return;
45
+ }
46
+ this.#provider = (options.createProvider ?? createDefaultProvider)(config);
47
+ this.#status = 'waiting_consent';
48
+ this.#identity = identityFor(options.auth.state);
49
+ this.#unsubscribeAuth = options.auth.subscribe((state) => {
50
+ this.#onAuthState(state);
51
+ });
52
+ const remembered = config.rememberConsent ? this.#readConsent() : null;
53
+ if (remembered === false)
54
+ this.#consent = 'denied';
55
+ if (remembered === true) {
56
+ this.#consent = 'granted';
57
+ // Asynchronous: constructing the client never waits for, nor fails because of, the provider.
58
+ this.#activate();
59
+ }
60
+ }
61
+ get consent() {
62
+ return this.#consent;
63
+ }
64
+ get status() {
65
+ return this.#status;
66
+ }
67
+ get sessionId() {
68
+ return this.#session?.id ?? null;
69
+ }
70
+ isEnabled() {
71
+ return this.#status === 'loading' || this.#status === 'ready';
72
+ }
73
+ setConsent(granted) {
74
+ const value = granted;
75
+ if (this.#destroyed || this.#config === null || typeof value !== 'boolean')
76
+ return;
77
+ const next = value ? 'granted' : 'denied';
78
+ // Repeating the decision is a no-op, except to retry a provider that could not load.
79
+ if (next === this.#consent && this.#status !== 'unavailable')
80
+ return;
81
+ this.#consent = next;
82
+ if (this.#config.rememberConsent)
83
+ this.#writeConsent(value);
84
+ if (value) {
85
+ this.#activate();
86
+ return;
87
+ }
88
+ this.#pending = [];
89
+ this.#status = 'waiting_consent';
90
+ if (this.#loading)
91
+ this.#revokedWhileLoading = true;
92
+ this.#guard(() => {
93
+ this.#provider.optOut();
94
+ });
95
+ }
96
+ track(event, properties) {
97
+ if (!isValidEventName(event)) {
98
+ this.#report('invalid_event', null);
99
+ return;
100
+ }
101
+ if (isCatalogEvent(event)) {
102
+ this.#report('reserved_event', event);
103
+ return;
104
+ }
105
+ this.#emit(event, properties, {});
106
+ }
107
+ startSession(properties) {
108
+ if (this.#destroyed || this.#session !== null)
109
+ return;
110
+ const now = this.#now();
111
+ // A no-op client never touches the page, not even to listen.
112
+ const visibility = this.#config === null ? null : this.#visibility;
113
+ const session = {
114
+ id: randomId(this.#browser),
115
+ startedAt: now,
116
+ visibleMs: 0,
117
+ visibleSince: visibility === null || visibility.isVisible() ? now : null,
118
+ unsubscribeVisibility: visibility?.subscribe(() => {
119
+ this.#onVisibilityChange(session);
120
+ }) ?? null,
121
+ levels: new Map(),
122
+ };
123
+ this.#session = session;
124
+ this.#emit(ANALYTICS_EVENTS.sessionStarted, properties, {});
125
+ }
126
+ endSession(properties) {
127
+ const session = this.#session;
128
+ if (session === null)
129
+ return;
130
+ const now = this.#now();
131
+ const visibleMs = session.visibleMs + (session.visibleSince === null ? 0 : now - session.visibleSince);
132
+ this.#emit(ANALYTICS_EVENTS.sessionEnded, properties, {
133
+ duration_ms: Math.max(0, Math.round(now - session.startedAt)),
134
+ visible_duration_ms: Math.max(0, Math.round(visibleMs)),
135
+ }, SESSION_END_KEYS);
136
+ this.#closeSession();
137
+ }
138
+ levelStarted(level, properties) {
139
+ const normalized = this.#level(ANALYTICS_EVENTS.levelStarted, level);
140
+ if (normalized === null)
141
+ return;
142
+ const levels = this.#session?.levels;
143
+ if (levels !== undefined) {
144
+ levels.delete(normalized);
145
+ if (levels.size >= MAX_TRACKED_LEVELS) {
146
+ const oldest = levels.keys().next();
147
+ if (oldest.done !== true)
148
+ levels.delete(oldest.value);
149
+ }
150
+ levels.set(normalized, this.#now());
151
+ }
152
+ this.#emit(ANALYTICS_EVENTS.levelStarted, properties, { level: normalized }, LEVEL_KEYS);
153
+ }
154
+ levelCompleted(level, properties) {
155
+ const normalized = this.#level(ANALYTICS_EVENTS.levelCompleted, level);
156
+ if (normalized === null)
157
+ return;
158
+ this.#emit(ANALYTICS_EVENTS.levelCompleted, properties, { level: normalized, ...this.#levelDuration(normalized) }, LEVEL_KEYS);
159
+ }
160
+ levelFailed(level, reason, properties) {
161
+ const normalized = this.#level(ANALYTICS_EVENTS.levelFailed, level);
162
+ if (normalized === null)
163
+ return;
164
+ const extra = { level: normalized, ...this.#levelDuration(normalized) };
165
+ if (reason !== undefined) {
166
+ const value = normalizeReason(reason);
167
+ if (value === null)
168
+ this.#report('invalid_property', ANALYTICS_EVENTS.levelFailed, 'reason');
169
+ else
170
+ extra.reason = value;
171
+ }
172
+ this.#emit(ANALYTICS_EVENTS.levelFailed, properties, extra, LEVEL_FAILED_KEYS);
173
+ }
174
+ /**
175
+ * @internal Called by `PixelumeClient.destroy()`: releases listeners and stops delivery. Does not
176
+ * end the game session nor change consent; posthog may still flush what it already accepted.
177
+ */
178
+ dispose() {
179
+ if (this.#destroyed)
180
+ return;
181
+ this.#destroyed = true;
182
+ this.#unsubscribeAuth();
183
+ this.#closeSession();
184
+ this.#pending = [];
185
+ this.#status = 'disabled';
186
+ this.#guard(() => {
187
+ this.#provider.detach();
188
+ });
189
+ }
190
+ #activate() {
191
+ if (this.#destroyed || this.#consent !== 'granted')
192
+ return;
193
+ if (this.#loading) {
194
+ this.#status = 'loading';
195
+ return;
196
+ }
197
+ if (this.#loaded) {
198
+ this.#status = 'ready';
199
+ this.#guard(() => {
200
+ this.#provider.optIn();
201
+ });
202
+ this.#applyIdentity();
203
+ return;
204
+ }
205
+ this.#status = 'loading';
206
+ this.#loading = true;
207
+ let loading;
208
+ try {
209
+ loading = this.#provider.load();
210
+ }
211
+ catch (error) {
212
+ loading = Promise.reject(error instanceof Error ? error : new Error(String(error)));
213
+ }
214
+ loading.then(() => {
215
+ this.#onProviderReady();
216
+ }, () => {
217
+ this.#onProviderFailed();
218
+ });
219
+ }
220
+ #onProviderReady() {
221
+ this.#loading = false;
222
+ if (this.#destroyed)
223
+ return;
224
+ this.#loaded = true;
225
+ if (this.#revokedWhileLoading) {
226
+ // Revoked while loading: wipe whatever the provider set up before using it again.
227
+ this.#revokedWhileLoading = false;
228
+ this.#guard(() => {
229
+ this.#provider.optOut();
230
+ });
231
+ }
232
+ if (this.#consent !== 'granted')
233
+ return;
234
+ this.#status = 'ready';
235
+ this.#guard(() => {
236
+ this.#provider.optIn();
237
+ });
238
+ this.#applyIdentity();
239
+ const pending = this.#pending;
240
+ this.#pending = [];
241
+ for (const event of pending)
242
+ this.#deliver(event);
243
+ }
244
+ #onProviderFailed() {
245
+ this.#loading = false;
246
+ this.#revokedWhileLoading = false;
247
+ if (this.#destroyed)
248
+ return;
249
+ this.#pending = [];
250
+ if (this.#consent !== 'granted')
251
+ return;
252
+ this.#status = 'unavailable';
253
+ this.#report('provider_unavailable', null);
254
+ }
255
+ #onAuthState(state) {
256
+ const identity = identityFor(state);
257
+ if (identity === null)
258
+ return;
259
+ this.#identity = identity;
260
+ this.#applyIdentity();
261
+ }
262
+ #applyIdentity() {
263
+ const identity = this.#identity;
264
+ if (identity === null || this.#status !== 'ready' || this.#consent !== 'granted')
265
+ return;
266
+ this.#guard(() => {
267
+ this.#provider.identify(identity);
268
+ });
269
+ }
270
+ #emit(name, properties, extra, protectedKeys = NO_PROTECTED_KEYS) {
271
+ if (this.#destroyed || this.#config === null)
272
+ return;
273
+ const keys = new Set([...protectedKeys, ...Object.keys(extra)]);
274
+ let sanitized;
275
+ try {
276
+ sanitized = sanitizeProperties(name, properties, keys, this.#reporter);
277
+ }
278
+ catch {
279
+ // Hostile input (throwing getters, proxies): drop the event, never the game.
280
+ this.#report('invalid_property', name);
281
+ return;
282
+ }
283
+ if (this.#consent !== 'granted')
284
+ return;
285
+ if (this.#status !== 'loading' && this.#status !== 'ready')
286
+ return;
287
+ const event = {
288
+ name,
289
+ properties: { ...sanitized, ...extra, ...this.#metadata() },
290
+ timestamp: this.#now(),
291
+ };
292
+ if (this.#status === 'ready') {
293
+ this.#deliver(event);
294
+ }
295
+ else if (this.#pending.length < MAX_PENDING_EVENTS) {
296
+ this.#pending.push({ ...event, delayed: true });
297
+ }
298
+ else {
299
+ this.#report('queue_full', name);
300
+ }
301
+ }
302
+ #deliver(event) {
303
+ this.#guard(() => {
304
+ this.#provider.capture(event);
305
+ }, event.name);
306
+ }
307
+ #metadata() {
308
+ const config = this.#config;
309
+ const metadata = {
310
+ game_id: config?.gameId ?? null,
311
+ sdk_version: SDK_VERSION,
312
+ platform: 'web',
313
+ };
314
+ if (config?.gameVersion != null)
315
+ metadata.game_version = config.gameVersion;
316
+ if (this.#session !== null)
317
+ metadata.session_id = this.#session.id;
318
+ return metadata;
319
+ }
320
+ #level(event, level) {
321
+ const normalized = normalizeLevel(level);
322
+ if (normalized === null)
323
+ this.#report('invalid_level', event);
324
+ return normalized;
325
+ }
326
+ #levelDuration(level) {
327
+ const levels = this.#session?.levels;
328
+ const startedAt = levels?.get(level);
329
+ if (levels === undefined || startedAt === undefined)
330
+ return {};
331
+ levels.delete(level);
332
+ return { duration_ms: Math.max(0, Math.round(this.#now() - startedAt)) };
333
+ }
334
+ #onVisibilityChange(session) {
335
+ if (this.#session !== session || this.#visibility === null)
336
+ return;
337
+ const now = this.#now();
338
+ if (this.#visibility.isVisible()) {
339
+ session.visibleSince ??= now;
340
+ }
341
+ else if (session.visibleSince !== null) {
342
+ session.visibleMs += now - session.visibleSince;
343
+ session.visibleSince = null;
344
+ }
345
+ }
346
+ #closeSession() {
347
+ const session = this.#session;
348
+ this.#session = null;
349
+ session?.unsubscribeVisibility?.();
350
+ }
351
+ #reporter = (code, event, property = null) => {
352
+ this.#report(code, event, property);
353
+ };
354
+ #report(code, event, property = null) {
355
+ const onIssue = this.#config?.onIssue;
356
+ if (!onIssue)
357
+ return;
358
+ try {
359
+ onIssue(Object.freeze({ code, event, property }));
360
+ }
361
+ catch {
362
+ // Diagnostics must never affect the game.
363
+ }
364
+ }
365
+ #guard(action, event = null) {
366
+ try {
367
+ action();
368
+ }
369
+ catch {
370
+ this.#report('provider_error', event);
371
+ }
372
+ }
373
+ #consentKey() {
374
+ return `pixelume.analytics.consent.${this.#config?.gameId ?? 'unconfigured'}`;
375
+ }
376
+ #storage() {
377
+ try {
378
+ if (this.#browser !== undefined)
379
+ return this.#browser.persistentStorage ?? null;
380
+ return typeof globalThis.localStorage === 'undefined' ? null : globalThis.localStorage;
381
+ }
382
+ catch {
383
+ // Blocked storage (privacy settings, sandboxed frames).
384
+ return null;
385
+ }
386
+ }
387
+ #readConsent() {
388
+ try {
389
+ const raw = this.#storage()?.getItem(this.#consentKey()) ?? null;
390
+ if (raw === null)
391
+ return null;
392
+ const value = JSON.parse(raw);
393
+ if (typeof value === 'object' &&
394
+ value !== null &&
395
+ value.version === CONSENT_VERSION &&
396
+ typeof value.granted === 'boolean') {
397
+ return value.granted;
398
+ }
399
+ return null;
400
+ }
401
+ catch {
402
+ return null;
403
+ }
404
+ }
405
+ #writeConsent(granted) {
406
+ try {
407
+ this.#storage()?.setItem(this.#consentKey(), JSON.stringify({ version: CONSENT_VERSION, granted, at: this.#now() }));
408
+ }
409
+ catch {
410
+ // Not remembered: the game asks again on the next visit.
411
+ }
412
+ }
413
+ }
414
+ function createDefaultProvider(config) {
415
+ return new PostHogAnalyticsProvider({
416
+ token: config.provider.token,
417
+ host: config.provider.host,
418
+ gameId: config.gameId,
419
+ });
420
+ }
421
+ /** `null` while authentication is still settling: keep the previous identity. */
422
+ function identityFor(state) {
423
+ if (state.status === 'authenticated')
424
+ return { player: state.player.id, separate: false };
425
+ if (state.status === 'anonymous') {
426
+ return { player: null, separate: SEPARATING_REASONS.has(state.reason) };
427
+ }
428
+ return null;
429
+ }
430
+ function randomId(browser) {
431
+ const globalCrypto = Reflect.get(globalThis, 'crypto');
432
+ const crypto = browser?.crypto ?? globalCrypto;
433
+ if (typeof crypto?.randomUUID === 'function')
434
+ return crypto.randomUUID();
435
+ const bytes = new Uint8Array(16);
436
+ if (typeof crypto?.getRandomValues === 'function') {
437
+ crypto.getRandomValues(bytes);
438
+ }
439
+ else {
440
+ for (let index = 0; index < bytes.length; index += 1) {
441
+ bytes[index] = Math.floor(Math.random() * 256);
442
+ }
443
+ }
444
+ return [...bytes].map((byte) => byte.toString(16).padStart(2, '0')).join('');
445
+ }
446
+ /** `document.visibilityState`, looked up lazily; `null` outside a browser. */
447
+ function browserVisibilitySource() {
448
+ const document = Reflect.get(globalThis, 'document');
449
+ if (document === undefined || typeof document.addEventListener !== 'function')
450
+ return null;
451
+ return {
452
+ isVisible: () => document.visibilityState !== 'hidden',
453
+ subscribe: (onChange) => {
454
+ document.addEventListener('visibilitychange', onChange);
455
+ return () => {
456
+ document.removeEventListener('visibilitychange', onChange);
457
+ };
458
+ },
459
+ };
460
+ }
461
+ //# sourceMappingURL=AnalyticsModule.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnalyticsModule.js","sourceRoot":"","sources":["../../../src/modules/analytics/AnalyticsModule.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAE/C,OAAO,EACL,qBAAqB,GAItB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EACL,gBAAgB,EAChB,cAAc,EACd,gBAAgB,EAChB,cAAc,EACd,eAAe,EACf,kBAAkB,GAGnB,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,wBAAwB,EAAE,MAAM,+BAA+B,CAAC;AAwFzE,8FAA8F;AAC9F,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAG,CAAC;AACtC,MAAM,kBAAkB,GAAG,EAAE,CAAC;AAC9B,MAAM,eAAe,GAAG,CAAC,CAAC;AAC1B,4EAA4E;AAC5E,MAAM,kBAAkB,GAAwB,IAAI,GAAG,CAAC,CAAC,YAAY,EAAE,SAAS,EAAE,eAAe,CAAC,CAAC,CAAC;AACpG,iGAAiG;AACjG,MAAM,iBAAiB,GAAwB,IAAI,GAAG,EAAE,CAAC;AACzD,MAAM,gBAAgB,GAAwB,IAAI,GAAG,CAAC,CAAC,aAAa,EAAE,qBAAqB,CAAC,CAAC,CAAC;AAC9F,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,aAAa,CAAC,CAAC,CAAC;AAC1E,MAAM,iBAAiB,GAAwB,IAAI,GAAG,CAAC,CAAC,OAAO,EAAE,aAAa,EAAE,QAAQ,CAAC,CAAC,CAAC;AAW3F,MAAM,OAAO,eAAe;IACjB,OAAO,CAAiC;IACxC,SAAS,CAAoB;IAC7B,QAAQ,CAAqC;IAC7C,WAAW,CAA0B;IACrC,IAAI,CAAe;IACnB,gBAAgB,CAAa;IACtC,QAAQ,GAA6B,SAAS,CAAC;IAC/C,OAAO,CAA0B;IACjC,QAAQ,GAAqB,EAAE,CAAC;IAChC,SAAS,GAA6B,IAAI,CAAC;IAC3C,QAAQ,GAAuB,IAAI,CAAC;IACpC,QAAQ,GAAG,KAAK,CAAC;IACjB,OAAO,GAAG,KAAK,CAAC;IAChB,0FAA0F;IAC1F,oBAAoB,GAAG,KAAK,CAAC;IAC7B,UAAU,GAAG,KAAK,CAAC;IAEnB,YAAY,OAA+B;QACzC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC;QAC9B,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;QAChC,IAAI,CAAC,WAAW;YACd,OAAO,CAAC,UAAU,KAAK,SAAS,CAAC,CAAC,CAAC,uBAAuB,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,CAAC;QACpF,IAAI,CAAC,IAAI,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;QAC9C,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACpB,IAAI,CAAC,SAAS,GAAG,IAAI,qBAAqB,EAAE,CAAC;YAC7C,IAAI,CAAC,OAAO,GAAG,UAAU,CAAC;YAC1B,IAAI,CAAC,gBAAgB,GAAG,GAAG,EAAE,CAAC,SAAS,CAAC;YACxC,OAAO;QACT,CAAC;QACD,IAAI,CAAC,SAAS,GAAG,CAAC,OAAO,CAAC,cAAc,IAAI,qBAAqB,CAAC,CAAC,MAAM,CAAC,CAAC;QAC3E,IAAI,CAAC,OAAO,GAAG,iBAAiB,CAAC;QACjC,IAAI,CAAC,SAAS,GAAG,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACjD,IAAI,CAAC,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,KAAK,EAAE,EAAE;YACvD,IAAI,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;QAC3B,CAAC,CAAC,CAAC;QACH,MAAM,UAAU,GAAG,MAAM,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;QACvE,IAAI,UAAU,KAAK,KAAK;YAAE,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACnD,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;YACxB,IAAI,CAAC,QAAQ,GAAG,SAAS,CAAC;YAC1B,6FAA6F;YAC7F,IAAI,CAAC,SAAS,EAAE,CAAC;QACnB,CAAC;IACH,CAAC;IAED,IAAI,OAAO;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC;IACvB,CAAC;IAED,IAAI,MAAM;QACR,OAAO,IAAI,CAAC,OAAO,CAAC;IACtB,CAAC;IAED,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,QAAQ,EAAE,EAAE,IAAI,IAAI,CAAC;IACnC,CAAC;IAED,SAAS;QACP,OAAO,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO,CAAC;IAChE,CAAC;IAED,UAAU,CAAC,OAAgB;QACzB,MAAM,KAAK,GAAY,OAAO,CAAC;QAC/B,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,SAAS;YAAE,OAAO;QACnF,MAAM,IAAI,GAA6B,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,QAAQ,CAAC;QACpE,qFAAqF;QACrF,IAAI,IAAI,KAAK,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,KAAK,aAAa;YAAE,OAAO;QACrE,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,IAAI,CAAC,OAAO,CAAC,eAAe;YAAE,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC5D,IAAI,KAAK,EAAE,CAAC;YACV,IAAI,CAAC,SAAS,EAAE,CAAC;YACjB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;QACnB,IAAI,CAAC,OAAO,GAAG,iBAAiB,CAAC;QACjC,IAAI,IAAI,CAAC,QAAQ;YAAE,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACpD,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE;YACf,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QAC1B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CAAC,KAAa,EAAE,UAAwC;QAC3D,IAAI,CAAC,gBAAgB,CAAC,KAAK,CAAC,EAAE,CAAC;YAC7B,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,IAAI,CAAC,CAAC;YACpC,OAAO;QACT,CAAC;QACD,IAAI,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC;YACtC,OAAO;QACT,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC;IACpC,CAAC;IAED,YAAY,CAAC,UAAwC;QACnD,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI;YAAE,OAAO;QACtD,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,6DAA6D;QAC7D,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC;QACnE,MAAM,OAAO,GAAgB;YAC3B,EAAE,EAAE,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC;YAC3B,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,CAAC;YACZ,YAAY,EAAE,UAAU,KAAK,IAAI,IAAI,UAAU,CAAC,SAAS,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI;YACxE,qBAAqB,EACnB,UAAU,EAAE,SAAS,CAAC,GAAG,EAAE;gBACzB,IAAI,CAAC,mBAAmB,CAAC,OAAO,CAAC,CAAC;YACpC,CAAC,CAAC,IAAI,IAAI;YACZ,MAAM,EAAE,IAAI,GAAG,EAAE;SAClB,CAAC;QACF,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,cAAc,EAAE,UAAU,EAAE,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED,UAAU,CAAC,UAAwC;QACjD,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC9B,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO;QAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,MAAM,SAAS,GACb,OAAO,CAAC,SAAS,GAAG,CAAC,OAAO,CAAC,YAAY,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,GAAG,OAAO,CAAC,YAAY,CAAC,CAAC;QACvF,IAAI,CAAC,KAAK,CACR,gBAAgB,CAAC,YAAY,EAC7B,UAAU,EACV;YACE,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,GAAG,OAAO,CAAC,SAAS,CAAC,CAAC;YAC7D,mBAAmB,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;SACxD,EACD,gBAAgB,CACjB,CAAC;QACF,IAAI,CAAC,aAAa,EAAE,CAAC;IACvB,CAAC;IAED,YAAY,CAAC,KAAsB,EAAE,UAAwC;QAC3E,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;QACrE,IAAI,UAAU,KAAK,IAAI;YAAE,OAAO;QAChC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC;QACrC,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC;YAC1B,IAAI,MAAM,CAAC,IAAI,IAAI,kBAAkB,EAAE,CAAC;gBACtC,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC;gBACpC,IAAI,MAAM,CAAC,IAAI,KAAK,IAAI;oBAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACxD,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,UAAU,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QACtC,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,YAAY,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,EAAE,UAAU,CAAC,CAAC;IAC3F,CAAC;IAED,cAAc,CAAC,KAAsB,EAAE,UAAwC;QAC7E,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,cAAc,EAAE,KAAK,CAAC,CAAC;QACvE,IAAI,UAAU,KAAK,IAAI;YAAE,OAAO;QAChC,IAAI,CAAC,KAAK,CACR,gBAAgB,CAAC,cAAc,EAC/B,UAAU,EACV,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,EACzD,UAAU,CACX,CAAC;IACJ,CAAC;IAED,WAAW,CACT,KAAsB,EACtB,MAAe,EACf,UAAwC;QAExC,MAAM,UAAU,GAAG,IAAI,CAAC,MAAM,CAAC,gBAAgB,CAAC,WAAW,EAAE,KAAK,CAAC,CAAC;QACpE,IAAI,UAAU,KAAK,IAAI;YAAE,OAAO;QAChC,MAAM,KAAK,GAAwB,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,IAAI,CAAC,cAAc,CAAC,UAAU,CAAC,EAAE,CAAC;QAC7F,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;YACzB,MAAM,KAAK,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;YACtC,IAAI,KAAK,KAAK,IAAI;gBAAE,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,gBAAgB,CAAC,WAAW,EAAE,QAAQ,CAAC,CAAC;;gBACxF,KAAK,CAAC,MAAM,GAAG,KAAK,CAAC;QAC5B,CAAC;QACD,IAAI,CAAC,KAAK,CAAC,gBAAgB,CAAC,WAAW,EAAE,UAAU,EAAE,KAAK,EAAE,iBAAiB,CAAC,CAAC;IACjF,CAAC;IAED;;;OAGG;IACH,OAAO;QACL,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAC5B,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC;QACvB,IAAI,CAAC,gBAAgB,EAAE,CAAC;QACxB,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;QACnB,IAAI,CAAC,OAAO,GAAG,UAAU,CAAC;QAC1B,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE;YACf,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;QAC1B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,SAAS;QACP,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;YAAE,OAAO;QAC3D,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YAClB,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;YACzB,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;YACvB,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE;gBACf,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;YACzB,CAAC,CAAC,CAAC;YACH,IAAI,CAAC,cAAc,EAAE,CAAC;YACtB,OAAO;QACT,CAAC;QACD,IAAI,CAAC,OAAO,GAAG,SAAS,CAAC;QACzB,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,IAAI,OAAsB,CAAC;QAC3B,IAAI,CAAC;YACH,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;QAClC,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,GAAG,OAAO,CAAC,MAAM,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACtF,CAAC;QACD,OAAO,CAAC,IAAI,CACV,GAAG,EAAE;YACH,IAAI,CAAC,gBAAgB,EAAE,CAAC;QAC1B,CAAC,EACD,GAAG,EAAE;YACH,IAAI,CAAC,iBAAiB,EAAE,CAAC;QAC3B,CAAC,CACF,CAAC;IACJ,CAAC;IAED,gBAAgB;QACd,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAC5B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAC9B,kFAAkF;YAClF,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;YAClC,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE;gBACf,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,CAAC;YAC1B,CAAC,CAAC,CAAC;QACL,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;YAAE,OAAO;QACxC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE;YACf,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QACzB,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,cAAc,EAAE,CAAC;QACtB,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC9B,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;QACnB,KAAK,MAAM,KAAK,IAAI,OAAO;YAAE,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IACpD,CAAC;IAED,iBAAiB;QACf,IAAI,CAAC,QAAQ,GAAG,KAAK,CAAC;QACtB,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;QAClC,IAAI,IAAI,CAAC,UAAU;YAAE,OAAO;QAC5B,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;QACnB,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;YAAE,OAAO;QACxC,IAAI,CAAC,OAAO,GAAG,aAAa,CAAC;QAC7B,IAAI,CAAC,OAAO,CAAC,sBAAsB,EAAE,IAAI,CAAC,CAAC;IAC7C,CAAC;IAED,YAAY,CAAC,KAAwB;QACnC,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QACpC,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO;QAC9B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC;QAC1B,IAAI,CAAC,cAAc,EAAE,CAAC;IACxB,CAAC;IAED,cAAc;QACZ,MAAM,QAAQ,GAAG,IAAI,CAAC,SAAS,CAAC;QAChC,IAAI,QAAQ,KAAK,IAAI,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;YAAE,OAAO;QACzF,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE;YACf,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;QACpC,CAAC,CAAC,CAAC;IACL,CAAC;IAED,KAAK,CACH,IAAY,EACZ,UAAmD,EACnD,KAA0B,EAC1B,gBAAqC,iBAAiB;QAEtD,IAAI,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI;YAAE,OAAO;QACrD,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,GAAG,aAAa,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAChE,IAAI,SAA8B,CAAC;QACnC,IAAI,CAAC;YACH,SAAS,GAAG,kBAAkB,CAAC,IAAI,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;QACzE,CAAC;QAAC,MAAM,CAAC;YACP,6EAA6E;YAC7E,IAAI,CAAC,OAAO,CAAC,kBAAkB,EAAE,IAAI,CAAC,CAAC;YACvC,OAAO;QACT,CAAC;QACD,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;YAAE,OAAO;QACxC,IAAI,IAAI,CAAC,OAAO,KAAK,SAAS,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO;YAAE,OAAO;QACnE,MAAM,KAAK,GAAmB;YAC5B,IAAI;YACJ,UAAU,EAAE,EAAE,GAAG,SAAS,EAAE,GAAG,KAAK,EAAE,GAAG,IAAI,CAAC,SAAS,EAAE,EAAE;YAC3D,SAAS,EAAE,IAAI,CAAC,IAAI,EAAE;SACvB,CAAC;QACF,IAAI,IAAI,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;YAC7B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;QACvB,CAAC;aAAM,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,kBAAkB,EAAE,CAAC;YACrD,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,GAAG,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC;QAClD,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,IAAI,CAAC,CAAC;QACnC,CAAC;IACH,CAAC;IAED,QAAQ,CAAC,KAAqB;QAC5B,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE;YACf,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;QAChC,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAED,SAAS;QACP,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC;QAC5B,MAAM,QAAQ,GAAwB;YACpC,OAAO,EAAE,MAAM,EAAE,MAAM,IAAI,IAAI;YAC/B,WAAW,EAAE,WAAW;YACxB,QAAQ,EAAE,KAAK;SAChB,CAAC;QACF,IAAI,MAAM,EAAE,WAAW,IAAI,IAAI;YAAE,QAAQ,CAAC,YAAY,GAAG,MAAM,CAAC,WAAW,CAAC;QAC5E,IAAI,IAAI,CAAC,QAAQ,KAAK,IAAI;YAAE,QAAQ,CAAC,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;QACnE,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,CAAC,KAAa,EAAE,KAAc;QAClC,MAAM,UAAU,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;QACzC,IAAI,UAAU,KAAK,IAAI;YAAE,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,KAAK,CAAC,CAAC;QAC9D,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,cAAc,CAAC,KAAa;QAC1B,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC;QACrC,MAAM,SAAS,GAAG,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;QACrC,IAAI,MAAM,KAAK,SAAS,IAAI,SAAS,KAAK,SAAS;YAAE,OAAO,EAAE,CAAC;QAC/D,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACrB,OAAO,EAAE,WAAW,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,SAAS,CAAC,CAAC,EAAE,CAAC;IAC3E,CAAC;IAED,mBAAmB,CAAC,OAAoB;QACtC,IAAI,IAAI,CAAC,QAAQ,KAAK,OAAO,IAAI,IAAI,CAAC,WAAW,KAAK,IAAI;YAAE,OAAO;QACnE,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QACxB,IAAI,IAAI,CAAC,WAAW,CAAC,SAAS,EAAE,EAAE,CAAC;YACjC,OAAO,CAAC,YAAY,KAAK,GAAG,CAAC;QAC/B,CAAC;aAAM,IAAI,OAAO,CAAC,YAAY,KAAK,IAAI,EAAE,CAAC;YACzC,OAAO,CAAC,SAAS,IAAI,GAAG,GAAG,OAAO,CAAC,YAAY,CAAC;YAChD,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;QAC9B,CAAC;IACH,CAAC;IAED,aAAa;QACX,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC;QAC9B,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC;QACrB,OAAO,EAAE,qBAAqB,EAAE,EAAE,CAAC;IACrC,CAAC;IAEQ,SAAS,GAAkB,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,GAAG,IAAI,EAAE,EAAE;QACnE,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;IACtC,CAAC,CAAC;IAEF,OAAO,CACL,IAAoC,EACpC,KAAoB,EACpB,WAA0B,IAAI;QAE9B,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,EAAE,OAAO,CAAC;QACtC,IAAI,CAAC,OAAO;YAAE,OAAO;QACrB,IAAI,CAAC;YACH,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;QACpD,CAAC;QAAC,MAAM,CAAC;YACP,0CAA0C;QAC5C,CAAC;IACH,CAAC;IAED,MAAM,CAAC,MAAkB,EAAE,QAAuB,IAAI;QACpD,IAAI,CAAC;YACH,MAAM,EAAE,CAAC;QACX,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,CAAC,OAAO,CAAC,gBAAgB,EAAE,KAAK,CAAC,CAAC;QACxC,CAAC;IACH,CAAC;IAED,WAAW;QACT,OAAO,8BAA8B,IAAI,CAAC,OAAO,EAAE,MAAM,IAAI,cAAc,EAAE,CAAC;IAChF,CAAC;IAED,QAAQ;QACN,IAAI,CAAC;YACH,IAAI,IAAI,CAAC,QAAQ,KAAK,SAAS;gBAAE,OAAO,IAAI,CAAC,QAAQ,CAAC,iBAAiB,IAAI,IAAI,CAAC;YAChF,OAAO,OAAO,UAAU,CAAC,YAAY,KAAK,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC;QACzF,CAAC;QAAC,MAAM,CAAC;YACP,wDAAwD;YACxD,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,YAAY;QACV,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,EAAE,EAAE,OAAO,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,IAAI,IAAI,CAAC;YACjE,IAAI,GAAG,KAAK,IAAI;gBAAE,OAAO,IAAI,CAAC;YAC9B,MAAM,KAAK,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACvC,IACE,OAAO,KAAK,KAAK,QAAQ;gBACzB,KAAK,KAAK,IAAI;gBACb,KAA+B,CAAC,OAAO,KAAK,eAAe;gBAC5D,OAAQ,KAA+B,CAAC,OAAO,KAAK,SAAS,EAC7D,CAAC;gBACD,OAAQ,KAA8B,CAAC,OAAO,CAAC;YACjD,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAED,aAAa,CAAC,OAAgB;QAC5B,IAAI,CAAC;YACH,IAAI,CAAC,QAAQ,EAAE,EAAE,OAAO,CACtB,IAAI,CAAC,WAAW,EAAE,EAClB,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,eAAe,EAAE,OAAO,EAAE,EAAE,EAAE,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC,CACvE,CAAC;QACJ,CAAC;QAAC,MAAM,CAAC;YACP,yDAAyD;QAC3D,CAAC;IACH,CAAC;CACF;AAED,SAAS,qBAAqB,CAAC,MAA+B;IAC5D,OAAO,IAAI,wBAAwB,CAAC;QAClC,KAAK,EAAE,MAAM,CAAC,QAAQ,CAAC,KAAK;QAC5B,IAAI,EAAE,MAAM,CAAC,QAAQ,CAAC,IAAI;QAC1B,MAAM,EAAE,MAAM,CAAC,MAAM;KACtB,CAAC,CAAC;AACL,CAAC;AAED,iFAAiF;AACjF,SAAS,WAAW,CAAC,KAAwB;IAC3C,IAAI,KAAK,CAAC,MAAM,KAAK,eAAe;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;IAC1F,IAAI,KAAK,CAAC,MAAM,KAAK,WAAW,EAAE,CAAC;QACjC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;IAC1E,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,SAAS,QAAQ,CAAC,OAA2C;IAC3D,MAAM,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,QAAQ,CAAuB,CAAC;IAC7E,MAAM,MAAM,GAAG,OAAO,EAAE,MAAM,IAAI,YAAY,CAAC;IAC/C,IAAI,OAAO,MAAM,EAAE,UAAU,KAAK,UAAU;QAAE,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC;IACzE,MAAM,KAAK,GAAG,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC;IACjC,IAAI,OAAO,MAAM,EAAE,eAAe,KAAK,UAAU,EAAE,CAAC;QAClD,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;IAChC,CAAC;SAAM,CAAC;QACN,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;YACrD,KAAK,CAAC,KAAK,CAAC,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;IACD,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAC/E,CAAC;AAED,8EAA8E;AAC9E,SAAS,uBAAuB;IAC9B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,UAAU,EAAE,UAAU,CAAyB,CAAC;IAC7E,IAAI,QAAQ,KAAK,SAAS,IAAI,OAAO,QAAQ,CAAC,gBAAgB,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IAC3F,OAAO;QACL,SAAS,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,eAAe,KAAK,QAAQ;QACtD,SAAS,EAAE,CAAC,QAAQ,EAAE,EAAE;YACtB,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAC;YACxD,OAAO,GAAG,EAAE;gBACV,QAAQ,CAAC,mBAAmB,CAAC,kBAAkB,EAAE,QAAQ,CAAC,CAAC;YAC7D,CAAC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type { ResolvedAnalyticsConfig } from '../../core/config.js';\nimport type {\n PixelumeAnalyticsIssue,\n PixelumeBrowserAdapter,\n PixelumeSessionStorage,\n} from '../../core/types.js';\nimport { SDK_VERSION } from '../../version.js';\nimport type { PixelumeAuthListener, PixelumeAuthState } from '../auth/AuthModule.js';\nimport {\n NoopAnalyticsProvider,\n type AnalyticsEvent,\n type AnalyticsIdentity,\n type AnalyticsProvider,\n} from './AnalyticsProvider.js';\nimport {\n ANALYTICS_EVENTS,\n isCatalogEvent,\n isValidEventName,\n normalizeLevel,\n normalizeReason,\n sanitizeProperties,\n type AnalyticsProperties,\n type IssueReporter,\n} from './events.js';\nimport { PostHogAnalyticsProvider } from './PostHogAnalyticsProvider.js';\n\n/** Flat values accepted as event properties. Other values are dropped (see `onIssue`). */\nexport type PixelumeAnalyticsProperties = Readonly<\n Record<string, string | number | boolean | null | undefined>\n>;\n\n/**\n * - `unknown`: the player has not decided (or the choice is not remembered). Nothing is sent.\n * - `granted`: events are sent.\n * - `denied`: nothing is sent and the analytics identifiers were removed from the browser.\n */\nexport type PixelumeAnalyticsConsent = 'unknown' | 'granted' | 'denied';\n\n/**\n * - `disabled`: not configured (or the client was destroyed): every call is a no-op.\n * - `waiting_consent`: configured, consent not granted. Events are discarded, not queued.\n * - `loading`: consent granted, provider loading; events wait in a small in-memory queue.\n * - `ready`: events are being delivered.\n * - `unavailable`: the provider could not load here (no browser, blocked script/network). Events\n * are discarded; a later `setConsent(true)` tries again.\n */\nexport type PixelumeAnalyticsStatus =\n 'disabled' | 'waiting_consent' | 'loading' | 'ready' | 'unavailable';\n\n/**\n * Gameplay analytics, decoupled from the provider (PostHog today). Every method is synchronous,\n * never throws and never waits for the network: a failing provider cannot stop the game.\n *\n * A **game session** is one continuous play period defined by the game (e.g. from \"Play\" until the\n * player returns to the menu, or one match); it is unrelated to the Pixelume login session.\n */\nexport interface PixelumeAnalyticsApi {\n readonly consent: PixelumeAnalyticsConsent;\n readonly status: PixelumeAnalyticsStatus;\n /** Id of the current game session (`session_id` of every event), or `null` outside one. */\n readonly sessionId: string | null;\n /** `true` when events are (or will shortly be) delivered: configured, consented, available. */\n isEnabled(): boolean;\n /**\n * Records the player's explicit decision. `false` discards pending events, stops delivery and\n * removes the analytics identifiers from the browser; `true` after `false` starts a new\n * anonymous identity. Repeating the current value does nothing.\n */\n setConsent(granted: boolean): void;\n /** Custom event: snake_case name (1–64 chars) not in the built-in catalog. */\n track(event: string, properties?: PixelumeAnalyticsProperties): void;\n /** Starts a game session (`game_session_started`). Does nothing while one is active. */\n startSession(properties?: PixelumeAnalyticsProperties): void;\n /**\n * Ends the current game session (`game_session_ended` with `duration_ms` and\n * `visible_duration_ms`). Does nothing without an active session. Closing the tab does **not**\n * end the session: only the game knows when play really ended.\n */\n endSession(properties?: PixelumeAnalyticsProperties): void;\n levelStarted(level: string | number, properties?: PixelumeAnalyticsProperties): void;\n /** `level_completed`; carries `duration_ms` when the matching `levelStarted` was seen. */\n levelCompleted(level: string | number, properties?: PixelumeAnalyticsProperties): void;\n /** `level_failed` with an optional short `reason` (e.g. `'out_of_moves'`). */\n levelFailed(\n level: string | number,\n reason?: string,\n properties?: PixelumeAnalyticsProperties,\n ): void;\n}\n\n/** Subset of `pixelume.auth` used to follow the player's identity. */\nexport interface AnalyticsAuthSource {\n readonly state: PixelumeAuthState;\n subscribe(listener: PixelumeAuthListener): () => void;\n}\n\n/** Visibility signals; tests inject a double. */\nexport interface VisibilitySource {\n isVisible(): boolean;\n subscribe(onChange: () => void): () => void;\n}\n\nexport interface AnalyticsModuleOptions {\n readonly config: ResolvedAnalyticsConfig | null;\n readonly auth: AnalyticsAuthSource;\n readonly browser?: PixelumeBrowserAdapter | undefined;\n /** Test seams. */\n readonly createProvider?: (config: ResolvedAnalyticsConfig) => AnalyticsProvider;\n readonly visibility?: VisibilitySource | null;\n readonly now?: () => number;\n}\n\n/** Events kept while the provider loads after consent; older ones are kept, newer dropped. */\nexport const MAX_PENDING_EVENTS = 100;\nconst MAX_TRACKED_LEVELS = 50;\nconst CONSENT_VERSION = 1;\n/** Anonymous reasons meaning \"the account is no longer the one playing\". */\nconst SEPARATING_REASONS: ReadonlySet<string> = new Set(['logged_out', 'expired', 'session_ended']);\n/** Keys set by the SDK for an event, protected even when the SDK does not set them this time. */\nconst NO_PROTECTED_KEYS: ReadonlySet<string> = new Set();\nconst SESSION_END_KEYS: ReadonlySet<string> = new Set(['duration_ms', 'visible_duration_ms']);\nconst LEVEL_KEYS: ReadonlySet<string> = new Set(['level', 'duration_ms']);\nconst LEVEL_FAILED_KEYS: ReadonlySet<string> = new Set(['level', 'duration_ms', 'reason']);\n\ninterface GameSession {\n readonly id: string;\n readonly startedAt: number;\n visibleMs: number;\n visibleSince: number | null;\n readonly unsubscribeVisibility: (() => void) | null;\n readonly levels: Map<string, number>;\n}\n\nexport class AnalyticsModule implements PixelumeAnalyticsApi {\n readonly #config: ResolvedAnalyticsConfig | null;\n readonly #provider: AnalyticsProvider;\n readonly #browser: PixelumeBrowserAdapter | undefined;\n readonly #visibility: VisibilitySource | null;\n readonly #now: () => number;\n readonly #unsubscribeAuth: () => void;\n #consent: PixelumeAnalyticsConsent = 'unknown';\n #status: PixelumeAnalyticsStatus;\n #pending: AnalyticsEvent[] = [];\n #identity: AnalyticsIdentity | null = null;\n #session: GameSession | null = null;\n #loading = false;\n #loaded = false;\n /** Consent was revoked while the provider was loading: wipe it as soon as it is ready. */\n #revokedWhileLoading = false;\n #destroyed = false;\n\n constructor(options: AnalyticsModuleOptions) {\n this.#config = options.config;\n this.#browser = options.browser;\n this.#visibility =\n options.visibility === undefined ? browserVisibilitySource() : options.visibility;\n this.#now = options.now ?? (() => Date.now());\n const config = this.#config;\n if (config === null) {\n this.#provider = new NoopAnalyticsProvider();\n this.#status = 'disabled';\n this.#unsubscribeAuth = () => undefined;\n return;\n }\n this.#provider = (options.createProvider ?? createDefaultProvider)(config);\n this.#status = 'waiting_consent';\n this.#identity = identityFor(options.auth.state);\n this.#unsubscribeAuth = options.auth.subscribe((state) => {\n this.#onAuthState(state);\n });\n const remembered = config.rememberConsent ? this.#readConsent() : null;\n if (remembered === false) this.#consent = 'denied';\n if (remembered === true) {\n this.#consent = 'granted';\n // Asynchronous: constructing the client never waits for, nor fails because of, the provider.\n this.#activate();\n }\n }\n\n get consent(): PixelumeAnalyticsConsent {\n return this.#consent;\n }\n\n get status(): PixelumeAnalyticsStatus {\n return this.#status;\n }\n\n get sessionId(): string | null {\n return this.#session?.id ?? null;\n }\n\n isEnabled(): boolean {\n return this.#status === 'loading' || this.#status === 'ready';\n }\n\n setConsent(granted: boolean): void {\n const value: unknown = granted;\n if (this.#destroyed || this.#config === null || typeof value !== 'boolean') return;\n const next: PixelumeAnalyticsConsent = value ? 'granted' : 'denied';\n // Repeating the decision is a no-op, except to retry a provider that could not load.\n if (next === this.#consent && this.#status !== 'unavailable') return;\n this.#consent = next;\n if (this.#config.rememberConsent) this.#writeConsent(value);\n if (value) {\n this.#activate();\n return;\n }\n this.#pending = [];\n this.#status = 'waiting_consent';\n if (this.#loading) this.#revokedWhileLoading = true;\n this.#guard(() => {\n this.#provider.optOut();\n });\n }\n\n track(event: string, properties?: PixelumeAnalyticsProperties): void {\n if (!isValidEventName(event)) {\n this.#report('invalid_event', null);\n return;\n }\n if (isCatalogEvent(event)) {\n this.#report('reserved_event', event);\n return;\n }\n this.#emit(event, properties, {});\n }\n\n startSession(properties?: PixelumeAnalyticsProperties): void {\n if (this.#destroyed || this.#session !== null) return;\n const now = this.#now();\n // A no-op client never touches the page, not even to listen.\n const visibility = this.#config === null ? null : this.#visibility;\n const session: GameSession = {\n id: randomId(this.#browser),\n startedAt: now,\n visibleMs: 0,\n visibleSince: visibility === null || visibility.isVisible() ? now : null,\n unsubscribeVisibility:\n visibility?.subscribe(() => {\n this.#onVisibilityChange(session);\n }) ?? null,\n levels: new Map(),\n };\n this.#session = session;\n this.#emit(ANALYTICS_EVENTS.sessionStarted, properties, {});\n }\n\n endSession(properties?: PixelumeAnalyticsProperties): void {\n const session = this.#session;\n if (session === null) return;\n const now = this.#now();\n const visibleMs =\n session.visibleMs + (session.visibleSince === null ? 0 : now - session.visibleSince);\n this.#emit(\n ANALYTICS_EVENTS.sessionEnded,\n properties,\n {\n duration_ms: Math.max(0, Math.round(now - session.startedAt)),\n visible_duration_ms: Math.max(0, Math.round(visibleMs)),\n },\n SESSION_END_KEYS,\n );\n this.#closeSession();\n }\n\n levelStarted(level: string | number, properties?: PixelumeAnalyticsProperties): void {\n const normalized = this.#level(ANALYTICS_EVENTS.levelStarted, level);\n if (normalized === null) return;\n const levels = this.#session?.levels;\n if (levels !== undefined) {\n levels.delete(normalized);\n if (levels.size >= MAX_TRACKED_LEVELS) {\n const oldest = levels.keys().next();\n if (oldest.done !== true) levels.delete(oldest.value);\n }\n levels.set(normalized, this.#now());\n }\n this.#emit(ANALYTICS_EVENTS.levelStarted, properties, { level: normalized }, LEVEL_KEYS);\n }\n\n levelCompleted(level: string | number, properties?: PixelumeAnalyticsProperties): void {\n const normalized = this.#level(ANALYTICS_EVENTS.levelCompleted, level);\n if (normalized === null) return;\n this.#emit(\n ANALYTICS_EVENTS.levelCompleted,\n properties,\n { level: normalized, ...this.#levelDuration(normalized) },\n LEVEL_KEYS,\n );\n }\n\n levelFailed(\n level: string | number,\n reason?: string,\n properties?: PixelumeAnalyticsProperties,\n ): void {\n const normalized = this.#level(ANALYTICS_EVENTS.levelFailed, level);\n if (normalized === null) return;\n const extra: AnalyticsProperties = { level: normalized, ...this.#levelDuration(normalized) };\n if (reason !== undefined) {\n const value = normalizeReason(reason);\n if (value === null) this.#report('invalid_property', ANALYTICS_EVENTS.levelFailed, 'reason');\n else extra.reason = value;\n }\n this.#emit(ANALYTICS_EVENTS.levelFailed, properties, extra, LEVEL_FAILED_KEYS);\n }\n\n /**\n * @internal Called by `PixelumeClient.destroy()`: releases listeners and stops delivery. Does not\n * end the game session nor change consent; posthog may still flush what it already accepted.\n */\n dispose(): void {\n if (this.#destroyed) return;\n this.#destroyed = true;\n this.#unsubscribeAuth();\n this.#closeSession();\n this.#pending = [];\n this.#status = 'disabled';\n this.#guard(() => {\n this.#provider.detach();\n });\n }\n\n #activate(): void {\n if (this.#destroyed || this.#consent !== 'granted') return;\n if (this.#loading) {\n this.#status = 'loading';\n return;\n }\n if (this.#loaded) {\n this.#status = 'ready';\n this.#guard(() => {\n this.#provider.optIn();\n });\n this.#applyIdentity();\n return;\n }\n this.#status = 'loading';\n this.#loading = true;\n let loading: Promise<void>;\n try {\n loading = this.#provider.load();\n } catch (error) {\n loading = Promise.reject(error instanceof Error ? error : new Error(String(error)));\n }\n loading.then(\n () => {\n this.#onProviderReady();\n },\n () => {\n this.#onProviderFailed();\n },\n );\n }\n\n #onProviderReady(): void {\n this.#loading = false;\n if (this.#destroyed) return;\n this.#loaded = true;\n if (this.#revokedWhileLoading) {\n // Revoked while loading: wipe whatever the provider set up before using it again.\n this.#revokedWhileLoading = false;\n this.#guard(() => {\n this.#provider.optOut();\n });\n }\n if (this.#consent !== 'granted') return;\n this.#status = 'ready';\n this.#guard(() => {\n this.#provider.optIn();\n });\n this.#applyIdentity();\n const pending = this.#pending;\n this.#pending = [];\n for (const event of pending) this.#deliver(event);\n }\n\n #onProviderFailed(): void {\n this.#loading = false;\n this.#revokedWhileLoading = false;\n if (this.#destroyed) return;\n this.#pending = [];\n if (this.#consent !== 'granted') return;\n this.#status = 'unavailable';\n this.#report('provider_unavailable', null);\n }\n\n #onAuthState(state: PixelumeAuthState): void {\n const identity = identityFor(state);\n if (identity === null) return;\n this.#identity = identity;\n this.#applyIdentity();\n }\n\n #applyIdentity(): void {\n const identity = this.#identity;\n if (identity === null || this.#status !== 'ready' || this.#consent !== 'granted') return;\n this.#guard(() => {\n this.#provider.identify(identity);\n });\n }\n\n #emit(\n name: string,\n properties: PixelumeAnalyticsProperties | undefined,\n extra: AnalyticsProperties,\n protectedKeys: ReadonlySet<string> = NO_PROTECTED_KEYS,\n ): void {\n if (this.#destroyed || this.#config === null) return;\n const keys = new Set([...protectedKeys, ...Object.keys(extra)]);\n let sanitized: AnalyticsProperties;\n try {\n sanitized = sanitizeProperties(name, properties, keys, this.#reporter);\n } catch {\n // Hostile input (throwing getters, proxies): drop the event, never the game.\n this.#report('invalid_property', name);\n return;\n }\n if (this.#consent !== 'granted') return;\n if (this.#status !== 'loading' && this.#status !== 'ready') return;\n const event: AnalyticsEvent = {\n name,\n properties: { ...sanitized, ...extra, ...this.#metadata() },\n timestamp: this.#now(),\n };\n if (this.#status === 'ready') {\n this.#deliver(event);\n } else if (this.#pending.length < MAX_PENDING_EVENTS) {\n this.#pending.push({ ...event, delayed: true });\n } else {\n this.#report('queue_full', name);\n }\n }\n\n #deliver(event: AnalyticsEvent): void {\n this.#guard(() => {\n this.#provider.capture(event);\n }, event.name);\n }\n\n #metadata(): AnalyticsProperties {\n const config = this.#config;\n const metadata: AnalyticsProperties = {\n game_id: config?.gameId ?? null,\n sdk_version: SDK_VERSION,\n platform: 'web',\n };\n if (config?.gameVersion != null) metadata.game_version = config.gameVersion;\n if (this.#session !== null) metadata.session_id = this.#session.id;\n return metadata;\n }\n\n #level(event: string, level: unknown): string | null {\n const normalized = normalizeLevel(level);\n if (normalized === null) this.#report('invalid_level', event);\n return normalized;\n }\n\n #levelDuration(level: string): { duration_ms?: number } {\n const levels = this.#session?.levels;\n const startedAt = levels?.get(level);\n if (levels === undefined || startedAt === undefined) return {};\n levels.delete(level);\n return { duration_ms: Math.max(0, Math.round(this.#now() - startedAt)) };\n }\n\n #onVisibilityChange(session: GameSession): void {\n if (this.#session !== session || this.#visibility === null) return;\n const now = this.#now();\n if (this.#visibility.isVisible()) {\n session.visibleSince ??= now;\n } else if (session.visibleSince !== null) {\n session.visibleMs += now - session.visibleSince;\n session.visibleSince = null;\n }\n }\n\n #closeSession(): void {\n const session = this.#session;\n this.#session = null;\n session?.unsubscribeVisibility?.();\n }\n\n readonly #reporter: IssueReporter = (code, event, property = null) => {\n this.#report(code, event, property);\n };\n\n #report(\n code: PixelumeAnalyticsIssue['code'],\n event: string | null,\n property: string | null = null,\n ): void {\n const onIssue = this.#config?.onIssue;\n if (!onIssue) return;\n try {\n onIssue(Object.freeze({ code, event, property }));\n } catch {\n // Diagnostics must never affect the game.\n }\n }\n\n #guard(action: () => void, event: string | null = null): void {\n try {\n action();\n } catch {\n this.#report('provider_error', event);\n }\n }\n\n #consentKey(): string {\n return `pixelume.analytics.consent.${this.#config?.gameId ?? 'unconfigured'}`;\n }\n\n #storage(): PixelumeSessionStorage | null {\n try {\n if (this.#browser !== undefined) return this.#browser.persistentStorage ?? null;\n return typeof globalThis.localStorage === 'undefined' ? null : globalThis.localStorage;\n } catch {\n // Blocked storage (privacy settings, sandboxed frames).\n return null;\n }\n }\n\n #readConsent(): boolean | null {\n try {\n const raw = this.#storage()?.getItem(this.#consentKey()) ?? null;\n if (raw === null) return null;\n const value: unknown = JSON.parse(raw);\n if (\n typeof value === 'object' &&\n value !== null &&\n (value as { version?: unknown }).version === CONSENT_VERSION &&\n typeof (value as { granted?: unknown }).granted === 'boolean'\n ) {\n return (value as { granted: boolean }).granted;\n }\n return null;\n } catch {\n return null;\n }\n }\n\n #writeConsent(granted: boolean): void {\n try {\n this.#storage()?.setItem(\n this.#consentKey(),\n JSON.stringify({ version: CONSENT_VERSION, granted, at: this.#now() }),\n );\n } catch {\n // Not remembered: the game asks again on the next visit.\n }\n }\n}\n\nfunction createDefaultProvider(config: ResolvedAnalyticsConfig): AnalyticsProvider {\n return new PostHogAnalyticsProvider({\n token: config.provider.token,\n host: config.provider.host,\n gameId: config.gameId,\n });\n}\n\n/** `null` while authentication is still settling: keep the previous identity. */\nfunction identityFor(state: PixelumeAuthState): AnalyticsIdentity | null {\n if (state.status === 'authenticated') return { player: state.player.id, separate: false };\n if (state.status === 'anonymous') {\n return { player: null, separate: SEPARATING_REASONS.has(state.reason) };\n }\n return null;\n}\n\nfunction randomId(browser: PixelumeBrowserAdapter | undefined): string {\n const globalCrypto = Reflect.get(globalThis, 'crypto') as Crypto | undefined;\n const crypto = browser?.crypto ?? globalCrypto;\n if (typeof crypto?.randomUUID === 'function') return crypto.randomUUID();\n const bytes = new Uint8Array(16);\n if (typeof crypto?.getRandomValues === 'function') {\n crypto.getRandomValues(bytes);\n } else {\n for (let index = 0; index < bytes.length; index += 1) {\n bytes[index] = Math.floor(Math.random() * 256);\n }\n }\n return [...bytes].map((byte) => byte.toString(16).padStart(2, '0')).join('');\n}\n\n/** `document.visibilityState`, looked up lazily; `null` outside a browser. */\nfunction browserVisibilitySource(): VisibilitySource | null {\n const document = Reflect.get(globalThis, 'document') as Document | undefined;\n if (document === undefined || typeof document.addEventListener !== 'function') return null;\n return {\n isVisible: () => document.visibilityState !== 'hidden',\n subscribe: (onChange) => {\n document.addEventListener('visibilitychange', onChange);\n return () => {\n document.removeEventListener('visibilitychange', onChange);\n };\n },\n };\n}\n"]}