@grandgular/rive-angular 0.1.1 → 0.2.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.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { viewChild, inject, DestroyRef, PLATFORM_ID, NgZone, input, output, signal, effect, untracked, ChangeDetectionStrategy, Component, Injectable } from '@angular/core';
2
+ import { inject, PLATFORM_ID, Injectable, InjectionToken, viewChild, DestroyRef, NgZone, input, output, signal, effect, untracked, ChangeDetectionStrategy, Component } from '@angular/core';
3
3
  import { isPlatformBrowser } from '@angular/common';
4
4
  import { Fit, Alignment, Layout, Rive, RiveFile, EventType } from '@rive-app/canvas';
5
5
  export { Alignment, EventType, Fit, Layout, Rive, RiveFile, StateMachineInput } from '@rive-app/canvas';
@@ -8,16 +8,104 @@ export { Alignment, EventType, Fit, Layout, Rive, RiveFile, StateMachineInput }
8
8
  * Re-export Rive SDK types for consumer convenience
9
9
  */
10
10
  /**
11
- * Error thrown when Rive animation fails to load
11
+ * Error thrown when Rive animation fails to load.
12
+ * Supports legacy constructor for backward compatibility.
12
13
  */
13
14
  class RiveLoadError extends Error {
15
+ code;
16
+ suggestion;
17
+ docsUrl;
14
18
  originalError;
15
- constructor(message, originalError) {
16
- super(message);
17
- this.originalError = originalError;
19
+ constructor(messageOrOptions, originalError) {
20
+ if (typeof messageOrOptions === 'string') {
21
+ // Legacy constructor: new RiveLoadError(message, originalError)
22
+ super(messageOrOptions);
23
+ this.originalError =
24
+ originalError instanceof Error ? originalError : undefined;
25
+ }
26
+ else {
27
+ // New constructor: new RiveLoadError(options)
28
+ super(messageOrOptions.message);
29
+ this.code = messageOrOptions.code;
30
+ this.suggestion = messageOrOptions.suggestion;
31
+ this.docsUrl = messageOrOptions.docsUrl;
32
+ this.originalError =
33
+ messageOrOptions.cause instanceof Error
34
+ ? messageOrOptions.cause
35
+ : undefined;
36
+ }
18
37
  this.name = 'RiveLoadError';
19
38
  }
20
39
  }
40
+ /**
41
+ * Error thrown when validation fails (e.g. missing artboard/animation/input).
42
+ * These errors are typically non-fatal but indicate a configuration mismatch.
43
+ */
44
+ class RiveValidationError extends Error {
45
+ code;
46
+ availableOptions;
47
+ suggestion;
48
+ constructor(message, code, availableOptions, suggestion) {
49
+ super(message);
50
+ this.code = code;
51
+ this.availableOptions = availableOptions;
52
+ this.suggestion = suggestion;
53
+ this.name = 'RiveValidationError';
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Error codes used throughout the Rive Angular library.
59
+ *
60
+ * Ranges:
61
+ * - RIVE_1xx: Load errors (file not found, network, bad format)
62
+ * - RIVE_2xx: Validation errors (artboard, animation, state machine mismatch)
63
+ * - RIVE_3xx: Configuration/Usage errors (missing source, bad canvas)
64
+ */
65
+ var RiveErrorCode;
66
+ (function (RiveErrorCode) {
67
+ // Load Errors
68
+ RiveErrorCode["FileNotFound"] = "RIVE_101";
69
+ RiveErrorCode["InvalidFormat"] = "RIVE_102";
70
+ RiveErrorCode["NetworkError"] = "RIVE_103";
71
+ // Validation Errors
72
+ RiveErrorCode["ArtboardNotFound"] = "RIVE_201";
73
+ RiveErrorCode["AnimationNotFound"] = "RIVE_202";
74
+ RiveErrorCode["StateMachineNotFound"] = "RIVE_203";
75
+ RiveErrorCode["InputNotFound"] = "RIVE_204";
76
+ // Configuration Errors
77
+ RiveErrorCode["NoSource"] = "RIVE_301";
78
+ RiveErrorCode["InvalidCanvas"] = "RIVE_302";
79
+ })(RiveErrorCode || (RiveErrorCode = {}));
80
+ /**
81
+ * Template messages for each error code.
82
+ * Used by formatErrorMessage to generate user-friendly descriptions.
83
+ */
84
+ const ERROR_MESSAGES = {
85
+ [RiveErrorCode.FileNotFound]: 'File not found: {url}',
86
+ [RiveErrorCode.InvalidFormat]: 'Invalid .riv file format',
87
+ [RiveErrorCode.NetworkError]: 'Network error while loading file',
88
+ [RiveErrorCode.ArtboardNotFound]: 'Artboard "{name}" not found',
89
+ [RiveErrorCode.AnimationNotFound]: 'Animation "{name}" not found',
90
+ [RiveErrorCode.StateMachineNotFound]: 'State machine "{name}" not found',
91
+ [RiveErrorCode.InputNotFound]: 'Input "{name}" not found in "{stateMachine}"',
92
+ [RiveErrorCode.NoSource]: 'No animation source provided',
93
+ [RiveErrorCode.InvalidCanvas]: 'Invalid canvas element',
94
+ };
95
+ /**
96
+ * Formats an error message by replacing placeholders with actual values.
97
+ *
98
+ * @param code - The error code
99
+ * @param params - Record of values to replace in the template (e.g. { name: 'MyAnim' })
100
+ * @returns The formatted error string
101
+ */
102
+ function formatErrorMessage(code, params = {}) {
103
+ let message = ERROR_MESSAGES[code] || 'Unknown Rive error';
104
+ for (const [key, value] of Object.entries(params)) {
105
+ message = message.replace(`{${key}}`, value);
106
+ }
107
+ return message;
108
+ }
21
109
 
22
110
  /**
23
111
  * Fake IntersectionObserver for environments where it's not available (e.g., SSR)
@@ -45,12 +133,21 @@ const MyIntersectionObserver = (typeof globalThis !== 'undefined' && globalThis.
45
133
  * Singleton IntersectionObserver wrapper for observing multiple elements
46
134
  * with individual callbacks. This avoids creating multiple IntersectionObserver
47
135
  * instances which is more efficient.
136
+ *
137
+ * Provided as an Angular service for better testability and DI integration.
48
138
  */
49
139
  class ElementObserver {
50
140
  observer;
51
141
  elementsMap = new Map();
142
+ platformId = inject(PLATFORM_ID);
52
143
  constructor() {
53
- this.observer = new MyIntersectionObserver(this.onObserved);
144
+ // Only create real observer in browser environment
145
+ if (isPlatformBrowser(this.platformId)) {
146
+ this.observer = new MyIntersectionObserver(this.onObserved);
147
+ }
148
+ else {
149
+ this.observer = new FakeIntersectionObserver();
150
+ }
54
151
  }
55
152
  onObserved = (entries) => {
56
153
  entries.forEach((entry) => {
@@ -68,17 +165,238 @@ class ElementObserver {
68
165
  this.observer.unobserve(element);
69
166
  this.elementsMap.delete(element);
70
167
  }
168
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ElementObserver, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
169
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ElementObserver, providedIn: 'root' });
71
170
  }
72
- // Singleton instance
73
- let observerInstance = null;
171
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ElementObserver, decorators: [{
172
+ type: Injectable,
173
+ args: [{
174
+ providedIn: 'root',
175
+ }]
176
+ }], ctorParameters: () => [] });
177
+ // Legacy function for backward compatibility
178
+ // New code should inject ElementObserver directly
179
+ let legacyObserverInstance = null;
74
180
  /**
181
+ * @deprecated Use dependency injection instead: `inject(ElementObserver)`
75
182
  * Get the singleton ElementObserver instance
76
183
  */
77
184
  function getElementObserver() {
78
- if (!observerInstance) {
79
- observerInstance = new ElementObserver();
185
+ if (!legacyObserverInstance) {
186
+ legacyObserverInstance = new ElementObserver();
187
+ }
188
+ return legacyObserverInstance;
189
+ }
190
+
191
+ /**
192
+ * Injection token for global Rive debug configuration.
193
+ * Can be provided via provideRiveDebug().
194
+ */
195
+ const RIVE_DEBUG_CONFIG = new InjectionToken('RIVE_DEBUG_CONFIG');
196
+ /**
197
+ * Provides global configuration for Rive debugging.
198
+ * Use this in your app.config.ts or module providers.
199
+ *
200
+ * @example
201
+ * providers: [
202
+ * provideRiveDebug({ logLevel: 'debug' })
203
+ * ]
204
+ */
205
+ function provideRiveDebug(config) {
206
+ return {
207
+ provide: RIVE_DEBUG_CONFIG,
208
+ useValue: config,
209
+ };
210
+ }
211
+
212
+ /**
213
+ * Internal logger for Rive Angular library.
214
+ * Handles log levels and formatting.
215
+ * Not exported publicly.
216
+ */
217
+ class RiveLogger {
218
+ level;
219
+ constructor(globalConfig, localDebug) {
220
+ this.level = this.resolveLogLevel(globalConfig, localDebug);
221
+ }
222
+ /**
223
+ * Resolve effective log level based on precedence rules:
224
+ * 1. Local debug=true -> 'debug'
225
+ * 2. Local debug=false/undefined -> Use global config
226
+ * 3. No config -> 'error' (default)
227
+ */
228
+ resolveLogLevel(globalConfig, localDebug) {
229
+ if (localDebug === true) {
230
+ return 'debug';
231
+ }
232
+ if (globalConfig?.logLevel) {
233
+ return globalConfig.logLevel;
234
+ }
235
+ return 'error';
236
+ }
237
+ /**
238
+ * Update log level dynamically (e.g. when input changes)
239
+ */
240
+ update(globalConfig, localDebug) {
241
+ this.level = this.resolveLogLevel(globalConfig, localDebug);
242
+ }
243
+ debug(message, ...args) {
244
+ if (this.shouldLog('debug')) {
245
+ console.debug(`[Rive] ${message}`, ...args);
246
+ }
247
+ }
248
+ info(message, ...args) {
249
+ if (this.shouldLog('info')) {
250
+ console.info(`[Rive] ${message}`, ...args);
251
+ }
252
+ }
253
+ warn(message, ...args) {
254
+ if (this.shouldLog('warn')) {
255
+ console.warn(`[Rive] ${message}`, ...args);
256
+ }
257
+ }
258
+ error(message, ...args) {
259
+ if (this.shouldLog('error')) {
260
+ console.error(`[Rive] ${message}`, ...args);
261
+ }
262
+ }
263
+ shouldLog(level) {
264
+ const levels = ['none', 'error', 'warn', 'info', 'debug'];
265
+ const currentIdx = levels.indexOf(this.level);
266
+ const targetIdx = levels.indexOf(level);
267
+ return currentIdx >= targetIdx;
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Validates requested artboard name against available artboards.
273
+ * Returns error if not found.
274
+ */
275
+ function validateArtboard(rive, requestedName) {
276
+ if (!requestedName)
277
+ return null;
278
+ try {
279
+ // Safe check: ensure artboardNames exist on runtime
280
+ // Note: These properties exist at runtime but may not be in type definitions
281
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
282
+ const available = rive.artboardNames;
283
+ if (!available || !available.includes(requestedName)) {
284
+ return new RiveValidationError(formatErrorMessage(RiveErrorCode.ArtboardNotFound, {
285
+ name: requestedName,
286
+ }), RiveErrorCode.ArtboardNotFound, available || [], available && available.length > 0
287
+ ? `Available artboards: ${available.join(', ')}`
288
+ : 'No artboards found in file');
289
+ }
290
+ }
291
+ catch {
292
+ // Graceful fallback if runtime metadata is not accessible
293
+ // Return null silently to avoid breaking validation flow
294
+ }
295
+ return null;
296
+ }
297
+ /**
298
+ * Validates requested animation names against available animations.
299
+ * Returns first error found.
300
+ */
301
+ function validateAnimations(rive, requestedNames) {
302
+ if (!requestedNames)
303
+ return null;
304
+ const names = Array.isArray(requestedNames)
305
+ ? requestedNames
306
+ : [requestedNames];
307
+ try {
308
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
309
+ const available = rive.animationNames;
310
+ for (const name of names) {
311
+ if (!available || !available.includes(name)) {
312
+ return new RiveValidationError(formatErrorMessage(RiveErrorCode.AnimationNotFound, { name }), RiveErrorCode.AnimationNotFound, available || [], available && available.length > 0
313
+ ? `Available animations: ${available.join(', ')}`
314
+ : 'No animations found in file');
315
+ }
316
+ }
317
+ }
318
+ catch {
319
+ // Graceful fallback if runtime metadata is not accessible
320
+ // Return null silently to avoid breaking validation flow
321
+ }
322
+ return null;
323
+ }
324
+ /**
325
+ * Validates requested state machine names against available state machines.
326
+ * Returns first error found.
327
+ */
328
+ function validateStateMachines(rive, requestedNames) {
329
+ if (!requestedNames)
330
+ return null;
331
+ const names = Array.isArray(requestedNames)
332
+ ? requestedNames
333
+ : [requestedNames];
334
+ try {
335
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
336
+ const available = rive.stateMachineNames;
337
+ for (const name of names) {
338
+ if (!available || !available.includes(name)) {
339
+ return new RiveValidationError(formatErrorMessage(RiveErrorCode.StateMachineNotFound, { name }), RiveErrorCode.StateMachineNotFound, available || [], available && available.length > 0
340
+ ? `Available state machines: ${available.join(', ')}`
341
+ : 'No state machines found in file');
342
+ }
343
+ }
344
+ }
345
+ catch {
346
+ // Graceful fallback if runtime metadata is not accessible
347
+ // Return null silently to avoid breaking validation flow
80
348
  }
81
- return observerInstance;
349
+ return null;
350
+ }
351
+ /**
352
+ * Validates if an input exists within a specific state machine.
353
+ */
354
+ function validateInput(rive, stateMachineName, inputName) {
355
+ try {
356
+ const inputs = rive.stateMachineInputs(stateMachineName);
357
+ if (!inputs)
358
+ return null; // Should not happen if SM exists
359
+ const found = inputs.find((i) => i.name === inputName);
360
+ if (!found) {
361
+ const available = inputs.map((i) => i.name);
362
+ return new RiveValidationError(formatErrorMessage(RiveErrorCode.InputNotFound, {
363
+ name: inputName,
364
+ stateMachine: stateMachineName,
365
+ }), RiveErrorCode.InputNotFound, available, available.length > 0
366
+ ? `Available inputs in "${stateMachineName}": ${available.join(', ')}`
367
+ : `No inputs found in state machine "${stateMachineName}"`);
368
+ }
369
+ }
370
+ catch {
371
+ // Graceful fallback if runtime metadata is not accessible
372
+ // Return null silently to avoid breaking validation flow
373
+ }
374
+ return null;
375
+ }
376
+ /**
377
+ * Runs full configuration validation.
378
+ * Logs warnings and returns array of errors.
379
+ */
380
+ function validateConfiguration(rive, config, logger) {
381
+ const errors = [];
382
+ const artboardError = validateArtboard(rive, config.artboard);
383
+ if (artboardError)
384
+ errors.push(artboardError);
385
+ const animError = validateAnimations(rive, config.animations);
386
+ if (animError)
387
+ errors.push(animError);
388
+ const smError = validateStateMachines(rive, config.stateMachines);
389
+ if (smError)
390
+ errors.push(smError);
391
+ if (errors.length > 0) {
392
+ logger.warn(`Validation failed with ${errors.length} errors:`);
393
+ errors.forEach((err) => {
394
+ logger.warn(`- ${err.message}`);
395
+ if (err.suggestion)
396
+ logger.warn(` Suggestion: ${err.suggestion}`);
397
+ });
398
+ }
399
+ return errors;
82
400
  }
83
401
 
84
402
  /**
@@ -110,6 +428,8 @@ class RiveCanvasComponent {
110
428
  #destroyRef = inject(DestroyRef);
111
429
  #platformId = inject(PLATFORM_ID);
112
430
  #ngZone = inject(NgZone);
431
+ #globalDebugConfig = inject(RIVE_DEBUG_CONFIG, { optional: true });
432
+ #elementObserver = inject(ElementObserver);
113
433
  src = input(...(ngDevMode ? [undefined, { debugName: "src" }] : []));
114
434
  buffer = input(...(ngDevMode ? [undefined, { debugName: "buffer" }] : []));
115
435
  /**
@@ -139,6 +459,13 @@ class RiveCanvasComponent {
139
459
  * Default is false for security - events must be handled manually via riveEvent output.
140
460
  */
141
461
  automaticallyHandleEvents = input(false, ...(ngDevMode ? [{ debugName: "automaticallyHandleEvents" }] : []));
462
+ /**
463
+ * Enable debug mode for this specific instance.
464
+ * Overrides global configuration if set.
465
+ * - true: 'debug' level
466
+ * - false/undefined: use global level
467
+ */
468
+ debugMode = input(...(ngDevMode ? [undefined, { debugName: "debugMode" }] : []));
142
469
  // Outputs (Events)
143
470
  loaded = output();
144
471
  loadError = output();
@@ -153,31 +480,50 @@ class RiveCanvasComponent {
153
480
  */
154
481
  riveEvent = output();
155
482
  /**
156
- * Emitted when Rive instance is created and ready.
483
+ * Emitted when Rive instance is fully loaded and ready.
157
484
  * Provides direct access to the Rive instance for advanced use cases.
485
+ * Note: This fires AFTER the animation is loaded, not just instantiated.
158
486
  */
159
487
  riveReady = output();
160
- // Signals for reactive state
161
- isPlaying = signal(false, ...(ngDevMode ? [{ debugName: "isPlaying" }] : []));
162
- isPaused = signal(false, ...(ngDevMode ? [{ debugName: "isPaused" }] : []));
163
- isLoaded = signal(false, ...(ngDevMode ? [{ debugName: "isLoaded" }] : []));
488
+ // Private writable signals
489
+ #isPlaying = signal(false, ...(ngDevMode ? [{ debugName: "#isPlaying" }] : []));
490
+ #isPaused = signal(false, ...(ngDevMode ? [{ debugName: "#isPaused" }] : []));
491
+ #isLoaded = signal(false, ...(ngDevMode ? [{ debugName: "#isLoaded" }] : []));
492
+ #riveInstance = signal(null, ...(ngDevMode ? [{ debugName: "#riveInstance" }] : []));
493
+ // Public readonly signals
494
+ isPlaying = this.#isPlaying.asReadonly();
495
+ isPaused = this.#isPaused.asReadonly();
496
+ isLoaded = this.#isLoaded.asReadonly();
164
497
  /**
165
498
  * Public signal providing access to the Rive instance.
166
499
  * Use this to access advanced Rive SDK features.
167
500
  */
168
- riveInstance = signal(null, ...(ngDevMode ? [{ debugName: "riveInstance" }] : []));
501
+ riveInstance = this.#riveInstance.asReadonly();
169
502
  // Private state
170
503
  #rive = null;
504
+ logger;
171
505
  resizeObserver = null;
172
506
  isInitialized = false;
173
507
  isPausedByIntersectionObserver = false;
174
508
  retestIntersectionTimeoutId = null;
509
+ resizeRafId = null;
510
+ lastWidth = 0;
511
+ lastHeight = 0;
175
512
  constructor() {
176
- // Effect to reload animation when src, buffer, or riveFile changes
513
+ this.logger = new RiveLogger(this.#globalDebugConfig, this.debugMode());
514
+ // Effect to update logger level when debugMode changes
515
+ effect(() => {
516
+ this.logger.update(this.#globalDebugConfig, this.debugMode());
517
+ });
518
+ // Effect to reload animation when src, buffer, riveFile, or configuration changes
177
519
  effect(() => {
178
520
  const src = this.src();
179
521
  const buffer = this.buffer();
180
522
  const riveFile = this.riveFile();
523
+ // Track configuration changes to trigger reload
524
+ this.artboard();
525
+ this.animations();
526
+ this.stateMachines();
181
527
  untracked(() => {
182
528
  if ((src || buffer || riveFile) &&
183
529
  isPlatformBrowser(this.#platformId) &&
@@ -185,6 +531,17 @@ class RiveCanvasComponent {
185
531
  this.loadAnimation();
186
532
  });
187
533
  });
534
+ // Effect to update layout when fit or alignment changes
535
+ effect(() => {
536
+ const fit = this.fit();
537
+ const alignment = this.alignment();
538
+ untracked(() => {
539
+ if (this.#rive && isPlatformBrowser(this.#platformId)) {
540
+ const layoutParams = { fit, alignment };
541
+ this.#rive.layout = new Layout(layoutParams);
542
+ }
543
+ });
544
+ });
188
545
  // Auto cleanup on destroy
189
546
  this.#destroyRef.onDestroy(() => {
190
547
  this.cleanupRive();
@@ -205,16 +562,30 @@ class RiveCanvasComponent {
205
562
  */
206
563
  setupResizeObserver() {
207
564
  const canvas = this.canvas().nativeElement;
208
- const dpr = window.devicePixelRatio || 1;
209
565
  this.resizeObserver = new ResizeObserver((entries) => {
566
+ // Cancel any pending resize frame
567
+ if (this.resizeRafId) {
568
+ cancelAnimationFrame(this.resizeRafId);
569
+ }
210
570
  for (const entry of entries) {
211
571
  const { width, height } = entry.contentRect;
212
- // Set canvas size with device pixel ratio for sharp rendering
213
- canvas.width = width * dpr;
214
- canvas.height = height * dpr;
215
- // Resize Rive instance if it exists
216
- if (this.#rive)
217
- this.#rive.resizeDrawingSurfaceToCanvas();
572
+ // Skip if dimensions haven't changed (prevents unnecessary updates)
573
+ if (width === this.lastWidth && height === this.lastHeight) {
574
+ continue;
575
+ }
576
+ this.lastWidth = width;
577
+ this.lastHeight = height;
578
+ // Defer resize to next animation frame to prevent excessive updates in Safari
579
+ this.resizeRafId = requestAnimationFrame(() => {
580
+ // Read current DPR to support monitor changes and zoom
581
+ const dpr = window.devicePixelRatio || 1;
582
+ // Set canvas size with device pixel ratio for sharp rendering
583
+ canvas.width = width * dpr;
584
+ canvas.height = height * dpr;
585
+ // Resize Rive instance if it exists
586
+ if (this.#rive)
587
+ this.#rive.resizeDrawingSurfaceToCanvas();
588
+ });
218
589
  }
219
590
  });
220
591
  this.resizeObserver.observe(canvas);
@@ -223,6 +594,10 @@ class RiveCanvasComponent {
223
594
  * Disconnect ResizeObserver
224
595
  */
225
596
  disconnectResizeObserver() {
597
+ if (this.resizeRafId) {
598
+ cancelAnimationFrame(this.resizeRafId);
599
+ this.resizeRafId = null;
600
+ }
226
601
  if (this.resizeObserver) {
227
602
  this.resizeObserver.disconnect();
228
603
  this.resizeObserver = null;
@@ -235,7 +610,6 @@ class RiveCanvasComponent {
235
610
  if (!this.shouldUseIntersectionObserver())
236
611
  return;
237
612
  const canvas = this.canvas().nativeElement;
238
- const observer = getElementObserver();
239
613
  const onIntersectionChange = (entry) => {
240
614
  if (entry.isIntersecting) {
241
615
  // Canvas is visible - start rendering
@@ -262,7 +636,7 @@ class RiveCanvasComponent {
262
636
  }
263
637
  }
264
638
  };
265
- observer.registerCallback(canvas, onIntersectionChange);
639
+ this.#elementObserver.registerCallback(canvas, onIntersectionChange);
266
640
  }
267
641
  /**
268
642
  * Retest intersection - workaround for Chrome bug
@@ -294,8 +668,7 @@ class RiveCanvasComponent {
294
668
  }
295
669
  if (this.shouldUseIntersectionObserver()) {
296
670
  const canvas = this.canvas().nativeElement;
297
- const observer = getElementObserver();
298
- observer.removeCallback(canvas);
671
+ this.#elementObserver.removeCallback(canvas);
299
672
  }
300
673
  }
301
674
  /**
@@ -311,16 +684,27 @@ class RiveCanvasComponent {
311
684
  const src = this.src();
312
685
  const buffer = this.buffer();
313
686
  const riveFile = this.riveFile();
314
- if (!src && !buffer && !riveFile)
687
+ if (!src && !buffer && !riveFile) {
688
+ this.logger.warn('No animation source provided (src, buffer, or riveFile)');
689
+ this.#ngZone.run(() => this.loadError.emit(new RiveLoadError({
690
+ message: 'No animation source provided',
691
+ code: RiveErrorCode.NoSource,
692
+ })));
315
693
  return;
694
+ }
695
+ this.logger.info(`Loading animation`, {
696
+ src: src || (buffer ? 'buffer' : 'riveFile'),
697
+ canvasWidth: canvas.width,
698
+ canvasHeight: canvas.height,
699
+ dpr: window.devicePixelRatio,
700
+ });
316
701
  // Build layout configuration
317
702
  const layoutParams = {
318
703
  fit: this.fit(),
319
704
  alignment: this.alignment(),
320
705
  };
321
- // Create Rive instance configuration
322
- // Using Record to allow dynamic property assignment
323
- const config = {
706
+ // Build typed Rive configuration
707
+ const baseConfig = {
324
708
  canvas,
325
709
  autoplay: this.autoplay(),
326
710
  layout: new Layout(layoutParams),
@@ -335,73 +719,114 @@ class RiveCanvasComponent {
335
719
  onStateChange: (event) => this.onStateChange(event),
336
720
  onRiveEvent: (event) => this.onRiveEvent(event),
337
721
  };
338
- // Add src, buffer, or riveFile (priority: riveFile > src > buffer)
339
- if (riveFile) {
340
- config['riveFile'] = riveFile;
341
- }
342
- else if (src) {
343
- config['src'] = src;
344
- }
345
- else if (buffer) {
346
- config['buffer'] = buffer;
347
- }
348
- // Add artboard if specified
349
- const artboard = this.artboard();
350
- if (artboard)
351
- config['artboard'] = artboard;
352
- // Add animations if specified
353
- const animations = this.animations();
354
- if (animations)
355
- config['animations'] = animations;
356
- // Add state machines if specified
357
- const stateMachines = this.stateMachines();
358
- if (stateMachines)
359
- config['stateMachines'] = stateMachines;
360
- // Safe type assertion - config contains all required properties
361
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
722
+ // Add source (priority: riveFile > src > buffer)
723
+ const sourceConfig = riveFile
724
+ ? { riveFile }
725
+ : src
726
+ ? { src }
727
+ : buffer
728
+ ? { buffer }
729
+ : {};
730
+ // Add optional configuration
731
+ const optionalConfig = {
732
+ ...(this.artboard() ? { artboard: this.artboard() } : {}),
733
+ ...(this.animations() ? { animations: this.animations() } : {}),
734
+ ...(this.stateMachines()
735
+ ? { stateMachines: this.stateMachines() }
736
+ : {}),
737
+ };
738
+ // Combine all configurations
739
+ const config = { ...baseConfig, ...sourceConfig, ...optionalConfig };
362
740
  this.#rive = new Rive(config);
363
- // Update public signal and emit riveReady event
741
+ // Update public signal (riveReady will be emitted in onLoad)
364
742
  this.#ngZone.run(() => {
365
- this.riveInstance.set(this.#rive);
366
- this.riveReady.emit(this.#rive);
743
+ this.#riveInstance.set(this.#rive);
367
744
  });
368
745
  }
369
746
  catch (error) {
370
- console.error('Failed to initialize Rive instance:', error);
371
- this.#ngZone.run(() => this.loadError.emit(new RiveLoadError('Failed to load Rive animation', error)));
747
+ this.logger.error('Failed to initialize Rive instance:', error);
748
+ this.#ngZone.run(() => this.loadError.emit(new RiveLoadError({
749
+ message: 'Failed to initialize Rive instance',
750
+ code: RiveErrorCode.InvalidFormat,
751
+ cause: error instanceof Error ? error : undefined,
752
+ })));
372
753
  }
373
754
  });
374
755
  }
375
756
  // Event handlers (run inside Angular zone for change detection)
376
757
  onLoad() {
758
+ // Validate loaded configuration
759
+ if (this.#rive) {
760
+ const validationErrors = validateConfiguration(this.#rive, {
761
+ artboard: this.artboard(),
762
+ animations: this.animations(),
763
+ stateMachines: this.stateMachines(),
764
+ }, this.logger);
765
+ // Emit validation errors via loadError output
766
+ if (validationErrors.length > 0) {
767
+ this.#ngZone.run(() => {
768
+ validationErrors.forEach((err) => this.loadError.emit(err));
769
+ });
770
+ }
771
+ // Log available assets in debug mode
772
+ // Note: These properties exist at runtime but may not be in type definitions
773
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
774
+ const riveWithMetadata = this.#rive;
775
+ this.logger.debug('Animation loaded successfully. Available assets:', {
776
+ artboards: riveWithMetadata.artboardNames,
777
+ animations: riveWithMetadata.animationNames,
778
+ stateMachines: riveWithMetadata.stateMachineNames,
779
+ });
780
+ }
377
781
  this.#ngZone.run(() => {
378
- this.isLoaded.set(true);
782
+ this.#isLoaded.set(true);
379
783
  this.loaded.emit();
784
+ // Emit riveReady after animation is fully loaded
785
+ if (this.#rive) {
786
+ this.riveReady.emit(this.#rive);
787
+ }
380
788
  });
381
789
  }
382
790
  onLoadError(originalError) {
383
791
  this.#ngZone.run(() => {
384
- const error = new RiveLoadError('Failed to load Rive animation', originalError instanceof Error ? originalError : undefined);
385
- console.error('Rive load error:', error);
792
+ // Determine probable cause and code
793
+ let code = RiveErrorCode.NetworkError;
794
+ let message = 'Failed to load Rive animation';
795
+ if (originalError instanceof Error) {
796
+ if (originalError.message.includes('404')) {
797
+ code = RiveErrorCode.FileNotFound;
798
+ message = `File not found: ${this.src()}`;
799
+ }
800
+ else if (originalError.message.includes('format')) {
801
+ code = RiveErrorCode.InvalidFormat;
802
+ message = 'Invalid .riv file format';
803
+ }
804
+ }
805
+ const error = new RiveLoadError({
806
+ message,
807
+ code,
808
+ cause: originalError instanceof Error ? originalError : undefined,
809
+ });
810
+ this.logger.error('Rive load error:', error);
386
811
  this.loadError.emit(error);
387
812
  });
388
813
  }
389
814
  onPlay() {
390
815
  this.#ngZone.run(() => {
391
- this.isPlaying.set(true);
392
- this.isPaused.set(false);
816
+ this.#isPlaying.set(true);
817
+ this.#isPaused.set(false);
393
818
  });
394
819
  }
395
820
  onPause() {
396
821
  this.#ngZone.run(() => {
397
- this.isPlaying.set(false);
398
- this.isPaused.set(true);
822
+ this.#isPlaying.set(false);
823
+ this.#isPaused.set(true);
399
824
  });
400
825
  }
401
826
  onStop() {
402
827
  this.#ngZone.run(() => {
403
- this.isPlaying.set(false);
404
- this.isPaused.set(false);
828
+ this.#isPlaying.set(false);
829
+ this.#isPaused.set(false);
405
830
  });
406
831
  }
407
832
  onStateChange(event) {
@@ -473,6 +898,13 @@ class RiveCanvasComponent {
473
898
  if (!this.#rive)
474
899
  return;
475
900
  this.#ngZone.runOutsideAngular(() => {
901
+ // Validate input existence first
902
+ const error = validateInput(this.#rive, stateMachineName, inputName);
903
+ if (error) {
904
+ this.logger.warn(error.message);
905
+ this.#ngZone.run(() => this.loadError.emit(error));
906
+ return;
907
+ }
476
908
  const inputs = this.#rive.stateMachineInputs(stateMachineName);
477
909
  const input = inputs.find((i) => i.name === inputName);
478
910
  if (input && 'value' in input) {
@@ -487,6 +919,13 @@ class RiveCanvasComponent {
487
919
  if (!this.#rive)
488
920
  return;
489
921
  this.#ngZone.runOutsideAngular(() => {
922
+ // Validate trigger (input) existence first
923
+ const error = validateInput(this.#rive, stateMachineName, triggerName);
924
+ if (error) {
925
+ this.logger.warn(error.message);
926
+ this.#ngZone.run(() => this.loadError.emit(error));
927
+ return;
928
+ }
490
929
  const inputs = this.#rive.stateMachineInputs(stateMachineName);
491
930
  const input = inputs.find((i) => i.name === triggerName);
492
931
  if (input && 'fire' in input && typeof input.fire === 'function') {
@@ -503,18 +942,18 @@ class RiveCanvasComponent {
503
942
  this.#rive.cleanup();
504
943
  }
505
944
  catch (error) {
506
- console.warn('Error during Rive cleanup:', error);
945
+ this.logger.warn('Error during Rive cleanup:', error);
507
946
  }
508
947
  this.#rive = null;
509
948
  }
510
949
  // Reset signals
511
- this.riveInstance.set(null);
512
- this.isLoaded.set(false);
513
- this.isPlaying.set(false);
514
- this.isPaused.set(false);
950
+ this.#riveInstance.set(null);
951
+ this.#isLoaded.set(false);
952
+ this.#isPlaying.set(false);
953
+ this.#isPaused.set(false);
515
954
  }
516
955
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
517
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.4", type: RiveCanvasComponent, isStandalone: true, selector: "rive-canvas", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, buffer: { classPropertyName: "buffer", publicName: "buffer", isSignal: true, isRequired: false, transformFunction: null }, riveFile: { classPropertyName: "riveFile", publicName: "riveFile", isSignal: true, isRequired: false, transformFunction: null }, artboard: { classPropertyName: "artboard", publicName: "artboard", isSignal: true, isRequired: false, transformFunction: null }, animations: { classPropertyName: "animations", publicName: "animations", isSignal: true, isRequired: false, transformFunction: null }, stateMachines: { classPropertyName: "stateMachines", publicName: "stateMachines", isSignal: true, isRequired: false, transformFunction: null }, autoplay: { classPropertyName: "autoplay", publicName: "autoplay", isSignal: true, isRequired: false, transformFunction: null }, fit: { classPropertyName: "fit", publicName: "fit", isSignal: true, isRequired: false, transformFunction: null }, alignment: { classPropertyName: "alignment", publicName: "alignment", isSignal: true, isRequired: false, transformFunction: null }, useOffscreenRenderer: { classPropertyName: "useOffscreenRenderer", publicName: "useOffscreenRenderer", isSignal: true, isRequired: false, transformFunction: null }, shouldUseIntersectionObserver: { classPropertyName: "shouldUseIntersectionObserver", publicName: "shouldUseIntersectionObserver", isSignal: true, isRequired: false, transformFunction: null }, shouldDisableRiveListeners: { classPropertyName: "shouldDisableRiveListeners", publicName: "shouldDisableRiveListeners", isSignal: true, isRequired: false, transformFunction: null }, automaticallyHandleEvents: { classPropertyName: "automaticallyHandleEvents", publicName: "automaticallyHandleEvents", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loaded: "loaded", loadError: "loadError", stateChange: "stateChange", riveEvent: "riveEvent", riveReady: "riveReady" }, viewQueries: [{ propertyName: "canvas", first: true, predicate: ["canvas"], descendants: true, isSignal: true }], ngImport: i0, template: `
956
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "21.1.4", type: RiveCanvasComponent, isStandalone: true, selector: "rive-canvas", inputs: { src: { classPropertyName: "src", publicName: "src", isSignal: true, isRequired: false, transformFunction: null }, buffer: { classPropertyName: "buffer", publicName: "buffer", isSignal: true, isRequired: false, transformFunction: null }, riveFile: { classPropertyName: "riveFile", publicName: "riveFile", isSignal: true, isRequired: false, transformFunction: null }, artboard: { classPropertyName: "artboard", publicName: "artboard", isSignal: true, isRequired: false, transformFunction: null }, animations: { classPropertyName: "animations", publicName: "animations", isSignal: true, isRequired: false, transformFunction: null }, stateMachines: { classPropertyName: "stateMachines", publicName: "stateMachines", isSignal: true, isRequired: false, transformFunction: null }, autoplay: { classPropertyName: "autoplay", publicName: "autoplay", isSignal: true, isRequired: false, transformFunction: null }, fit: { classPropertyName: "fit", publicName: "fit", isSignal: true, isRequired: false, transformFunction: null }, alignment: { classPropertyName: "alignment", publicName: "alignment", isSignal: true, isRequired: false, transformFunction: null }, useOffscreenRenderer: { classPropertyName: "useOffscreenRenderer", publicName: "useOffscreenRenderer", isSignal: true, isRequired: false, transformFunction: null }, shouldUseIntersectionObserver: { classPropertyName: "shouldUseIntersectionObserver", publicName: "shouldUseIntersectionObserver", isSignal: true, isRequired: false, transformFunction: null }, shouldDisableRiveListeners: { classPropertyName: "shouldDisableRiveListeners", publicName: "shouldDisableRiveListeners", isSignal: true, isRequired: false, transformFunction: null }, automaticallyHandleEvents: { classPropertyName: "automaticallyHandleEvents", publicName: "automaticallyHandleEvents", isSignal: true, isRequired: false, transformFunction: null }, debugMode: { classPropertyName: "debugMode", publicName: "debugMode", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { loaded: "loaded", loadError: "loadError", stateChange: "stateChange", riveEvent: "riveEvent", riveReady: "riveReady" }, viewQueries: [{ propertyName: "canvas", first: true, predicate: ["canvas"], descendants: true, isSignal: true }], ngImport: i0, template: `
518
957
  <canvas #canvas [style.width.%]="100" [style.height.%]="100"></canvas>
519
958
  `, isInline: true, styles: [":host{display:block;width:100%;height:100%}canvas{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
520
959
  }
@@ -523,7 +962,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
523
962
  args: [{ selector: 'rive-canvas', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
524
963
  <canvas #canvas [style.width.%]="100" [style.height.%]="100"></canvas>
525
964
  `, styles: [":host{display:block;width:100%;height:100%}canvas{display:block}\n"] }]
526
- }], ctorParameters: () => [], propDecorators: { canvas: [{ type: i0.ViewChild, args: ['canvas', { isSignal: true }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], buffer: [{ type: i0.Input, args: [{ isSignal: true, alias: "buffer", required: false }] }], riveFile: [{ type: i0.Input, args: [{ isSignal: true, alias: "riveFile", required: false }] }], artboard: [{ type: i0.Input, args: [{ isSignal: true, alias: "artboard", required: false }] }], animations: [{ type: i0.Input, args: [{ isSignal: true, alias: "animations", required: false }] }], stateMachines: [{ type: i0.Input, args: [{ isSignal: true, alias: "stateMachines", required: false }] }], autoplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoplay", required: false }] }], fit: [{ type: i0.Input, args: [{ isSignal: true, alias: "fit", required: false }] }], alignment: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignment", required: false }] }], useOffscreenRenderer: [{ type: i0.Input, args: [{ isSignal: true, alias: "useOffscreenRenderer", required: false }] }], shouldUseIntersectionObserver: [{ type: i0.Input, args: [{ isSignal: true, alias: "shouldUseIntersectionObserver", required: false }] }], shouldDisableRiveListeners: [{ type: i0.Input, args: [{ isSignal: true, alias: "shouldDisableRiveListeners", required: false }] }], automaticallyHandleEvents: [{ type: i0.Input, args: [{ isSignal: true, alias: "automaticallyHandleEvents", required: false }] }], loaded: [{ type: i0.Output, args: ["loaded"] }], loadError: [{ type: i0.Output, args: ["loadError"] }], stateChange: [{ type: i0.Output, args: ["stateChange"] }], riveEvent: [{ type: i0.Output, args: ["riveEvent"] }], riveReady: [{ type: i0.Output, args: ["riveReady"] }] } });
965
+ }], ctorParameters: () => [], propDecorators: { canvas: [{ type: i0.ViewChild, args: ['canvas', { isSignal: true }] }], src: [{ type: i0.Input, args: [{ isSignal: true, alias: "src", required: false }] }], buffer: [{ type: i0.Input, args: [{ isSignal: true, alias: "buffer", required: false }] }], riveFile: [{ type: i0.Input, args: [{ isSignal: true, alias: "riveFile", required: false }] }], artboard: [{ type: i0.Input, args: [{ isSignal: true, alias: "artboard", required: false }] }], animations: [{ type: i0.Input, args: [{ isSignal: true, alias: "animations", required: false }] }], stateMachines: [{ type: i0.Input, args: [{ isSignal: true, alias: "stateMachines", required: false }] }], autoplay: [{ type: i0.Input, args: [{ isSignal: true, alias: "autoplay", required: false }] }], fit: [{ type: i0.Input, args: [{ isSignal: true, alias: "fit", required: false }] }], alignment: [{ type: i0.Input, args: [{ isSignal: true, alias: "alignment", required: false }] }], useOffscreenRenderer: [{ type: i0.Input, args: [{ isSignal: true, alias: "useOffscreenRenderer", required: false }] }], shouldUseIntersectionObserver: [{ type: i0.Input, args: [{ isSignal: true, alias: "shouldUseIntersectionObserver", required: false }] }], shouldDisableRiveListeners: [{ type: i0.Input, args: [{ isSignal: true, alias: "shouldDisableRiveListeners", required: false }] }], automaticallyHandleEvents: [{ type: i0.Input, args: [{ isSignal: true, alias: "automaticallyHandleEvents", required: false }] }], debugMode: [{ type: i0.Input, args: [{ isSignal: true, alias: "debugMode", required: false }] }], loaded: [{ type: i0.Output, args: ["loaded"] }], loadError: [{ type: i0.Output, args: ["loadError"] }], stateChange: [{ type: i0.Output, args: ["stateChange"] }], riveEvent: [{ type: i0.Output, args: ["riveEvent"] }], riveReady: [{ type: i0.Output, args: ["riveReady"] }] } });
527
966
 
528
967
  /**
529
968
  * Service for preloading and caching Rive files.
@@ -556,7 +995,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
556
995
  class RiveFileService {
557
996
  cache = new Map();
558
997
  pendingLoads = new Map();
998
+ bufferIdMap = new WeakMap();
559
999
  bufferIdCounter = 0;
1000
+ // Optional debug configuration
1001
+ globalDebugConfig = inject(RIVE_DEBUG_CONFIG, {
1002
+ optional: true,
1003
+ });
560
1004
  /**
561
1005
  * Load a RiveFile from URL or ArrayBuffer.
562
1006
  * Returns a signal with the file state and loading status.
@@ -568,15 +1012,20 @@ class RiveFileService {
568
1012
  */
569
1013
  loadFile(params) {
570
1014
  const cacheKey = this.getCacheKey(params);
1015
+ // Initialize logger for this request
1016
+ const logger = new RiveLogger(this.globalDebugConfig, params.debug);
1017
+ logger.debug(`RiveFileService: Request to load file`, { cacheKey });
571
1018
  // Return cached entry if exists
572
1019
  const cached = this.cache.get(cacheKey);
573
1020
  if (cached) {
574
1021
  cached.refCount++;
1022
+ logger.debug(`RiveFileService: Cache hit for ${cacheKey}`);
575
1023
  return cached.state;
576
1024
  }
577
1025
  // Return pending load if already in progress
578
1026
  const pending = this.pendingLoads.get(cacheKey);
579
1027
  if (pending) {
1028
+ logger.debug(`RiveFileService: Reuse pending load for ${cacheKey}`);
580
1029
  return pending.stateSignal.asReadonly();
581
1030
  }
582
1031
  // Create new loading state
@@ -585,7 +1034,7 @@ class RiveFileService {
585
1034
  status: 'loading',
586
1035
  }, ...(ngDevMode ? [{ debugName: "stateSignal" }] : []));
587
1036
  // Start loading and track as pending
588
- const promise = this.loadRiveFile(params, stateSignal, cacheKey);
1037
+ const promise = this.loadRiveFile(params, stateSignal, cacheKey, logger);
589
1038
  this.pendingLoads.set(cacheKey, { stateSignal, promise });
590
1039
  return stateSignal.asReadonly();
591
1040
  }
@@ -611,9 +1060,18 @@ class RiveFileService {
611
1060
  }
612
1061
  }
613
1062
  /**
614
- * Clear all cached files
1063
+ * Clear all cached files and abort pending loads
615
1064
  */
616
1065
  clearCache() {
1066
+ // Clear pending loads first to prevent them from populating the cache
1067
+ this.pendingLoads.forEach((pending) => {
1068
+ pending.stateSignal.set({
1069
+ riveFile: null,
1070
+ status: 'failed',
1071
+ });
1072
+ });
1073
+ this.pendingLoads.clear();
1074
+ // Clean up cached files
617
1075
  this.cache.forEach((entry) => {
618
1076
  try {
619
1077
  entry.file.cleanup();
@@ -632,67 +1090,73 @@ class RiveFileService {
632
1090
  return `src:${params.src}`;
633
1091
  }
634
1092
  if (params.buffer) {
635
- // For buffers, generate unique ID to avoid collisions
636
- // Store the ID on the buffer object itself
637
- const bufferWithId = params.buffer;
638
- if (!bufferWithId.__riveBufferId) {
639
- bufferWithId.__riveBufferId = ++this.bufferIdCounter;
1093
+ // For buffers, use WeakMap to track unique IDs without mutating the buffer
1094
+ let bufferId = this.bufferIdMap.get(params.buffer);
1095
+ if (bufferId === undefined) {
1096
+ bufferId = ++this.bufferIdCounter;
1097
+ this.bufferIdMap.set(params.buffer, bufferId);
640
1098
  }
641
- return `buffer:${bufferWithId.__riveBufferId}`;
1099
+ return `buffer:${bufferId}`;
642
1100
  }
643
1101
  return 'unknown';
644
1102
  }
645
1103
  /**
646
- * Load RiveFile and update state signal
1104
+ * Load RiveFile and update state signal.
1105
+ * Addresses race condition by setting up listeners BEFORE init.
647
1106
  */
648
- loadRiveFile(params, stateSignal, cacheKey) {
649
- return new Promise((resolve) => {
650
- try {
651
- const file = new RiveFile(params);
652
- file.init();
653
- file.on(EventType.Load, () => {
654
- // Request an instance to increment reference count
655
- // This prevents the file from being destroyed while in use
656
- file.getInstance();
657
- stateSignal.set({
658
- riveFile: file,
659
- status: 'success',
660
- });
661
- // Cache the successfully loaded file
662
- this.cache.set(cacheKey, {
663
- file,
664
- state: stateSignal.asReadonly(),
665
- refCount: 1,
666
- });
667
- // Remove from pending loads
668
- this.pendingLoads.delete(cacheKey);
669
- resolve();
1107
+ async loadRiveFile(params, stateSignal, cacheKey, logger) {
1108
+ // Guard to ensure pending load is cleaned up exactly once
1109
+ let pendingCleanupDone = false;
1110
+ const finalizePendingLoadOnce = () => {
1111
+ if (!pendingCleanupDone) {
1112
+ this.pendingLoads.delete(cacheKey);
1113
+ pendingCleanupDone = true;
1114
+ }
1115
+ };
1116
+ try {
1117
+ // Extract debug parameter - it's not part of RiveFile SDK API
1118
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1119
+ const { debug, ...sdkParams } = params;
1120
+ const file = new RiveFile(sdkParams);
1121
+ // Listeners must be attached BEFORE calling init() to avoid race conditions
1122
+ // where init() completes or fails synchronously/immediately.
1123
+ file.on(EventType.Load, () => {
1124
+ logger.debug(`RiveFileService: File loaded successfully`, { cacheKey });
1125
+ // Request an instance to increment reference count
1126
+ // This prevents the file from being destroyed while in use
1127
+ file.getInstance();
1128
+ stateSignal.set({
1129
+ riveFile: file,
1130
+ status: 'success',
670
1131
  });
671
- file.on(EventType.LoadError, () => {
672
- stateSignal.set({
673
- riveFile: null,
674
- status: 'failed',
675
- });
676
- // Remove from pending loads
677
- this.pendingLoads.delete(cacheKey);
678
- // Resolve (not reject) — error state is communicated via the signal.
679
- // Rejecting would cause an unhandled promise rejection since no
680
- // consumer awaits or catches this promise.
681
- resolve();
1132
+ // Cache the successfully loaded file
1133
+ this.cache.set(cacheKey, {
1134
+ file,
1135
+ state: stateSignal.asReadonly(),
1136
+ refCount: 1,
682
1137
  });
683
- }
684
- catch (error) {
685
- console.error('Failed to load RiveFile:', error);
1138
+ finalizePendingLoadOnce();
1139
+ });
1140
+ file.on(EventType.LoadError, () => {
1141
+ logger.warn(`RiveFileService: Failed to load file`, { cacheKey });
686
1142
  stateSignal.set({
687
1143
  riveFile: null,
688
1144
  status: 'failed',
689
1145
  });
690
- // Remove from pending loads
691
- this.pendingLoads.delete(cacheKey);
692
- // Resolve (not reject) error state is communicated via the signal.
693
- resolve();
694
- }
695
- });
1146
+ finalizePendingLoadOnce();
1147
+ });
1148
+ logger.debug(`RiveFileService: Initializing file`, { cacheKey });
1149
+ // Await init() to catch initialization errors (e.g. WASM issues)
1150
+ await file.init();
1151
+ }
1152
+ catch (error) {
1153
+ logger.error('RiveFileService: Unexpected error loading file', error);
1154
+ stateSignal.set({
1155
+ riveFile: null,
1156
+ status: 'failed',
1157
+ });
1158
+ finalizePendingLoadOnce();
1159
+ }
696
1160
  }
697
1161
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
698
1162
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, providedIn: 'root' });
@@ -713,5 +1177,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
713
1177
  * Generated bundle index. Do not edit.
714
1178
  */
715
1179
 
716
- export { RiveCanvasComponent, RiveFileService, RiveLoadError };
1180
+ export { RiveCanvasComponent, RiveErrorCode, RiveFileService, RiveLoadError, RiveValidationError, provideRiveDebug };
717
1181
  //# sourceMappingURL=grandgular-rive-angular.mjs.map