facebetter 2.0.0 → 2.0.1

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.
@@ -5,58 +5,33 @@
5
5
 
6
6
  /**
7
7
  * Engine configuration class
8
- * Analogous to EngineConfig in C++ / Java / Objective-C, with Web-only auth fields.
8
+ * Analogous to EngineConfig in C++ / Java / Objective-C. Web only accepts licenseToken.
9
9
  */
10
10
  class EngineConfig {
11
11
  /**
12
12
  * @param {Object} config
13
- * @param {string} [config.licenseToken] Compact JWS, or the raw `{success, token}`
13
+ * @param {string} config.licenseToken Compact JWS, or the raw `{success, token}`
14
14
  * HTTP response body from your auth server.
15
- * @param {string} [config.authProxyUrl] Production: forward the WASM auth
16
- * challenge to your server, which holds app_id/app_key and calls Cloudflare.
17
- * @param {Function} [config.fetchAuthResponse] Custom auth hook with signature
18
- * `async (challenge) => string|object`. For local debugging you can use
19
- * `createDirectAuthFetcher` to hit Cloudflare directly — never ship keys in
20
- * a production frontend bundle.
21
15
  * @param {boolean} [config.externalContext] Kept for cross-platform alignment;
22
- * unused on Web/WASM today.
16
+ * unused on Web today.
23
17
  */
24
18
  constructor(config = {}) {
25
19
  this.licenseToken = config.licenseToken || null;
26
- this.authProxyUrl = config.authProxyUrl || null;
27
- this.fetchAuthResponse = config.fetchAuthResponse || null;
28
20
  this.resourcePath = '/resource.fbd';
29
21
  /**
30
22
  * Whether to use an external GL context (native platforms only).
31
- * On Web/WASM this field is kept for config-shape alignment and has no effect.
23
+ * On Web this field is kept for config-shape alignment and has no effect.
32
24
  */
33
25
  this.externalContext = !!config.externalContext;
34
26
  }
35
27
 
36
28
  isValid() {
37
- if (this.licenseToken && typeof this.licenseToken === 'string' &&
38
- this.licenseToken.trim() !== '') {
39
- return true;
40
- }
41
- if (typeof this.fetchAuthResponse === 'function') {
42
- return true;
43
- }
44
- if (this.authProxyUrl && typeof this.authProxyUrl === 'string' &&
45
- this.authProxyUrl.trim() !== '') {
46
- return true;
47
- }
48
- return false;
29
+ return typeof this.licenseToken === 'string' &&
30
+ this.licenseToken.trim() !== '';
49
31
  }
50
32
 
51
33
  toString() {
52
- const mode = this.licenseToken
53
- ? 'licenseToken'
54
- : this.fetchAuthResponse
55
- ? 'fetchAuthResponse'
56
- : this.authProxyUrl
57
- ? 'authProxyUrl'
58
- : 'invalid';
59
- return `EngineConfig{mode='${mode}'}`;
34
+ return `EngineConfig{mode='${this.isValid() ? 'licenseToken' : 'invalid'}'}`;
60
35
  }
61
36
  }
62
37
 
@@ -306,40 +281,6 @@ var constants = /*#__PURE__*/Object.freeze({
306
281
  WhiteningStyle: WhiteningStyle$1
307
282
  });
308
283
 
309
- /**
310
- * Browser-side console lines aligned with C++ Logger (src/base/logging.cc) Release pattern:
311
- * [%Y-%m-%d %H:%M:%S.%e] [facebetter] [%l] [%t] - %v
312
- * spdlog level names: info, warning, error (see spdlog SPDLOG_LEVEL_NAMES).
313
- */
314
-
315
- function pad2(n) {
316
- return String(n).padStart(2, '0');
317
- }
318
-
319
- /**
320
- * @param {'info'|'warning'|'error'} levelTag — must match spdlog long level strings
321
- * @param {string} message
322
- * @returns {string}
323
- */
324
- function formatLogLine(levelTag, message) {
325
- const d = new Date();
326
- const ms = String(d.getMilliseconds()).padStart(3, '0');
327
- const ts = `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(
328
- d.getHours()
329
- )}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${ms}`;
330
- return `[${ts}] [facebetter] [${levelTag}] [0] - ${message}`;
331
- }
332
-
333
- /** @param {string} message */
334
- function logInfo(message) {
335
- console.info(formatLogLine('info', message));
336
- }
337
-
338
- /** @param {string} message */
339
- function logWarn(message) {
340
- console.warn(formatLogLine('warning', message));
341
- }
342
-
343
284
  /**
344
285
  * Beauty Effect Engine Core
345
286
  * Platform-agnostic engine implementation with dependency injection
@@ -404,8 +345,6 @@ class BeautyEffectEngine {
404
345
 
405
346
  this.config = engineConfig;
406
347
  this.licenseToken = engineConfig.licenseToken;
407
- this.authProxyUrl = engineConfig.authProxyUrl;
408
- this.fetchAuthResponse = engineConfig.fetchAuthResponse;
409
348
  this.resourcePath = '/resource.fbd';
410
349
  this.enginePtr = null;
411
350
  this.initialized = false;
@@ -425,7 +364,8 @@ class BeautyEffectEngine {
425
364
  this._toImageData = platformAPI.toImageData;
426
365
  this._ensureFacebetterCanvas = platformAPI.ensureFacebetterCanvas;
427
366
  this._cleanupFacebetterCanvas = platformAPI.cleanupFacebetterCanvas;
428
-
367
+ this._pendingLogConfig = null;
368
+
429
369
  // Browser-specific state
430
370
  this._facebetterCanvas = null;
431
371
  this._createdFacebetterCanvas = false;
@@ -436,9 +376,8 @@ class BeautyEffectEngine {
436
376
  if (this._ensureFacebetterCanvas) {
437
377
  this._ensureFacebetterCanvas();
438
378
  }
439
-
440
- // Start loading WASM module immediately in constructor
441
- this._wasmLoadPromise = this._loadWasmModule();
379
+
380
+ this._wasmLoadPromise = null;
442
381
  this._initPromise = null;
443
382
  }
444
383
 
@@ -495,8 +434,12 @@ class BeautyEffectEngine {
495
434
  /**
496
435
  * Initializes the engine
497
436
  * @param {Object} [options] - Initialization options
498
- * @param {number} [options.timeout] - Timeout in milliseconds (default: 30000 for WASM, 10000 for auth)
499
- * @param {number} [options.authTimeout] - Timeout for online authentication in milliseconds (default: 10000)
437
+ * @param {number} [options.timeout] - Timeout in milliseconds (default: 120000)
438
+ * @param {number} [options.authTimeout] - Unused. Kept for compatibility.
439
+ * @param {function} [options.onProgress] - Download progress:
440
+ * `{ loaded, total, percent }` while fetching the runtime files.
441
+ * @param {string} [options.assetBaseUrl] - Directory that contains
442
+ * `facebetter-core.wasm` and `resource.fbd`. Optional when using npm.
500
443
  * @returns {Promise<void>} Promise that resolves when initialization is complete
501
444
  */
502
445
  async init(options = {}) {
@@ -513,9 +456,18 @@ class BeautyEffectEngine {
513
456
  // Create initialization Promise
514
457
  this._initPromise = (async () => {
515
458
  try {
516
- const wasmTimeout = options.timeout || 30000;
459
+ const wasmTimeout = options.timeout || 120000;
460
+
461
+ if (!this._wasmLoadPromise) {
462
+ this._wasmLoadPromise = this._loadWasmModule({
463
+ onProgress: options.onProgress,
464
+ assetBaseUrl: options.assetBaseUrl,
465
+ wasmUrl: options.wasmUrl,
466
+ wasmAssetUrl: options.wasmAssetUrl,
467
+ resourceAssetUrl: options.resourceAssetUrl,
468
+ });
469
+ }
517
470
 
518
- // Wait for WASM module loading (with timeout)
519
471
  try {
520
472
  await Promise.race([
521
473
  this._wasmLoadPromise,
@@ -525,6 +477,11 @@ class BeautyEffectEngine {
525
477
  throw this._enhanceError(error, 'Failed to load WASM module');
526
478
  }
527
479
 
480
+ if (this._pendingLogConfig) {
481
+ await this._applyLogConfig(this._pendingLogConfig);
482
+ this._pendingLogConfig = null;
483
+ }
484
+
528
485
  const Module = this._getWasmModule();
529
486
 
530
487
  // Setup usage report proxy for WASM
@@ -550,66 +507,12 @@ class BeautyEffectEngine {
550
507
  };
551
508
  }
552
509
 
553
- // Web v2 auth: never accept appId/appKey in the browser.
554
- // - licenseToken: pre-fetched JWS or `{success, token}` body
555
- // - authProxyUrl: production — forward the challenge to your server
556
- // - fetchAuthResponse: custom hook; locally use createDirectAuthFetcher
510
+ // Web: fetch the token outside the engine, then pass licenseToken.
557
511
  if (!this.licenseToken) {
558
- if (typeof this.fetchAuthResponse === 'function') {
559
- Module.onOnlineAuth = async (payloadJson) => {
560
- try {
561
- const challenge = JSON.parse(payloadJson);
562
- const result = await this.fetchAuthResponse(challenge);
563
- if (result && typeof result === 'object' && typeof result.response === 'string') {
564
- return { response: result.response };
565
- }
566
- if (typeof result === 'string') {
567
- return result;
568
- }
569
- if (result && typeof result === 'object') {
570
- return JSON.stringify(result);
571
- }
572
- return '';
573
- } catch (err) {
574
- logWarn(
575
- `Online license: fetchAuthResponse error${err?.message ? `: ${err.message}` : ''}`
576
- );
577
- return JSON.stringify({
578
- success: false,
579
- error: 'auth_fetch_failed',
580
- message: err?.message || 'auth fetch failed',
581
- });
582
- }
583
- };
584
- } else if (this.authProxyUrl) {
585
- Module.onOnlineAuth = async (payloadJson) => {
586
- logInfo('Online license: proxying auth challenge');
587
- try {
588
- const response = await fetch(this.authProxyUrl, {
589
- method: 'POST',
590
- headers: { 'Content-Type': 'application/json' },
591
- body: payloadJson,
592
- });
593
- if (!response.ok) {
594
- logWarn(`Online license: proxy HTTP ${response.status}`);
595
- return '';
596
- }
597
- const text = await response.text();
598
- logInfo('Online license: proxy response ok');
599
- return text;
600
- } catch (err) {
601
- logWarn(
602
- `Online license: proxy error${err?.message ? `: ${err.message}` : ''}`
603
- );
604
- return '';
605
- }
606
- };
607
- } else {
608
- throw new FacebetterError(
609
- 'Web auth requires licenseToken, authProxyUrl, or fetchAuthResponse.',
610
- 'LICENSE_ERROR'
611
- );
612
- }
512
+ throw new FacebetterError(
513
+ 'Web auth requires licenseToken. Fetch it from your server before init().',
514
+ 'LICENSE_ERROR'
515
+ );
613
516
  }
614
517
 
615
518
  // C ABI still has app_id/app_key slots; leave them empty on Web.
@@ -653,7 +556,8 @@ class BeautyEffectEngine {
653
556
  this.initialized = true;
654
557
  this._initPromise = null; // Clear Promise cache, allow re-initialization (if needed)
655
558
  } catch (error) {
656
- this._initPromise = null; // Clear Promise cache, allow retry
559
+ this._initPromise = null;
560
+ this._wasmLoadPromise = null;
657
561
  throw error;
658
562
  }
659
563
  })();
@@ -710,8 +614,9 @@ class BeautyEffectEngine {
710
614
  }
711
615
 
712
616
  /**
713
- * Sets log configuration
714
- * Can be called before init() since fb_set_log_config is a global function
617
+ * Sets log configuration.
618
+ * If called before `init()`, the values are applied after the runtime downloads
619
+ * and before the engine is created.
715
620
  * @param {Object} config - Log configuration
716
621
  * @param {boolean} config.consoleEnabled - Enable console logging
717
622
  * @param {boolean} config.fileEnabled - Enable file logging
@@ -720,8 +625,15 @@ class BeautyEffectEngine {
720
625
  * @returns {Promise<void>} Promise that resolves when log config is set
721
626
  */
722
627
  async setLogConfig(config) {
723
- // Wait for WASM module to be loaded (started in constructor)
628
+ if (!this._wasmLoadPromise) {
629
+ this._pendingLogConfig = config;
630
+ return;
631
+ }
724
632
  await this._wasmLoadPromise;
633
+ await this._applyLogConfig(config);
634
+ }
635
+
636
+ async _applyLogConfig(config) {
725
637
  const Module = this._getWasmModule();
726
638
 
727
639
  const fileNamePtr = allocUtf8(Module, config.fileName);
@@ -1626,100 +1538,204 @@ class BeautyEffectEngine {
1626
1538
 
1627
1539
  }
1628
1540
 
1629
- /** Cloudflare v2 online auth URL. Proxy through your server in production; never ship keys in the frontend bundle. */
1630
- const FACEBETTER_AUTH_URL =
1631
- 'https://facebetter.pixpark.net/facebetter/v2/auth';
1541
+ const WASM_FILE = 'facebetter-core.wasm';
1542
+ const RESOURCE_FILE = 'resource.fbd';
1543
+ const MEMFS_RESOURCE_PATH = '/resource.fbd';
1632
1544
 
1633
- function toHex(buffer) {
1634
- return Array.from(new Uint8Array(buffer), (b) =>
1635
- b.toString(16).padStart(2, '0')
1636
- ).join('');
1545
+ function emitProgress(onProgress, loaded, total) {
1546
+ if (typeof onProgress !== 'function') {
1547
+ return;
1548
+ }
1549
+ const safeTotal = total > 0 ? total : loaded;
1550
+ const percent =
1551
+ safeTotal > 0 ? Math.min(100, Math.round((loaded / safeTotal) * 100)) : 0;
1552
+ onProgress({ loaded, total: safeTotal, percent });
1637
1553
  }
1638
1554
 
1639
- async function hmacSha256Hex(key, message) {
1640
- const encoder = new TextEncoder();
1641
- const cryptoKey = await crypto.subtle.importKey(
1642
- 'raw',
1643
- encoder.encode(key),
1644
- { name: 'HMAC', hash: 'SHA-256' },
1645
- false,
1646
- ['sign']
1647
- );
1648
- const signature = await crypto.subtle.sign(
1649
- 'HMAC',
1650
- cryptoKey,
1651
- encoder.encode(message)
1652
- );
1653
- return toHex(signature);
1555
+ async function readFileUrl(url, onChunk) {
1556
+ const { readFile } = await import('node:fs/promises');
1557
+ const { fileURLToPath } = await import('node:url');
1558
+ const buf = await readFile(fileURLToPath(url));
1559
+ const bytes = new Uint8Array(buf);
1560
+ onChunk(bytes.byteLength, bytes.byteLength);
1561
+ return bytes;
1654
1562
  }
1655
1563
 
1656
1564
  /**
1657
- * Call Cloudflare v2/auth directly with app_id / app_key.
1658
- * Intended for local debugging, or environments where key leakage is not a risk.
1659
- *
1660
- * @param {Object} options
1661
- * @param {string} options.appId
1662
- * @param {string} options.appKey
1663
- * @param {Object} options.challenge WASM challenge `{nonce, timestamp, platform, user_agent}`
1664
- * @param {string} [options.authUrl]
1665
- * @returns {Promise<string>} Raw HTTP body (`{success, token}` or an error envelope)
1565
+ * Fetch a binary with byte-level download progress.
1566
+ * @param {string} url
1567
+ * @param {function(number, number): void} onChunk loaded, total (0 if unknown)
1568
+ * @returns {Promise<Uint8Array>}
1666
1569
  */
1667
- async function requestFacebetterAuth({
1668
- appId,
1669
- appKey,
1670
- challenge,
1671
- authUrl = FACEBETTER_AUTH_URL,
1672
- }) {
1673
- const id = typeof appId === 'string' ? appId.trim() : '';
1674
- const key = typeof appKey === 'string' ? appKey.trim() : '';
1675
- if (!id || !key) {
1676
- throw new FacebetterError(
1677
- 'requestFacebetterAuth requires appId and appKey',
1678
- 'LICENSE_ERROR'
1679
- );
1570
+ async function fetchBinary(url, onChunk) {
1571
+ if (typeof url === 'string' && url.startsWith('file:')) {
1572
+ return readFileUrl(url, onChunk);
1680
1573
  }
1681
- if (!challenge || typeof challenge !== 'object') {
1574
+
1575
+ const response = await fetch(url);
1576
+ if (!response.ok) {
1682
1577
  throw new FacebetterError(
1683
- 'requestFacebetterAuth requires a WASM auth challenge',
1684
- 'LICENSE_ERROR'
1578
+ `Failed to download ${url} (${response.status})`,
1579
+ 'WASM_LOAD_ERROR'
1685
1580
  );
1686
1581
  }
1687
1582
 
1688
- const nonce = String(challenge.nonce || '');
1689
- const timestamp = Number(challenge.timestamp);
1690
- const platform = String(challenge.platform || 'web');
1691
- const userAgent = String(challenge.user_agent || '');
1692
- const payload = `v2|${id}|${timestamp}|${nonce}|${platform}`;
1693
- const hmac = await hmacSha256Hex(key, payload);
1694
-
1695
- const response = await fetch(authUrl, {
1696
- method: 'POST',
1697
- headers: { 'Content-Type': 'application/json' },
1698
- body: JSON.stringify({
1699
- app_id: id,
1700
- hmac_signature: hmac,
1701
- timestamp,
1702
- nonce,
1703
- platform,
1704
- user_agent: userAgent,
1583
+ const totalHeader = Number(response.headers.get('content-length'));
1584
+ const total = Number.isFinite(totalHeader) && totalHeader > 0 ? totalHeader : 0;
1585
+
1586
+ if (!response.body || typeof response.body.getReader !== 'function') {
1587
+ const buffer = await response.arrayBuffer();
1588
+ const bytes = new Uint8Array(buffer);
1589
+ onChunk(bytes.byteLength, total || bytes.byteLength);
1590
+ return bytes;
1591
+ }
1592
+
1593
+ const reader = response.body.getReader();
1594
+ const chunks = [];
1595
+ let loaded = 0;
1596
+ while (true) {
1597
+ const { done, value } = await reader.read();
1598
+ if (done) {
1599
+ break;
1600
+ }
1601
+ chunks.push(value);
1602
+ loaded += value.byteLength;
1603
+ onChunk(loaded, total);
1604
+ }
1605
+
1606
+ const bytes = new Uint8Array(loaded);
1607
+ let offset = 0;
1608
+ for (const chunk of chunks) {
1609
+ bytes.set(chunk, offset);
1610
+ offset += chunk.byteLength;
1611
+ }
1612
+ onChunk(loaded, total || loaded);
1613
+ return bytes;
1614
+ }
1615
+
1616
+ async function fetchRuntimeAssets({ wasmUrl, resourceUrl, onProgress }) {
1617
+ let wasmLoaded = 0;
1618
+ let wasmTotal = 0;
1619
+ let resourceLoaded = 0;
1620
+ let resourceTotal = 0;
1621
+
1622
+ const emit = () => {
1623
+ emitProgress(
1624
+ onProgress,
1625
+ wasmLoaded + resourceLoaded,
1626
+ wasmTotal + resourceTotal
1627
+ );
1628
+ };
1629
+
1630
+ const [wasmBytes, resourceBytes] = await Promise.all([
1631
+ fetchBinary(wasmUrl, (loaded, total) => {
1632
+ wasmLoaded = loaded;
1633
+ wasmTotal = total || loaded;
1634
+ emit();
1705
1635
  }),
1706
- });
1707
- return response.text();
1636
+ fetchBinary(resourceUrl, (loaded, total) => {
1637
+ resourceLoaded = loaded;
1638
+ resourceTotal = total || loaded;
1639
+ emit();
1640
+ }),
1641
+ ]);
1642
+
1643
+ emitProgress(
1644
+ onProgress,
1645
+ wasmBytes.byteLength + resourceBytes.byteLength,
1646
+ wasmBytes.byteLength + resourceBytes.byteLength
1647
+ );
1648
+ return { wasmBytes, resourceBytes };
1649
+ }
1650
+
1651
+ function scriptDirectory() {
1652
+ if (typeof document === 'undefined') {
1653
+ return '';
1654
+ }
1655
+ if (document.currentScript?.src) {
1656
+ return document.currentScript.src.slice(
1657
+ 0,
1658
+ document.currentScript.src.lastIndexOf('/') + 1
1659
+ );
1660
+ }
1661
+ const scripts = document.getElementsByTagName('script');
1662
+ for (let i = scripts.length - 1; i >= 0; i--) {
1663
+ const src = scripts[i].src;
1664
+ if (src && (src.includes('facebetter.js') || src.includes('facebetter'))) {
1665
+ return src.slice(0, src.lastIndexOf('/') + 1);
1666
+ }
1667
+ }
1668
+ if (typeof location !== 'undefined') {
1669
+ return new URL('.', location.href).href;
1670
+ }
1671
+ return './';
1708
1672
  }
1709
1673
 
1710
1674
  /**
1711
- * Build an `EngineConfig.fetchAuthResponse` that talks to Cloudflare directly
1712
- * (no customer proxy).
1713
- *
1714
- * @param {Object} options
1715
- * @param {string} options.appId
1716
- * @param {string} options.appKey
1717
- * @param {string} [options.authUrl]
1718
- * @returns {(challenge: Object) => Promise<string>}
1675
+ * Resolve sidecar wasm / resource URLs.
1676
+ * Prefer `assetBaseUrl`, then `facebetter-core/assets`, then the script directory.
1719
1677
  */
1720
- function createDirectAuthFetcher({ appId, appKey, authUrl } = {}) {
1721
- return (challenge) =>
1722
- requestFacebetterAuth({ appId, appKey, challenge, authUrl });
1678
+ async function resolveRuntimeAssetUrls(options = {}) {
1679
+ const joinBase = (base) => {
1680
+ const prefix = base.endsWith('/') ? base : `${base}/`;
1681
+ return {
1682
+ wasmUrl: options.wasmAssetUrl || `${prefix}${WASM_FILE}`,
1683
+ resourceUrl: options.resourceAssetUrl || `${prefix}${RESOURCE_FILE}`,
1684
+ };
1685
+ };
1686
+
1687
+ if (options.wasmAssetUrl && options.resourceAssetUrl) {
1688
+ return {
1689
+ wasmUrl: options.wasmAssetUrl,
1690
+ resourceUrl: options.resourceAssetUrl,
1691
+ };
1692
+ }
1693
+ if (options.assetBaseUrl) {
1694
+ return joinBase(options.assetBaseUrl);
1695
+ }
1696
+ if (typeof options.wasmUrl === 'string' && /^(https?:|file:|\/)/.test(options.wasmUrl)) {
1697
+ return joinBase(options.wasmUrl.slice(0, options.wasmUrl.lastIndexOf('/') + 1));
1698
+ }
1699
+
1700
+ try {
1701
+ const assets = await import('facebetter-core/assets');
1702
+ return {
1703
+ wasmUrl: options.wasmAssetUrl || assets.wasmUrl,
1704
+ resourceUrl: options.resourceAssetUrl || assets.resourceUrl,
1705
+ };
1706
+ } catch {
1707
+ return joinBase(scriptDirectory());
1708
+ }
1709
+ }
1710
+
1711
+ /**
1712
+ * Instantiate the Emscripten factory using already-downloaded bytes.
1713
+ */
1714
+ async function instantiateRuntime(factory, wasmBytes, resourceBytes) {
1715
+ if (!factory) {
1716
+ throw new FacebetterError(
1717
+ 'WASM module does not export createFaceBetterModule',
1718
+ 'WASM_LOAD_ERROR'
1719
+ );
1720
+ }
1721
+
1722
+ const module = await factory({
1723
+ instantiateWasm(imports, receiveInstance) {
1724
+ return WebAssembly.instantiate(wasmBytes, imports).then((result) => {
1725
+ receiveInstance(result.instance, result.module);
1726
+ });
1727
+ },
1728
+ });
1729
+
1730
+ if (!module) {
1731
+ throw new FacebetterError('Failed to initialize WASM module', 'WASM_LOAD_ERROR');
1732
+ }
1733
+ if (!module.FS) {
1734
+ throw new FacebetterError('Runtime filesystem is not available', 'WASM_LOAD_ERROR');
1735
+ }
1736
+ module.FS.writeFile(MEMFS_RESOURCE_PATH, resourceBytes);
1737
+ module.ready = true;
1738
+ return module;
1723
1739
  }
1724
1740
 
1725
1741
  /**
@@ -1778,21 +1794,19 @@ function toImageData(source, width, height) {
1778
1794
  */
1779
1795
 
1780
1796
 
1781
- // ESM-specific WASM loader
1782
1797
  let wasmModuleInstance = null;
1783
1798
  let wasmModulePromise = null;
1784
1799
 
1785
1800
  /**
1786
- * Loads the WebAssembly module (ESM version)
1787
- * Supports ESM format WASM modules (generated with MODULARIZE and EXPORT_ES6)
1788
- * With SINGLE_FILE=1, WASM binary and data files are embedded in the JS file
1789
- * @param {Object} [options] - Options
1790
- * @param {string} [options.wasmUrl] - Custom WASM module URL (defaults to facebetter-core npm package)
1791
- * @param {Function} [options.locateFile] - Custom locateFile function (usually not needed with SINGLE_FILE=1)
1792
- * @returns {Promise<Object>} Promise that resolves with the WASM module
1801
+ * Load the runtime factory, then download .wasm and resource.fbd with progress.
1802
+ * @param {Object} [options]
1803
+ * @param {function} [options.onProgress]
1804
+ * @param {string} [options.assetBaseUrl]
1805
+ * @param {string} [options.wasmUrl] Glue JS URL (optional)
1793
1806
  */
1794
1807
  async function loadWasmModule(options = {}) {
1795
1808
  if (wasmModuleInstance) {
1809
+ options.onProgress?.({ loaded: 1, total: 1, percent: 100 });
1796
1810
  return wasmModuleInstance;
1797
1811
  }
1798
1812
 
@@ -1801,53 +1815,32 @@ async function loadWasmModule(options = {}) {
1801
1815
  }
1802
1816
 
1803
1817
  wasmModulePromise = (async () => {
1804
- // Try to import the WASM module factory
1805
- // facebetter-core.js is an Emscripten-generated ESM file (with MODULARIZE and EXPORT_ES6)
1806
- // It's provided by the facebetter-core npm package
1807
- let FaceBetterModuleFactory;
1808
-
1809
1818
  try {
1810
- // Get the WASM module
1819
+ let factory;
1811
1820
  if (options.wasmUrl) {
1812
- // If custom URL is provided, use it (for backward compatibility or custom builds)
1813
- const wasmModule = await import(options.wasmUrl);
1814
- FaceBetterModuleFactory = wasmModule.default || wasmModule.createFaceBetterModule;
1821
+ const wasmModule = await import(/* @vite-ignore */ options.wasmUrl);
1822
+ factory = wasmModule.default || wasmModule.createFaceBetterModule;
1815
1823
  } else {
1816
- // Import from facebetter-core npm package
1817
1824
  const wasmModule = await import('facebetter-core');
1818
-
1819
- // Get the factory function (default export or named export)
1820
- FaceBetterModuleFactory = wasmModule.default || wasmModule.createFaceBetterModule;
1821
- }
1822
-
1823
- if (!FaceBetterModuleFactory) {
1824
- throw new Error('WASM module does not export createFaceBetterModule function');
1825
+ factory = wasmModule.default || wasmModule.createFaceBetterModule;
1825
1826
  }
1826
- } catch (e) {
1827
- throw new FacebetterError(`Failed to load WASM module: ${e.message}`);
1828
- }
1829
-
1830
- // Configure module options
1831
- // With SINGLE_FILE=1, WASM binary and data files are embedded in the JS file
1832
- // No need to handle .wasm and .data file paths separately
1833
- const moduleOptions = {
1834
- locateFile: options.locateFile || function(path, prefix) {
1835
- // With SINGLE_FILE=1, Emscripten handles embedded files automatically
1836
- // Custom locateFile is only needed if user provides one
1837
- return prefix + path;
1838
- },
1839
- ...options
1840
- };
1841
-
1842
- // Initialize the module (MODULARIZE returns a Promise)
1843
- wasmModuleInstance = await FaceBetterModuleFactory(moduleOptions);
1844
-
1845
- if (!wasmModuleInstance) {
1846
- throw new FacebetterError('Failed to initialize WASM module');
1827
+ const urls = await resolveRuntimeAssetUrls(options);
1828
+ const { wasmBytes, resourceBytes } = await fetchRuntimeAssets({
1829
+ ...urls,
1830
+ onProgress: options.onProgress,
1831
+ });
1832
+ wasmModuleInstance = await instantiateRuntime(
1833
+ factory,
1834
+ wasmBytes,
1835
+ resourceBytes
1836
+ );
1837
+ return wasmModuleInstance;
1838
+ } catch (error) {
1839
+ wasmModulePromise = null;
1840
+ throw error instanceof FacebetterError
1841
+ ? error
1842
+ : new FacebetterError(`Failed to load WASM module: ${error.message}`, 'WASM_LOAD_ERROR');
1847
1843
  }
1848
-
1849
- wasmModuleInstance.ready = true;
1850
- return wasmModuleInstance;
1851
1844
  })();
1852
1845
 
1853
1846
  return wasmModulePromise;
@@ -1908,7 +1901,7 @@ function cleanupFacebetterCanvas() {
1908
1901
 
1909
1902
  // Create platform API
1910
1903
  const platformAPI = {
1911
- loadWasmModule: () => loadWasmModule(),
1904
+ loadWasmModule: (options) => loadWasmModule(options),
1912
1905
  getWasmModule,
1913
1906
  getWasmBuffer,
1914
1907
  toImageData: toImageData,
@@ -1957,12 +1950,9 @@ var index = {
1957
1950
  BeautyEffectEngine: ESMBeautyEffectEngine,
1958
1951
  EngineConfig,
1959
1952
  FacebetterError,
1960
- FACEBETTER_AUTH_URL,
1961
- createDirectAuthFetcher,
1962
- requestFacebetterAuth,
1963
1953
  ...constants,
1964
1954
  loadWasmModule
1965
1955
  };
1966
1956
 
1967
- export { ESMBeautyEffectEngine as BeautyEffectEngine, BlushColor, BlushStyle, ChromaKeyColor, ContourStyle, EngineConfig, EngineEventCode, EyeLinerColor, EyeLinerStyle, EyeShadowColor, EyeShadowStyle, EyebrowColor, EyebrowStyle, EyelashColor, EyelashStyle, FACEBETTER_AUTH_URL, FacebetterError, FrameType, LipstickColor, MirrorMode, PupilColor, Reshape, SmoothingStyle, WhiteningStyle, createBeautyEffectEngine, createDirectAuthFetcher, index as default, getWasmBuffer, getWasmModule, loadWasmModule, requestFacebetterAuth };
1957
+ export { ESMBeautyEffectEngine as BeautyEffectEngine, BlushColor, BlushStyle, ChromaKeyColor, ContourStyle, EngineConfig, EngineEventCode, EyeLinerColor, EyeLinerStyle, EyeShadowColor, EyeShadowStyle, EyebrowColor, EyebrowStyle, EyelashColor, EyelashStyle, FacebetterError, FrameType, LipstickColor, MirrorMode, PupilColor, Reshape, SmoothingStyle, WhiteningStyle, createBeautyEffectEngine, index as default, getWasmBuffer, getWasmModule, loadWasmModule };
1968
1958
  //# sourceMappingURL=facebetter.esm.js.map