@grandgular/rive-angular 0.1.2 → 0.3.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,106 @@ 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
+ RiveErrorCode["TextRunNotFound"] = "RIVE_205";
77
+ // Configuration Errors
78
+ RiveErrorCode["NoSource"] = "RIVE_301";
79
+ RiveErrorCode["InvalidCanvas"] = "RIVE_302";
80
+ })(RiveErrorCode || (RiveErrorCode = {}));
81
+ /**
82
+ * Template messages for each error code.
83
+ * Used by formatErrorMessage to generate user-friendly descriptions.
84
+ */
85
+ const ERROR_MESSAGES = {
86
+ [RiveErrorCode.FileNotFound]: 'File not found: {url}',
87
+ [RiveErrorCode.InvalidFormat]: 'Invalid .riv file format',
88
+ [RiveErrorCode.NetworkError]: 'Network error while loading file',
89
+ [RiveErrorCode.ArtboardNotFound]: 'Artboard "{name}" not found',
90
+ [RiveErrorCode.AnimationNotFound]: 'Animation "{name}" not found',
91
+ [RiveErrorCode.StateMachineNotFound]: 'State machine "{name}" not found',
92
+ [RiveErrorCode.InputNotFound]: 'Input "{name}" not found in "{stateMachine}"',
93
+ [RiveErrorCode.TextRunNotFound]: 'Text run "{name}" not found',
94
+ [RiveErrorCode.NoSource]: 'No animation source provided',
95
+ [RiveErrorCode.InvalidCanvas]: 'Invalid canvas element',
96
+ };
97
+ /**
98
+ * Formats an error message by replacing placeholders with actual values.
99
+ *
100
+ * @param code - The error code
101
+ * @param params - Record of values to replace in the template (e.g. { name: 'MyAnim' })
102
+ * @returns The formatted error string
103
+ */
104
+ function formatErrorMessage(code, params = {}) {
105
+ let message = ERROR_MESSAGES[code] || 'Unknown Rive error';
106
+ for (const [key, value] of Object.entries(params)) {
107
+ message = message.replace(`{${key}}`, value);
108
+ }
109
+ return message;
110
+ }
21
111
 
22
112
  /**
23
113
  * Fake IntersectionObserver for environments where it's not available (e.g., SSR)
@@ -45,12 +135,21 @@ const MyIntersectionObserver = (typeof globalThis !== 'undefined' && globalThis.
45
135
  * Singleton IntersectionObserver wrapper for observing multiple elements
46
136
  * with individual callbacks. This avoids creating multiple IntersectionObserver
47
137
  * instances which is more efficient.
138
+ *
139
+ * Provided as an Angular service for better testability and DI integration.
48
140
  */
49
141
  class ElementObserver {
50
142
  observer;
51
143
  elementsMap = new Map();
144
+ platformId = inject(PLATFORM_ID);
52
145
  constructor() {
53
- this.observer = new MyIntersectionObserver(this.onObserved);
146
+ // Only create real observer in browser environment
147
+ if (isPlatformBrowser(this.platformId)) {
148
+ this.observer = new MyIntersectionObserver(this.onObserved);
149
+ }
150
+ else {
151
+ this.observer = new FakeIntersectionObserver();
152
+ }
54
153
  }
55
154
  onObserved = (entries) => {
56
155
  entries.forEach((entry) => {
@@ -68,17 +167,238 @@ class ElementObserver {
68
167
  this.observer.unobserve(element);
69
168
  this.elementsMap.delete(element);
70
169
  }
170
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ElementObserver, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
171
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ElementObserver, providedIn: 'root' });
71
172
  }
72
- // Singleton instance
73
- let observerInstance = null;
173
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: ElementObserver, decorators: [{
174
+ type: Injectable,
175
+ args: [{
176
+ providedIn: 'root',
177
+ }]
178
+ }], ctorParameters: () => [] });
179
+ // Legacy function for backward compatibility
180
+ // New code should inject ElementObserver directly
181
+ let legacyObserverInstance = null;
74
182
  /**
183
+ * @deprecated Use dependency injection instead: `inject(ElementObserver)`
75
184
  * Get the singleton ElementObserver instance
76
185
  */
77
186
  function getElementObserver() {
78
- if (!observerInstance) {
79
- observerInstance = new ElementObserver();
187
+ if (!legacyObserverInstance) {
188
+ legacyObserverInstance = new ElementObserver();
189
+ }
190
+ return legacyObserverInstance;
191
+ }
192
+
193
+ /**
194
+ * Injection token for global Rive debug configuration.
195
+ * Can be provided via provideRiveDebug().
196
+ */
197
+ const RIVE_DEBUG_CONFIG = new InjectionToken('RIVE_DEBUG_CONFIG');
198
+ /**
199
+ * Provides global configuration for Rive debugging.
200
+ * Use this in your app.config.ts or module providers.
201
+ *
202
+ * @example
203
+ * providers: [
204
+ * provideRiveDebug({ logLevel: 'debug' })
205
+ * ]
206
+ */
207
+ function provideRiveDebug(config) {
208
+ return {
209
+ provide: RIVE_DEBUG_CONFIG,
210
+ useValue: config,
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Internal logger for Rive Angular library.
216
+ * Handles log levels and formatting.
217
+ * Not exported publicly.
218
+ */
219
+ class RiveLogger {
220
+ level;
221
+ constructor(globalConfig, localDebug) {
222
+ this.level = this.resolveLogLevel(globalConfig, localDebug);
223
+ }
224
+ /**
225
+ * Resolve effective log level based on precedence rules:
226
+ * 1. Local debug=true -> 'debug'
227
+ * 2. Local debug=false/undefined -> Use global config
228
+ * 3. No config -> 'error' (default)
229
+ */
230
+ resolveLogLevel(globalConfig, localDebug) {
231
+ if (localDebug === true) {
232
+ return 'debug';
233
+ }
234
+ if (globalConfig?.logLevel) {
235
+ return globalConfig.logLevel;
236
+ }
237
+ return 'error';
238
+ }
239
+ /**
240
+ * Update log level dynamically (e.g. when input changes)
241
+ */
242
+ update(globalConfig, localDebug) {
243
+ this.level = this.resolveLogLevel(globalConfig, localDebug);
244
+ }
245
+ debug(message, ...args) {
246
+ if (this.shouldLog('debug')) {
247
+ console.debug(`[Rive] ${message}`, ...args);
248
+ }
249
+ }
250
+ info(message, ...args) {
251
+ if (this.shouldLog('info')) {
252
+ console.info(`[Rive] ${message}`, ...args);
253
+ }
254
+ }
255
+ warn(message, ...args) {
256
+ if (this.shouldLog('warn')) {
257
+ console.warn(`[Rive] ${message}`, ...args);
258
+ }
259
+ }
260
+ error(message, ...args) {
261
+ if (this.shouldLog('error')) {
262
+ console.error(`[Rive] ${message}`, ...args);
263
+ }
264
+ }
265
+ shouldLog(level) {
266
+ const levels = ['none', 'error', 'warn', 'info', 'debug'];
267
+ const currentIdx = levels.indexOf(this.level);
268
+ const targetIdx = levels.indexOf(level);
269
+ return currentIdx >= targetIdx;
80
270
  }
81
- return observerInstance;
271
+ }
272
+
273
+ /**
274
+ * Validates requested artboard name against available artboards.
275
+ * Returns error if not found.
276
+ */
277
+ function validateArtboard(rive, requestedName) {
278
+ if (!requestedName)
279
+ return null;
280
+ try {
281
+ // Safe check: ensure artboardNames exist on runtime
282
+ // Note: These properties exist at runtime but may not be in type definitions
283
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
284
+ const available = rive.artboardNames;
285
+ if (!available || !available.includes(requestedName)) {
286
+ return new RiveValidationError(formatErrorMessage(RiveErrorCode.ArtboardNotFound, {
287
+ name: requestedName,
288
+ }), RiveErrorCode.ArtboardNotFound, available || [], available && available.length > 0
289
+ ? `Available artboards: ${available.join(', ')}`
290
+ : 'No artboards found in file');
291
+ }
292
+ }
293
+ catch {
294
+ // Graceful fallback if runtime metadata is not accessible
295
+ // Return null silently to avoid breaking validation flow
296
+ }
297
+ return null;
298
+ }
299
+ /**
300
+ * Validates requested animation names against available animations.
301
+ * Returns first error found.
302
+ */
303
+ function validateAnimations(rive, requestedNames) {
304
+ if (!requestedNames)
305
+ return null;
306
+ const names = Array.isArray(requestedNames)
307
+ ? requestedNames
308
+ : [requestedNames];
309
+ try {
310
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
311
+ const available = rive.animationNames;
312
+ for (const name of names) {
313
+ if (!available || !available.includes(name)) {
314
+ return new RiveValidationError(formatErrorMessage(RiveErrorCode.AnimationNotFound, { name }), RiveErrorCode.AnimationNotFound, available || [], available && available.length > 0
315
+ ? `Available animations: ${available.join(', ')}`
316
+ : 'No animations found in file');
317
+ }
318
+ }
319
+ }
320
+ catch {
321
+ // Graceful fallback if runtime metadata is not accessible
322
+ // Return null silently to avoid breaking validation flow
323
+ }
324
+ return null;
325
+ }
326
+ /**
327
+ * Validates requested state machine names against available state machines.
328
+ * Returns first error found.
329
+ */
330
+ function validateStateMachines(rive, requestedNames) {
331
+ if (!requestedNames)
332
+ return null;
333
+ const names = Array.isArray(requestedNames)
334
+ ? requestedNames
335
+ : [requestedNames];
336
+ try {
337
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
338
+ const available = rive.stateMachineNames;
339
+ for (const name of names) {
340
+ if (!available || !available.includes(name)) {
341
+ return new RiveValidationError(formatErrorMessage(RiveErrorCode.StateMachineNotFound, { name }), RiveErrorCode.StateMachineNotFound, available || [], available && available.length > 0
342
+ ? `Available state machines: ${available.join(', ')}`
343
+ : 'No state machines found in file');
344
+ }
345
+ }
346
+ }
347
+ catch {
348
+ // Graceful fallback if runtime metadata is not accessible
349
+ // Return null silently to avoid breaking validation flow
350
+ }
351
+ return null;
352
+ }
353
+ /**
354
+ * Validates if an input exists within a specific state machine.
355
+ */
356
+ function validateInput(rive, stateMachineName, inputName) {
357
+ try {
358
+ const inputs = rive.stateMachineInputs(stateMachineName);
359
+ if (!inputs)
360
+ return null; // Should not happen if SM exists
361
+ const found = inputs.find((i) => i.name === inputName);
362
+ if (!found) {
363
+ const available = inputs.map((i) => i.name);
364
+ return new RiveValidationError(formatErrorMessage(RiveErrorCode.InputNotFound, {
365
+ name: inputName,
366
+ stateMachine: stateMachineName,
367
+ }), RiveErrorCode.InputNotFound, available, available.length > 0
368
+ ? `Available inputs in "${stateMachineName}": ${available.join(', ')}`
369
+ : `No inputs found in state machine "${stateMachineName}"`);
370
+ }
371
+ }
372
+ catch {
373
+ // Graceful fallback if runtime metadata is not accessible
374
+ // Return null silently to avoid breaking validation flow
375
+ }
376
+ return null;
377
+ }
378
+ /**
379
+ * Runs full configuration validation.
380
+ * Logs warnings and returns array of errors.
381
+ */
382
+ function validateConfiguration(rive, config, logger) {
383
+ const errors = [];
384
+ const artboardError = validateArtboard(rive, config.artboard);
385
+ if (artboardError)
386
+ errors.push(artboardError);
387
+ const animError = validateAnimations(rive, config.animations);
388
+ if (animError)
389
+ errors.push(animError);
390
+ const smError = validateStateMachines(rive, config.stateMachines);
391
+ if (smError)
392
+ errors.push(smError);
393
+ if (errors.length > 0) {
394
+ logger.warn(`Validation failed with ${errors.length} errors:`);
395
+ errors.forEach((err) => {
396
+ logger.warn(`- ${err.message}`);
397
+ if (err.suggestion)
398
+ logger.warn(` Suggestion: ${err.suggestion}`);
399
+ });
400
+ }
401
+ return errors;
82
402
  }
83
403
 
84
404
  /**
@@ -110,6 +430,8 @@ class RiveCanvasComponent {
110
430
  #destroyRef = inject(DestroyRef);
111
431
  #platformId = inject(PLATFORM_ID);
112
432
  #ngZone = inject(NgZone);
433
+ #globalDebugConfig = inject(RIVE_DEBUG_CONFIG, { optional: true });
434
+ #elementObserver = inject(ElementObserver);
113
435
  src = input(...(ngDevMode ? [undefined, { debugName: "src" }] : []));
114
436
  buffer = input(...(ngDevMode ? [undefined, { debugName: "buffer" }] : []));
115
437
  /**
@@ -139,6 +461,20 @@ class RiveCanvasComponent {
139
461
  * Default is false for security - events must be handled manually via riveEvent output.
140
462
  */
141
463
  automaticallyHandleEvents = input(false, ...(ngDevMode ? [{ debugName: "automaticallyHandleEvents" }] : []));
464
+ /**
465
+ * Enable debug mode for this specific instance.
466
+ * Overrides global configuration if set.
467
+ * - true: 'debug' level
468
+ * - false/undefined: use global level
469
+ */
470
+ debugMode = input(...(ngDevMode ? [undefined, { debugName: "debugMode" }] : []));
471
+ /**
472
+ * Record of text run names to values for declarative text setting.
473
+ * Keys present in this input are CONTROLLED — the input is the source of truth.
474
+ * Keys absent from this input are UNCONTROLLED — managed imperatively.
475
+ * Values are applied reactively when input changes.
476
+ */
477
+ textRuns = input(...(ngDevMode ? [undefined, { debugName: "textRuns" }] : []));
142
478
  // Outputs (Events)
143
479
  loaded = output();
144
480
  loadError = output();
@@ -153,21 +489,28 @@ class RiveCanvasComponent {
153
489
  */
154
490
  riveEvent = output();
155
491
  /**
156
- * Emitted when Rive instance is created and ready.
492
+ * Emitted when Rive instance is fully loaded and ready.
157
493
  * Provides direct access to the Rive instance for advanced use cases.
494
+ * Note: This fires AFTER the animation is loaded, not just instantiated.
158
495
  */
159
496
  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" }] : []));
497
+ // Private writable signals
498
+ #isPlaying = signal(false, ...(ngDevMode ? [{ debugName: "#isPlaying" }] : []));
499
+ #isPaused = signal(false, ...(ngDevMode ? [{ debugName: "#isPaused" }] : []));
500
+ #isLoaded = signal(false, ...(ngDevMode ? [{ debugName: "#isLoaded" }] : []));
501
+ #riveInstance = signal(null, ...(ngDevMode ? [{ debugName: "#riveInstance" }] : []));
502
+ // Public readonly signals
503
+ isPlaying = this.#isPlaying.asReadonly();
504
+ isPaused = this.#isPaused.asReadonly();
505
+ isLoaded = this.#isLoaded.asReadonly();
164
506
  /**
165
507
  * Public signal providing access to the Rive instance.
166
508
  * Use this to access advanced Rive SDK features.
167
509
  */
168
- riveInstance = signal(null, ...(ngDevMode ? [{ debugName: "riveInstance" }] : []));
510
+ riveInstance = this.#riveInstance.asReadonly();
169
511
  // Private state
170
512
  #rive = null;
513
+ logger;
171
514
  resizeObserver = null;
172
515
  isInitialized = false;
173
516
  isPausedByIntersectionObserver = false;
@@ -176,11 +519,20 @@ class RiveCanvasComponent {
176
519
  lastWidth = 0;
177
520
  lastHeight = 0;
178
521
  constructor() {
179
- // Effect to reload animation when src, buffer, or riveFile changes
522
+ this.logger = new RiveLogger(this.#globalDebugConfig, this.debugMode());
523
+ // Effect to update logger level when debugMode changes
524
+ effect(() => {
525
+ this.logger.update(this.#globalDebugConfig, this.debugMode());
526
+ });
527
+ // Effect to reload animation when src, buffer, riveFile, or configuration changes
180
528
  effect(() => {
181
529
  const src = this.src();
182
530
  const buffer = this.buffer();
183
531
  const riveFile = this.riveFile();
532
+ // Track configuration changes to trigger reload
533
+ this.artboard();
534
+ this.animations();
535
+ this.stateMachines();
184
536
  untracked(() => {
185
537
  if ((src || buffer || riveFile) &&
186
538
  isPlatformBrowser(this.#platformId) &&
@@ -188,6 +540,27 @@ class RiveCanvasComponent {
188
540
  this.loadAnimation();
189
541
  });
190
542
  });
543
+ // Effect to update layout when fit or alignment changes
544
+ effect(() => {
545
+ const fit = this.fit();
546
+ const alignment = this.alignment();
547
+ untracked(() => {
548
+ if (this.#rive && isPlatformBrowser(this.#platformId)) {
549
+ const layoutParams = { fit, alignment };
550
+ this.#rive.layout = new Layout(layoutParams);
551
+ }
552
+ });
553
+ });
554
+ // Effect to apply text runs when input changes or animation loads
555
+ effect(() => {
556
+ const runs = this.textRuns();
557
+ const isLoaded = this.#isLoaded();
558
+ untracked(() => {
559
+ if (runs && isLoaded && this.#rive) {
560
+ this.applyTextRuns(runs);
561
+ }
562
+ });
563
+ });
191
564
  // Auto cleanup on destroy
192
565
  this.#destroyRef.onDestroy(() => {
193
566
  this.cleanupRive();
@@ -208,7 +581,6 @@ class RiveCanvasComponent {
208
581
  */
209
582
  setupResizeObserver() {
210
583
  const canvas = this.canvas().nativeElement;
211
- const dpr = window.devicePixelRatio || 1;
212
584
  this.resizeObserver = new ResizeObserver((entries) => {
213
585
  // Cancel any pending resize frame
214
586
  if (this.resizeRafId) {
@@ -224,6 +596,8 @@ class RiveCanvasComponent {
224
596
  this.lastHeight = height;
225
597
  // Defer resize to next animation frame to prevent excessive updates in Safari
226
598
  this.resizeRafId = requestAnimationFrame(() => {
599
+ // Read current DPR to support monitor changes and zoom
600
+ const dpr = window.devicePixelRatio || 1;
227
601
  // Set canvas size with device pixel ratio for sharp rendering
228
602
  canvas.width = width * dpr;
229
603
  canvas.height = height * dpr;
@@ -255,7 +629,6 @@ class RiveCanvasComponent {
255
629
  if (!this.shouldUseIntersectionObserver())
256
630
  return;
257
631
  const canvas = this.canvas().nativeElement;
258
- const observer = getElementObserver();
259
632
  const onIntersectionChange = (entry) => {
260
633
  if (entry.isIntersecting) {
261
634
  // Canvas is visible - start rendering
@@ -282,7 +655,7 @@ class RiveCanvasComponent {
282
655
  }
283
656
  }
284
657
  };
285
- observer.registerCallback(canvas, onIntersectionChange);
658
+ this.#elementObserver.registerCallback(canvas, onIntersectionChange);
286
659
  }
287
660
  /**
288
661
  * Retest intersection - workaround for Chrome bug
@@ -314,8 +687,7 @@ class RiveCanvasComponent {
314
687
  }
315
688
  if (this.shouldUseIntersectionObserver()) {
316
689
  const canvas = this.canvas().nativeElement;
317
- const observer = getElementObserver();
318
- observer.removeCallback(canvas);
690
+ this.#elementObserver.removeCallback(canvas);
319
691
  }
320
692
  }
321
693
  /**
@@ -331,16 +703,27 @@ class RiveCanvasComponent {
331
703
  const src = this.src();
332
704
  const buffer = this.buffer();
333
705
  const riveFile = this.riveFile();
334
- if (!src && !buffer && !riveFile)
706
+ if (!src && !buffer && !riveFile) {
707
+ this.logger.warn('No animation source provided (src, buffer, or riveFile)');
708
+ this.#ngZone.run(() => this.loadError.emit(new RiveLoadError({
709
+ message: 'No animation source provided',
710
+ code: RiveErrorCode.NoSource,
711
+ })));
335
712
  return;
713
+ }
714
+ this.logger.info(`Loading animation`, {
715
+ src: src || (buffer ? 'buffer' : 'riveFile'),
716
+ canvasWidth: canvas.width,
717
+ canvasHeight: canvas.height,
718
+ dpr: window.devicePixelRatio,
719
+ });
336
720
  // Build layout configuration
337
721
  const layoutParams = {
338
722
  fit: this.fit(),
339
723
  alignment: this.alignment(),
340
724
  };
341
- // Create Rive instance configuration
342
- // Using Record to allow dynamic property assignment
343
- const config = {
725
+ // Build typed Rive configuration
726
+ const baseConfig = {
344
727
  canvas,
345
728
  autoplay: this.autoplay(),
346
729
  layout: new Layout(layoutParams),
@@ -355,73 +738,114 @@ class RiveCanvasComponent {
355
738
  onStateChange: (event) => this.onStateChange(event),
356
739
  onRiveEvent: (event) => this.onRiveEvent(event),
357
740
  };
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
741
+ // Add source (priority: riveFile > src > buffer)
742
+ const sourceConfig = riveFile
743
+ ? { riveFile }
744
+ : src
745
+ ? { src }
746
+ : buffer
747
+ ? { buffer }
748
+ : {};
749
+ // Add optional configuration
750
+ const optionalConfig = {
751
+ ...(this.artboard() ? { artboard: this.artboard() } : {}),
752
+ ...(this.animations() ? { animations: this.animations() } : {}),
753
+ ...(this.stateMachines()
754
+ ? { stateMachines: this.stateMachines() }
755
+ : {}),
756
+ };
757
+ // Combine all configurations
758
+ const config = { ...baseConfig, ...sourceConfig, ...optionalConfig };
382
759
  this.#rive = new Rive(config);
383
- // Update public signal and emit riveReady event
760
+ // Update public signal (riveReady will be emitted in onLoad)
384
761
  this.#ngZone.run(() => {
385
- this.riveInstance.set(this.#rive);
386
- this.riveReady.emit(this.#rive);
762
+ this.#riveInstance.set(this.#rive);
387
763
  });
388
764
  }
389
765
  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)));
766
+ this.logger.error('Failed to initialize Rive instance:', error);
767
+ this.#ngZone.run(() => this.loadError.emit(new RiveLoadError({
768
+ message: 'Failed to initialize Rive instance',
769
+ code: RiveErrorCode.InvalidFormat,
770
+ cause: error instanceof Error ? error : undefined,
771
+ })));
392
772
  }
393
773
  });
394
774
  }
395
775
  // Event handlers (run inside Angular zone for change detection)
396
776
  onLoad() {
777
+ // Validate loaded configuration
778
+ if (this.#rive) {
779
+ const validationErrors = validateConfiguration(this.#rive, {
780
+ artboard: this.artboard(),
781
+ animations: this.animations(),
782
+ stateMachines: this.stateMachines(),
783
+ }, this.logger);
784
+ // Emit validation errors via loadError output
785
+ if (validationErrors.length > 0) {
786
+ this.#ngZone.run(() => {
787
+ validationErrors.forEach((err) => this.loadError.emit(err));
788
+ });
789
+ }
790
+ // Log available assets in debug mode
791
+ // Note: These properties exist at runtime but may not be in type definitions
792
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
793
+ const riveWithMetadata = this.#rive;
794
+ this.logger.debug('Animation loaded successfully. Available assets:', {
795
+ artboards: riveWithMetadata.artboardNames,
796
+ animations: riveWithMetadata.animationNames,
797
+ stateMachines: riveWithMetadata.stateMachineNames,
798
+ });
799
+ }
397
800
  this.#ngZone.run(() => {
398
- this.isLoaded.set(true);
801
+ this.#isLoaded.set(true);
399
802
  this.loaded.emit();
803
+ // Emit riveReady after animation is fully loaded
804
+ if (this.#rive) {
805
+ this.riveReady.emit(this.#rive);
806
+ }
400
807
  });
401
808
  }
402
809
  onLoadError(originalError) {
403
810
  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);
811
+ // Determine probable cause and code
812
+ let code = RiveErrorCode.NetworkError;
813
+ let message = 'Failed to load Rive animation';
814
+ if (originalError instanceof Error) {
815
+ if (originalError.message.includes('404')) {
816
+ code = RiveErrorCode.FileNotFound;
817
+ message = `File not found: ${this.src()}`;
818
+ }
819
+ else if (originalError.message.includes('format')) {
820
+ code = RiveErrorCode.InvalidFormat;
821
+ message = 'Invalid .riv file format';
822
+ }
823
+ }
824
+ const error = new RiveLoadError({
825
+ message,
826
+ code,
827
+ cause: originalError instanceof Error ? originalError : undefined,
828
+ });
829
+ this.logger.error('Rive load error:', error);
406
830
  this.loadError.emit(error);
407
831
  });
408
832
  }
409
833
  onPlay() {
410
834
  this.#ngZone.run(() => {
411
- this.isPlaying.set(true);
412
- this.isPaused.set(false);
835
+ this.#isPlaying.set(true);
836
+ this.#isPaused.set(false);
413
837
  });
414
838
  }
415
839
  onPause() {
416
840
  this.#ngZone.run(() => {
417
- this.isPlaying.set(false);
418
- this.isPaused.set(true);
841
+ this.#isPlaying.set(false);
842
+ this.#isPaused.set(true);
419
843
  });
420
844
  }
421
845
  onStop() {
422
846
  this.#ngZone.run(() => {
423
- this.isPlaying.set(false);
424
- this.isPaused.set(false);
847
+ this.#isPlaying.set(false);
848
+ this.#isPaused.set(false);
425
849
  });
426
850
  }
427
851
  onStateChange(event) {
@@ -493,6 +917,13 @@ class RiveCanvasComponent {
493
917
  if (!this.#rive)
494
918
  return;
495
919
  this.#ngZone.runOutsideAngular(() => {
920
+ // Validate input existence first
921
+ const error = validateInput(this.#rive, stateMachineName, inputName);
922
+ if (error) {
923
+ this.logger.warn(error.message);
924
+ this.#ngZone.run(() => this.loadError.emit(error));
925
+ return;
926
+ }
496
927
  const inputs = this.#rive.stateMachineInputs(stateMachineName);
497
928
  const input = inputs.find((i) => i.name === inputName);
498
929
  if (input && 'value' in input) {
@@ -507,6 +938,13 @@ class RiveCanvasComponent {
507
938
  if (!this.#rive)
508
939
  return;
509
940
  this.#ngZone.runOutsideAngular(() => {
941
+ // Validate trigger (input) existence first
942
+ const error = validateInput(this.#rive, stateMachineName, triggerName);
943
+ if (error) {
944
+ this.logger.warn(error.message);
945
+ this.#ngZone.run(() => this.loadError.emit(error));
946
+ return;
947
+ }
510
948
  const inputs = this.#rive.stateMachineInputs(stateMachineName);
511
949
  const input = inputs.find((i) => i.name === triggerName);
512
950
  if (input && 'fire' in input && typeof input.fire === 'function') {
@@ -514,6 +952,104 @@ class RiveCanvasComponent {
514
952
  }
515
953
  });
516
954
  }
955
+ /**
956
+ * Get the current value of a text run.
957
+ * Returns undefined if the text run doesn't exist or Rive instance is not loaded.
958
+ */
959
+ getTextRunValue(textRunName) {
960
+ if (!this.#rive)
961
+ return undefined;
962
+ try {
963
+ return this.#ngZone.runOutsideAngular(() => {
964
+ return this.#rive.getTextRunValue(textRunName);
965
+ });
966
+ }
967
+ catch (error) {
968
+ this.logger.warn(`Failed to get text run "${textRunName}":`, error);
969
+ return undefined;
970
+ }
971
+ }
972
+ /**
973
+ * Set a text run value.
974
+ * Warning: If the text run is controlled by textRuns input, this change will be overwritten
975
+ * on the next input update.
976
+ */
977
+ setTextRunValue(textRunName, textRunValue) {
978
+ if (!this.#rive)
979
+ return;
980
+ // Check if this key is controlled by textRuns input
981
+ const controlledRuns = this.textRuns();
982
+ if (controlledRuns && textRunName in controlledRuns) {
983
+ this.logger.warn(`Text run "${textRunName}" is controlled by textRuns input. This change will be overwritten on next input update.`);
984
+ }
985
+ this.#ngZone.runOutsideAngular(() => {
986
+ try {
987
+ this.#rive.setTextRunValue(textRunName, textRunValue);
988
+ this.logger.debug(`Text run "${textRunName}" set to "${textRunValue}"`);
989
+ }
990
+ catch (error) {
991
+ this.logger.warn(`Failed to set text run "${textRunName}":`, error);
992
+ this.#ngZone.run(() => this.loadError.emit(new RiveValidationError(formatErrorMessage(RiveErrorCode.TextRunNotFound, {
993
+ name: textRunName,
994
+ }), RiveErrorCode.TextRunNotFound)));
995
+ }
996
+ });
997
+ }
998
+ /**
999
+ * Get the current value of a text run at a specific path (for nested artboards/components).
1000
+ * Returns undefined if the text run doesn't exist or Rive instance is not loaded.
1001
+ */
1002
+ getTextRunValueAtPath(textRunName, path) {
1003
+ if (!this.#rive)
1004
+ return undefined;
1005
+ try {
1006
+ return this.#ngZone.runOutsideAngular(() => {
1007
+ return this.#rive.getTextRunValueAtPath(textRunName, path);
1008
+ });
1009
+ }
1010
+ catch (error) {
1011
+ this.logger.warn(`Failed to get text run "${textRunName}" at path "${path}":`, error);
1012
+ return undefined;
1013
+ }
1014
+ }
1015
+ /**
1016
+ * Set a text run value at a specific path (for nested artboards/components).
1017
+ * Note: AtPath text runs are always uncontrolled (not managed by textRuns input).
1018
+ */
1019
+ setTextRunValueAtPath(textRunName, textRunValue, path) {
1020
+ if (!this.#rive)
1021
+ return;
1022
+ this.#ngZone.runOutsideAngular(() => {
1023
+ try {
1024
+ this.#rive.setTextRunValueAtPath(textRunName, textRunValue, path);
1025
+ this.logger.debug(`Text run "${textRunName}" at path "${path}" set to "${textRunValue}"`);
1026
+ }
1027
+ catch (error) {
1028
+ this.logger.warn(`Failed to set text run "${textRunName}" at path "${path}":`, error);
1029
+ this.#ngZone.run(() => this.loadError.emit(new RiveValidationError(formatErrorMessage(RiveErrorCode.TextRunNotFound, {
1030
+ name: textRunName,
1031
+ }), RiveErrorCode.TextRunNotFound)));
1032
+ }
1033
+ });
1034
+ }
1035
+ /**
1036
+ * Apply all text runs from input (controlled keys).
1037
+ * Called on every input change or load.
1038
+ */
1039
+ applyTextRuns(runs) {
1040
+ this.#ngZone.runOutsideAngular(() => {
1041
+ for (const [name, value] of Object.entries(runs)) {
1042
+ try {
1043
+ this.#rive.setTextRunValue(name, value);
1044
+ this.logger.debug(`Text run "${name}" set to "${value}"`);
1045
+ }
1046
+ catch (error) {
1047
+ this.logger.warn(`Failed to set text run "${name}":`, error);
1048
+ this.#ngZone.run(() => this.loadError.emit(new RiveValidationError(formatErrorMessage(RiveErrorCode.TextRunNotFound, { name }), RiveErrorCode.TextRunNotFound)));
1049
+ }
1050
+ }
1051
+ });
1052
+ }
517
1053
  /**
518
1054
  * Clean up Rive instance only
519
1055
  */
@@ -523,18 +1059,18 @@ class RiveCanvasComponent {
523
1059
  this.#rive.cleanup();
524
1060
  }
525
1061
  catch (error) {
526
- console.warn('Error during Rive cleanup:', error);
1062
+ this.logger.warn('Error during Rive cleanup:', error);
527
1063
  }
528
1064
  this.#rive = null;
529
1065
  }
530
1066
  // Reset signals
531
- this.riveInstance.set(null);
532
- this.isLoaded.set(false);
533
- this.isPlaying.set(false);
534
- this.isPaused.set(false);
1067
+ this.#riveInstance.set(null);
1068
+ this.#isLoaded.set(false);
1069
+ this.#isPlaying.set(false);
1070
+ this.#isPaused.set(false);
535
1071
  }
536
1072
  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: `
1073
+ 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 }, textRuns: { classPropertyName: "textRuns", publicName: "textRuns", 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
1074
  <canvas #canvas [style.width.%]="100" [style.height.%]="100"></canvas>
539
1075
  `, isInline: true, styles: [":host{display:block;width:100%;height:100%}canvas{display:block}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush });
540
1076
  }
@@ -543,7 +1079,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
543
1079
  args: [{ selector: 'rive-canvas', standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: `
544
1080
  <canvas #canvas [style.width.%]="100" [style.height.%]="100"></canvas>
545
1081
  `, 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"] }] } });
1082
+ }], 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 }] }], textRuns: [{ type: i0.Input, args: [{ isSignal: true, alias: "textRuns", 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
1083
 
548
1084
  /**
549
1085
  * Service for preloading and caching Rive files.
@@ -576,7 +1112,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
576
1112
  class RiveFileService {
577
1113
  cache = new Map();
578
1114
  pendingLoads = new Map();
1115
+ bufferIdMap = new WeakMap();
579
1116
  bufferIdCounter = 0;
1117
+ // Optional debug configuration
1118
+ globalDebugConfig = inject(RIVE_DEBUG_CONFIG, {
1119
+ optional: true,
1120
+ });
580
1121
  /**
581
1122
  * Load a RiveFile from URL or ArrayBuffer.
582
1123
  * Returns a signal with the file state and loading status.
@@ -588,15 +1129,20 @@ class RiveFileService {
588
1129
  */
589
1130
  loadFile(params) {
590
1131
  const cacheKey = this.getCacheKey(params);
1132
+ // Initialize logger for this request
1133
+ const logger = new RiveLogger(this.globalDebugConfig, params.debug);
1134
+ logger.debug(`RiveFileService: Request to load file`, { cacheKey });
591
1135
  // Return cached entry if exists
592
1136
  const cached = this.cache.get(cacheKey);
593
1137
  if (cached) {
594
1138
  cached.refCount++;
1139
+ logger.debug(`RiveFileService: Cache hit for ${cacheKey}`);
595
1140
  return cached.state;
596
1141
  }
597
1142
  // Return pending load if already in progress
598
1143
  const pending = this.pendingLoads.get(cacheKey);
599
1144
  if (pending) {
1145
+ logger.debug(`RiveFileService: Reuse pending load for ${cacheKey}`);
600
1146
  return pending.stateSignal.asReadonly();
601
1147
  }
602
1148
  // Create new loading state
@@ -605,7 +1151,7 @@ class RiveFileService {
605
1151
  status: 'loading',
606
1152
  }, ...(ngDevMode ? [{ debugName: "stateSignal" }] : []));
607
1153
  // Start loading and track as pending
608
- const promise = this.loadRiveFile(params, stateSignal, cacheKey);
1154
+ const promise = this.loadRiveFile(params, stateSignal, cacheKey, logger);
609
1155
  this.pendingLoads.set(cacheKey, { stateSignal, promise });
610
1156
  return stateSignal.asReadonly();
611
1157
  }
@@ -631,9 +1177,18 @@ class RiveFileService {
631
1177
  }
632
1178
  }
633
1179
  /**
634
- * Clear all cached files
1180
+ * Clear all cached files and abort pending loads
635
1181
  */
636
1182
  clearCache() {
1183
+ // Clear pending loads first to prevent them from populating the cache
1184
+ this.pendingLoads.forEach((pending) => {
1185
+ pending.stateSignal.set({
1186
+ riveFile: null,
1187
+ status: 'failed',
1188
+ });
1189
+ });
1190
+ this.pendingLoads.clear();
1191
+ // Clean up cached files
637
1192
  this.cache.forEach((entry) => {
638
1193
  try {
639
1194
  entry.file.cleanup();
@@ -652,67 +1207,73 @@ class RiveFileService {
652
1207
  return `src:${params.src}`;
653
1208
  }
654
1209
  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;
1210
+ // For buffers, use WeakMap to track unique IDs without mutating the buffer
1211
+ let bufferId = this.bufferIdMap.get(params.buffer);
1212
+ if (bufferId === undefined) {
1213
+ bufferId = ++this.bufferIdCounter;
1214
+ this.bufferIdMap.set(params.buffer, bufferId);
660
1215
  }
661
- return `buffer:${bufferWithId.__riveBufferId}`;
1216
+ return `buffer:${bufferId}`;
662
1217
  }
663
1218
  return 'unknown';
664
1219
  }
665
1220
  /**
666
- * Load RiveFile and update state signal
1221
+ * Load RiveFile and update state signal.
1222
+ * Addresses race condition by setting up listeners BEFORE init.
667
1223
  */
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();
1224
+ async loadRiveFile(params, stateSignal, cacheKey, logger) {
1225
+ // Guard to ensure pending load is cleaned up exactly once
1226
+ let pendingCleanupDone = false;
1227
+ const finalizePendingLoadOnce = () => {
1228
+ if (!pendingCleanupDone) {
1229
+ this.pendingLoads.delete(cacheKey);
1230
+ pendingCleanupDone = true;
1231
+ }
1232
+ };
1233
+ try {
1234
+ // Extract debug parameter - it's not part of RiveFile SDK API
1235
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
1236
+ const { debug, ...sdkParams } = params;
1237
+ const file = new RiveFile(sdkParams);
1238
+ // Listeners must be attached BEFORE calling init() to avoid race conditions
1239
+ // where init() completes or fails synchronously/immediately.
1240
+ file.on(EventType.Load, () => {
1241
+ logger.debug(`RiveFileService: File loaded successfully`, { cacheKey });
1242
+ // Request an instance to increment reference count
1243
+ // This prevents the file from being destroyed while in use
1244
+ file.getInstance();
1245
+ stateSignal.set({
1246
+ riveFile: file,
1247
+ status: 'success',
690
1248
  });
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();
1249
+ // Cache the successfully loaded file
1250
+ this.cache.set(cacheKey, {
1251
+ file,
1252
+ state: stateSignal.asReadonly(),
1253
+ refCount: 1,
702
1254
  });
703
- }
704
- catch (error) {
705
- console.error('Failed to load RiveFile:', error);
1255
+ finalizePendingLoadOnce();
1256
+ });
1257
+ file.on(EventType.LoadError, () => {
1258
+ logger.warn(`RiveFileService: Failed to load file`, { cacheKey });
706
1259
  stateSignal.set({
707
1260
  riveFile: null,
708
1261
  status: 'failed',
709
1262
  });
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
- });
1263
+ finalizePendingLoadOnce();
1264
+ });
1265
+ logger.debug(`RiveFileService: Initializing file`, { cacheKey });
1266
+ // Await init() to catch initialization errors (e.g. WASM issues)
1267
+ await file.init();
1268
+ }
1269
+ catch (error) {
1270
+ logger.error('RiveFileService: Unexpected error loading file', error);
1271
+ stateSignal.set({
1272
+ riveFile: null,
1273
+ status: 'failed',
1274
+ });
1275
+ finalizePendingLoadOnce();
1276
+ }
716
1277
  }
717
1278
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
718
1279
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.1.4", ngImport: i0, type: RiveFileService, providedIn: 'root' });
@@ -733,5 +1294,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.4", ngImpor
733
1294
  * Generated bundle index. Do not edit.
734
1295
  */
735
1296
 
736
- export { RiveCanvasComponent, RiveFileService, RiveLoadError };
1297
+ export { RiveCanvasComponent, RiveErrorCode, RiveFileService, RiveLoadError, RiveValidationError, provideRiveDebug };
737
1298
  //# sourceMappingURL=grandgular-rive-angular.mjs.map