@grandgular/rive-angular 0.1.2 → 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
348
+ }
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
80
373
  }
81
- return observerInstance;
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,21 +480,28 @@ 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;
@@ -176,11 +510,20 @@ class RiveCanvasComponent {
176
510
  lastWidth = 0;
177
511
  lastHeight = 0;
178
512
  constructor() {
179
- // 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
180
519
  effect(() => {
181
520
  const src = this.src();
182
521
  const buffer = this.buffer();
183
522
  const riveFile = this.riveFile();
523
+ // Track configuration changes to trigger reload
524
+ this.artboard();
525
+ this.animations();
526
+ this.stateMachines();
184
527
  untracked(() => {
185
528
  if ((src || buffer || riveFile) &&
186
529
  isPlatformBrowser(this.#platformId) &&
@@ -188,6 +531,17 @@ class RiveCanvasComponent {
188
531
  this.loadAnimation();
189
532
  });
190
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
+ });
191
545
  // Auto cleanup on destroy
192
546
  this.#destroyRef.onDestroy(() => {
193
547
  this.cleanupRive();
@@ -208,7 +562,6 @@ class RiveCanvasComponent {
208
562
  */
209
563
  setupResizeObserver() {
210
564
  const canvas = this.canvas().nativeElement;
211
- const dpr = window.devicePixelRatio || 1;
212
565
  this.resizeObserver = new ResizeObserver((entries) => {
213
566
  // Cancel any pending resize frame
214
567
  if (this.resizeRafId) {
@@ -224,6 +577,8 @@ class RiveCanvasComponent {
224
577
  this.lastHeight = height;
225
578
  // Defer resize to next animation frame to prevent excessive updates in Safari
226
579
  this.resizeRafId = requestAnimationFrame(() => {
580
+ // Read current DPR to support monitor changes and zoom
581
+ const dpr = window.devicePixelRatio || 1;
227
582
  // Set canvas size with device pixel ratio for sharp rendering
228
583
  canvas.width = width * dpr;
229
584
  canvas.height = height * dpr;
@@ -255,7 +610,6 @@ class RiveCanvasComponent {
255
610
  if (!this.shouldUseIntersectionObserver())
256
611
  return;
257
612
  const canvas = this.canvas().nativeElement;
258
- const observer = getElementObserver();
259
613
  const onIntersectionChange = (entry) => {
260
614
  if (entry.isIntersecting) {
261
615
  // Canvas is visible - start rendering
@@ -282,7 +636,7 @@ class RiveCanvasComponent {
282
636
  }
283
637
  }
284
638
  };
285
- observer.registerCallback(canvas, onIntersectionChange);
639
+ this.#elementObserver.registerCallback(canvas, onIntersectionChange);
286
640
  }
287
641
  /**
288
642
  * Retest intersection - workaround for Chrome bug
@@ -314,8 +668,7 @@ class RiveCanvasComponent {
314
668
  }
315
669
  if (this.shouldUseIntersectionObserver()) {
316
670
  const canvas = this.canvas().nativeElement;
317
- const observer = getElementObserver();
318
- observer.removeCallback(canvas);
671
+ this.#elementObserver.removeCallback(canvas);
319
672
  }
320
673
  }
321
674
  /**
@@ -331,16 +684,27 @@ class RiveCanvasComponent {
331
684
  const src = this.src();
332
685
  const buffer = this.buffer();
333
686
  const riveFile = this.riveFile();
334
- 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
+ })));
335
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
+ });
336
701
  // Build layout configuration
337
702
  const layoutParams = {
338
703
  fit: this.fit(),
339
704
  alignment: this.alignment(),
340
705
  };
341
- // Create Rive instance configuration
342
- // Using Record to allow dynamic property assignment
343
- const config = {
706
+ // Build typed Rive configuration
707
+ const baseConfig = {
344
708
  canvas,
345
709
  autoplay: this.autoplay(),
346
710
  layout: new Layout(layoutParams),
@@ -355,73 +719,114 @@ class RiveCanvasComponent {
355
719
  onStateChange: (event) => this.onStateChange(event),
356
720
  onRiveEvent: (event) => this.onRiveEvent(event),
357
721
  };
358
- // Add src, buffer, or riveFile (priority: riveFile > src > buffer)
359
- if (riveFile) {
360
- config['riveFile'] = riveFile;
361
- }
362
- else if (src) {
363
- config['src'] = src;
364
- }
365
- else if (buffer) {
366
- config['buffer'] = buffer;
367
- }
368
- // Add artboard if specified
369
- const artboard = this.artboard();
370
- if (artboard)
371
- config['artboard'] = artboard;
372
- // Add animations if specified
373
- const animations = this.animations();
374
- if (animations)
375
- config['animations'] = animations;
376
- // Add state machines if specified
377
- const stateMachines = this.stateMachines();
378
- if (stateMachines)
379
- config['stateMachines'] = stateMachines;
380
- // Safe type assertion - config contains all required properties
381
- // 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 };
382
740
  this.#rive = new Rive(config);
383
- // Update public signal and emit riveReady event
741
+ // Update public signal (riveReady will be emitted in onLoad)
384
742
  this.#ngZone.run(() => {
385
- this.riveInstance.set(this.#rive);
386
- this.riveReady.emit(this.#rive);
743
+ this.#riveInstance.set(this.#rive);
387
744
  });
388
745
  }
389
746
  catch (error) {
390
- console.error('Failed to initialize Rive instance:', error);
391
- 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
+ })));
392
753
  }
393
754
  });
394
755
  }
395
756
  // Event handlers (run inside Angular zone for change detection)
396
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
+ }
397
781
  this.#ngZone.run(() => {
398
- this.isLoaded.set(true);
782
+ this.#isLoaded.set(true);
399
783
  this.loaded.emit();
784
+ // Emit riveReady after animation is fully loaded
785
+ if (this.#rive) {
786
+ this.riveReady.emit(this.#rive);
787
+ }
400
788
  });
401
789
  }
402
790
  onLoadError(originalError) {
403
791
  this.#ngZone.run(() => {
404
- const error = new RiveLoadError('Failed to load Rive animation', originalError instanceof Error ? originalError : undefined);
405
- 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);
406
811
  this.loadError.emit(error);
407
812
  });
408
813
  }
409
814
  onPlay() {
410
815
  this.#ngZone.run(() => {
411
- this.isPlaying.set(true);
412
- this.isPaused.set(false);
816
+ this.#isPlaying.set(true);
817
+ this.#isPaused.set(false);
413
818
  });
414
819
  }
415
820
  onPause() {
416
821
  this.#ngZone.run(() => {
417
- this.isPlaying.set(false);
418
- this.isPaused.set(true);
822
+ this.#isPlaying.set(false);
823
+ this.#isPaused.set(true);
419
824
  });
420
825
  }
421
826
  onStop() {
422
827
  this.#ngZone.run(() => {
423
- this.isPlaying.set(false);
424
- this.isPaused.set(false);
828
+ this.#isPlaying.set(false);
829
+ this.#isPaused.set(false);
425
830
  });
426
831
  }
427
832
  onStateChange(event) {
@@ -493,6 +898,13 @@ class RiveCanvasComponent {
493
898
  if (!this.#rive)
494
899
  return;
495
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
+ }
496
908
  const inputs = this.#rive.stateMachineInputs(stateMachineName);
497
909
  const input = inputs.find((i) => i.name === inputName);
498
910
  if (input && 'value' in input) {
@@ -507,6 +919,13 @@ class RiveCanvasComponent {
507
919
  if (!this.#rive)
508
920
  return;
509
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
+ }
510
929
  const inputs = this.#rive.stateMachineInputs(stateMachineName);
511
930
  const input = inputs.find((i) => i.name === triggerName);
512
931
  if (input && 'fire' in input && typeof input.fire === 'function') {
@@ -523,18 +942,18 @@ class RiveCanvasComponent {
523
942
  this.#rive.cleanup();
524
943
  }
525
944
  catch (error) {
526
- console.warn('Error during Rive cleanup:', error);
945
+ this.logger.warn('Error during Rive cleanup:', error);
527
946
  }
528
947
  this.#rive = null;
529
948
  }
530
949
  // Reset signals
531
- this.riveInstance.set(null);
532
- this.isLoaded.set(false);
533
- this.isPlaying.set(false);
534
- this.isPaused.set(false);
950
+ this.#riveInstance.set(null);
951
+ this.#isLoaded.set(false);
952
+ this.#isPlaying.set(false);
953
+ this.#isPaused.set(false);
535
954
  }
536
955
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveCanvasComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
537
- 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: `
538
957
  <canvas #canvas [style.width.%]="100" [style.height.%]="100"></canvas>
539
958
  `, isInline: true, styles: [":host{display:block;width:100%;height:100%}canvas{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
540
959
  }
@@ -543,7 +962,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
543
962
  args: [{ selector: 'rive-canvas', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
544
963
  <canvas #canvas [style.width.%]="100" [style.height.%]="100"></canvas>
545
964
  `, styles: [":host{display:block;width:100%;height:100%}canvas{display:block}\n"] }]
546
- }], 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"] }] } });
547
966
 
548
967
  /**
549
968
  * Service for preloading and caching Rive files.
@@ -576,7 +995,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
576
995
  class RiveFileService {
577
996
  cache = new Map();
578
997
  pendingLoads = new Map();
998
+ bufferIdMap = new WeakMap();
579
999
  bufferIdCounter = 0;
1000
+ // Optional debug configuration
1001
+ globalDebugConfig = inject(RIVE_DEBUG_CONFIG, {
1002
+ optional: true,
1003
+ });
580
1004
  /**
581
1005
  * Load a RiveFile from URL or ArrayBuffer.
582
1006
  * Returns a signal with the file state and loading status.
@@ -588,15 +1012,20 @@ class RiveFileService {
588
1012
  */
589
1013
  loadFile(params) {
590
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 });
591
1018
  // Return cached entry if exists
592
1019
  const cached = this.cache.get(cacheKey);
593
1020
  if (cached) {
594
1021
  cached.refCount++;
1022
+ logger.debug(`RiveFileService: Cache hit for ${cacheKey}`);
595
1023
  return cached.state;
596
1024
  }
597
1025
  // Return pending load if already in progress
598
1026
  const pending = this.pendingLoads.get(cacheKey);
599
1027
  if (pending) {
1028
+ logger.debug(`RiveFileService: Reuse pending load for ${cacheKey}`);
600
1029
  return pending.stateSignal.asReadonly();
601
1030
  }
602
1031
  // Create new loading state
@@ -605,7 +1034,7 @@ class RiveFileService {
605
1034
  status: 'loading',
606
1035
  }, ...(ngDevMode ? [{ debugName: "stateSignal" }] : []));
607
1036
  // Start loading and track as pending
608
- const promise = this.loadRiveFile(params, stateSignal, cacheKey);
1037
+ const promise = this.loadRiveFile(params, stateSignal, cacheKey, logger);
609
1038
  this.pendingLoads.set(cacheKey, { stateSignal, promise });
610
1039
  return stateSignal.asReadonly();
611
1040
  }
@@ -631,9 +1060,18 @@ class RiveFileService {
631
1060
  }
632
1061
  }
633
1062
  /**
634
- * Clear all cached files
1063
+ * Clear all cached files and abort pending loads
635
1064
  */
636
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
637
1075
  this.cache.forEach((entry) => {
638
1076
  try {
639
1077
  entry.file.cleanup();
@@ -652,67 +1090,73 @@ class RiveFileService {
652
1090
  return `src:${params.src}`;
653
1091
  }
654
1092
  if (params.buffer) {
655
- // For buffers, generate unique ID to avoid collisions
656
- // Store the ID on the buffer object itself
657
- const bufferWithId = params.buffer;
658
- if (!bufferWithId.__riveBufferId) {
659
- 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);
660
1098
  }
661
- return `buffer:${bufferWithId.__riveBufferId}`;
1099
+ return `buffer:${bufferId}`;
662
1100
  }
663
1101
  return 'unknown';
664
1102
  }
665
1103
  /**
666
- * Load RiveFile and update state signal
1104
+ * Load RiveFile and update state signal.
1105
+ * Addresses race condition by setting up listeners BEFORE init.
667
1106
  */
668
- loadRiveFile(params, stateSignal, cacheKey) {
669
- return new Promise((resolve) => {
670
- try {
671
- const file = new RiveFile(params);
672
- file.init();
673
- file.on(EventType.Load, () => {
674
- // Request an instance to increment reference count
675
- // This prevents the file from being destroyed while in use
676
- file.getInstance();
677
- stateSignal.set({
678
- riveFile: file,
679
- status: 'success',
680
- });
681
- // Cache the successfully loaded file
682
- this.cache.set(cacheKey, {
683
- file,
684
- state: stateSignal.asReadonly(),
685
- refCount: 1,
686
- });
687
- // Remove from pending loads
688
- this.pendingLoads.delete(cacheKey);
689
- 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',
690
1131
  });
691
- file.on(EventType.LoadError, () => {
692
- stateSignal.set({
693
- riveFile: null,
694
- status: 'failed',
695
- });
696
- // Remove from pending loads
697
- this.pendingLoads.delete(cacheKey);
698
- // Resolve (not reject) — error state is communicated via the signal.
699
- // Rejecting would cause an unhandled promise rejection since no
700
- // consumer awaits or catches this promise.
701
- resolve();
1132
+ // Cache the successfully loaded file
1133
+ this.cache.set(cacheKey, {
1134
+ file,
1135
+ state: stateSignal.asReadonly(),
1136
+ refCount: 1,
702
1137
  });
703
- }
704
- catch (error) {
705
- console.error('Failed to load RiveFile:', error);
1138
+ finalizePendingLoadOnce();
1139
+ });
1140
+ file.on(EventType.LoadError, () => {
1141
+ logger.warn(`RiveFileService: Failed to load file`, { cacheKey });
706
1142
  stateSignal.set({
707
1143
  riveFile: null,
708
1144
  status: 'failed',
709
1145
  });
710
- // Remove from pending loads
711
- this.pendingLoads.delete(cacheKey);
712
- // Resolve (not reject) error state is communicated via the signal.
713
- resolve();
714
- }
715
- });
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
+ }
716
1160
  }
717
1161
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
718
1162
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, providedIn: 'root' });
@@ -733,5 +1177,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
733
1177
  * Generated bundle index. Do not edit.
734
1178
  */
735
1179
 
736
- export { RiveCanvasComponent, RiveFileService, RiveLoadError };
1180
+ export { RiveCanvasComponent, RiveErrorCode, RiveFileService, RiveLoadError, RiveValidationError, provideRiveDebug };
737
1181
  //# sourceMappingURL=grandgular-rive-angular.mjs.map