facebetter 1.0.8 → 1.0.10

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.
@@ -230,60 +230,157 @@ class BeautyEffectEngine {
230
230
 
231
231
  // Start loading WASM module immediately in constructor
232
232
  this._wasmLoadPromise = this._loadWasmModule();
233
+ this._initPromise = null;
234
+ }
235
+
236
+ /**
237
+ * Creates a timeout promise
238
+ * @private
239
+ * @param {number} timeout - Timeout in milliseconds
240
+ * @param {string} operation - Operation name for error message
241
+ * @returns {Promise} Promise that rejects after timeout
242
+ */
243
+ _createTimeout(timeout, operation) {
244
+ return new Promise((_, reject) => {
245
+ setTimeout(() => {
246
+ reject(new FacebetterError(
247
+ `${operation} timed out after ${timeout}ms. Please check your network connection or try increasing the timeout.`,
248
+ 'TIMEOUT'
249
+ ));
250
+ }, timeout);
251
+ });
252
+ }
253
+
254
+ /**
255
+ * Enhances error with context information
256
+ * @private
257
+ * @param {Error} error - Original error
258
+ * @param {string} context - Context information
259
+ * @returns {FacebetterError} Enhanced error
260
+ */
261
+ _enhanceError(error, context) {
262
+ if (error instanceof FacebetterError) {
263
+ return error;
264
+ }
265
+
266
+ let errorCode = 'UNKNOWN_ERROR';
267
+ let message = error.message || 'Unknown error occurred';
268
+
269
+ // Categorize errors
270
+ if (error.message?.includes('timeout') || error.message?.includes('TIMEOUT')) {
271
+ errorCode = 'TIMEOUT';
272
+ } else if (error.message?.includes('network') || error.message?.includes('fetch')) {
273
+ errorCode = 'NETWORK_ERROR';
274
+ } else if (error.message?.includes('WASM') || error.message?.includes('wasm')) {
275
+ errorCode = 'WASM_LOAD_ERROR';
276
+ } else if (error.message?.includes('license') || error.message?.includes('auth')) {
277
+ errorCode = 'LICENSE_ERROR';
278
+ }
279
+
280
+ return new FacebetterError(
281
+ context ? `${context}: ${message}` : message,
282
+ errorCode
283
+ );
233
284
  }
234
285
 
235
286
  /**
236
287
  * Initializes the engine
288
+ * @param {Object} [options] - Initialization options
289
+ * @param {number} [options.timeout] - Timeout in milliseconds (default: 30000 for WASM, 10000 for auth)
290
+ * @param {number} [options.authTimeout] - Timeout for online authentication in milliseconds (default: 10000)
237
291
  * @returns {Promise<void>} Promise that resolves when initialization is complete
238
292
  */
239
- async init() {
293
+ async init(options = {}) {
294
+ // 并发控制:如果已经初始化,直接返回
240
295
  if (this.initialized) {
241
296
  return;
242
297
  }
243
298
 
244
- // Wait for WASM module to be loaded (started in constructor)
245
- await this._wasmLoadPromise;
246
- const Module = this._getWasmModule();
247
-
248
- let licenseJsonToUse = this.licenseJson;
299
+ // 并发控制:如果正在初始化,返回同一个 Promise
300
+ if (this._initPromise) {
301
+ return this._initPromise;
302
+ }
249
303
 
250
- // 如果用户提供了 appId 和 appKey(但没有提供 licenseJson),则在 JS 层发送 HTTP 请求
251
- if (this.appId && this.appKey && !this.licenseJson && this._verifyAppKeyOnline) {
304
+ // 创建初始化 Promise
305
+ this._initPromise = (async () => {
252
306
  try {
253
- // JS 层调用 online_auth API 获取服务器响应
254
- const authResponse = await this._verifyAppKeyOnline(this.appId, this.appKey);
255
-
256
- if (!authResponse) {
257
- throw new FacebetterError('Failed to get server response from online_auth API');
307
+ const wasmTimeout = options.timeout || 30000;
308
+ const authTimeout = options.authTimeout || 10000;
309
+
310
+ // 等待 WASM 模块加载(带超时)
311
+ try {
312
+ await Promise.race([
313
+ this._wasmLoadPromise,
314
+ this._createTimeout(wasmTimeout, 'WASM module loading')
315
+ ]);
316
+ } catch (error) {
317
+ throw this._enhanceError(error, 'Failed to load WASM module');
258
318
  }
259
319
 
260
- // 将服务器响应转换为 JSON 字符串,作为 licenseJson 传递给 WASM 层
261
- licenseJsonToUse = JSON.stringify(authResponse);
262
- console.log('Online auth response received, using as licenseJson');
263
- } catch (error) {
264
- throw new FacebetterError(`Failed to verify app key online: ${error.message}`);
265
- }
266
- }
320
+ const Module = this._getWasmModule();
267
321
 
268
- // 如果用户直接提供了 licenseJson,则直接使用
269
- // 如果通过 appId/appKey 获取到了响应,则使用响应作为 licenseJson
270
- // WASM 层只需要验证 licenseJson,不需要发送 HTTP 请求
271
- const enginePtr = Module.ccall(
272
- 'CreateBeautyEffectEngine',
273
- 'number',
274
- ['string', 'string'],
275
- [
276
- this.resourcePath,
277
- licenseJsonToUse || '' // 使用 licenseJson 验证
278
- ]
279
- );
322
+ let licenseJsonToUse = this.licenseJson;
280
323
 
281
- if (!enginePtr) {
282
- throw new FacebetterError('Failed to create BeautyEffect engine');
283
- }
324
+ // 如果用户提供了 appId 和 appKey(但没有提供 licenseJson),则在 JS 层发送 HTTP 请求
325
+ if (this.appId && this.appKey && !this.licenseJson && this._verifyAppKeyOnline) {
326
+ try {
327
+ // 在线认证(带超时)
328
+ const authResponse = await Promise.race([
329
+ this._verifyAppKeyOnline(this.appId, this.appKey),
330
+ this._createTimeout(authTimeout, 'Online authentication')
331
+ ]);
332
+
333
+ if (!authResponse) {
334
+ throw new FacebetterError(
335
+ 'Failed to get server response from online_auth API. The server returned an empty response.',
336
+ 'AUTH_EMPTY_RESPONSE'
337
+ );
338
+ }
339
+
340
+ // 将服务器响应转换为 JSON 字符串,作为 licenseJson 传递给 WASM 层
341
+ licenseJsonToUse = JSON.stringify(authResponse);
342
+ console.log('Online auth response received, using as licenseJson');
343
+ } catch (error) {
344
+ if (error instanceof FacebetterError && error.code === 'TIMEOUT') {
345
+ throw new FacebetterError(
346
+ `Online authentication timed out after ${authTimeout}ms. Please check your network connection or provide licenseJson directly.`,
347
+ 'AUTH_TIMEOUT'
348
+ );
349
+ }
350
+ throw this._enhanceError(error, 'Failed to verify app key online');
351
+ }
352
+ }
353
+
354
+ // 如果用户直接提供了 licenseJson,则直接使用
355
+ // 如果通过 appId/appKey 获取到了响应,则使用响应作为 licenseJson
356
+ // WASM 层只需要验证 licenseJson,不需要发送 HTTP 请求
357
+ const enginePtr = Module.ccall(
358
+ 'CreateBeautyEffectEngine',
359
+ 'number',
360
+ ['string', 'string'],
361
+ [
362
+ this.resourcePath,
363
+ licenseJsonToUse || '' // 使用 licenseJson 验证
364
+ ]
365
+ );
366
+
367
+ if (!enginePtr) {
368
+ throw new FacebetterError(
369
+ 'Failed to create BeautyEffect engine. This may be due to invalid license or resource path issues.',
370
+ 'ENGINE_CREATE_FAILED'
371
+ );
372
+ }
373
+
374
+ this.enginePtr = enginePtr;
375
+ this.initialized = true;
376
+ this._initPromise = null; // 清除 Promise 缓存,允许重新初始化(如果需要)
377
+ } catch (error) {
378
+ this._initPromise = null; // 清除 Promise 缓存,允许重试
379
+ throw error;
380
+ }
381
+ })();
284
382
 
285
- this.enginePtr = enginePtr;
286
- this.initialized = true;
383
+ return this._initPromise;
287
384
  }
288
385
 
289
386
  /**
@@ -310,6 +407,7 @@ class BeautyEffectEngine {
310
407
  this.enginePtr = null;
311
408
  this.initialized = false;
312
409
  this.bufferSize = 0;
410
+ this._initPromise = null; // 清除初始化 Promise
313
411
 
314
412
  // Clean up offscreen canvas
315
413
  if (this._offscreenCanvas) {
@@ -790,7 +888,7 @@ async function verifyAppKeyOnline(appId, appKey) {
790
888
  /**
791
889
  * ESM Entry Point
792
890
  * For Vue/React/Vite/Webpack (ES Module)
793
- * Uses import.meta.url for automatic path resolution
891
+ * Uses facebetter-core npm package for WASM module loading
794
892
  */
795
893
 
796
894
 
@@ -798,49 +896,13 @@ async function verifyAppKeyOnline(appId, appKey) {
798
896
  let wasmModuleInstance = null;
799
897
  let wasmModulePromise = null;
800
898
 
801
- /**
802
- * Gets the base path of the current module
803
- * Uses import.meta.url (ES Module equivalent of __dirname)
804
- * @returns {string} Base path of the current module (with trailing slash)
805
- */
806
- function getModuleBasePath() {
807
- try {
808
- const url = new URL('.', import.meta.url);
809
- return url.href;
810
- } catch (e) {
811
- // Fallback
812
- return './';
813
- }
814
- }
815
-
816
- /**
817
- * Resolves a relative path to absolute URL (ESM version)
818
- * Handles various path formats
819
- * @param {string} relativePath - Relative path
820
- * @returns {string} Absolute URL
821
- */
822
- function resolvePath(relativePath) {
823
- // If relativePath is already absolute, return as-is
824
- if (relativePath.startsWith('http://') || relativePath.startsWith('https://') || relativePath.startsWith('//')) {
825
- return relativePath;
826
- }
827
-
828
- try {
829
- return new URL(relativePath, import.meta.url).href;
830
- } catch (e) {
831
- // Fallback
832
- const base = getModuleBasePath();
833
- const cleanPath = relativePath.replace(/^\.\//, '');
834
- return base + cleanPath;
835
- }
836
- }
837
-
838
899
  /**
839
900
  * Loads the WebAssembly module (ESM version)
840
901
  * Supports ESM format WASM modules (generated with MODULARIZE and EXPORT_ES6)
902
+ * With SINGLE_FILE=1, WASM binary and data files are embedded in the JS file
841
903
  * @param {Object} [options] - Options
842
- * @param {string} [options.wasmUrl] - Custom WASM URL (auto-detected if not provided)
843
- * @param {Function} [options.locateFile] - Custom locateFile function
904
+ * @param {string} [options.wasmUrl] - Custom WASM module URL (defaults to facebetter-core npm package)
905
+ * @param {Function} [options.locateFile] - Custom locateFile function (usually not needed with SINGLE_FILE=1)
844
906
  * @returns {Promise<Object>} Promise that resolves with the WASM module
845
907
  */
846
908
  async function loadWasmModule(options = {}) {
@@ -854,72 +916,23 @@ async function loadWasmModule(options = {}) {
854
916
 
855
917
  wasmModulePromise = (async () => {
856
918
  // Try to import the WASM module factory
857
- // facebetter_wasm.js is an Emscripten-generated ESM file (with MODULARIZE and EXPORT_ES6)
919
+ // facebetter-core.js is an Emscripten-generated ESM file (with MODULARIZE and EXPORT_ES6)
920
+ // It's provided by the facebetter-core npm package
858
921
  let FaceBetterModuleFactory;
859
922
 
860
923
  try {
861
- // Get the WASM module URL
862
- let wasmModuleUrl;
924
+ // Get the WASM module
863
925
  if (options.wasmUrl) {
864
- wasmModuleUrl = options.wasmUrl;
926
+ // If custom URL is provided, use it (for backward compatibility or custom builds)
927
+ const wasmModule = await import(options.wasmUrl);
928
+ FaceBetterModuleFactory = wasmModule.default || wasmModule.createFaceBetterModule;
865
929
  } else {
866
- // Use relative path for build time
867
- // At runtime in Vite, the path will be resolved correctly
868
- wasmModuleUrl = resolvePath('./facebetter_wasm.js');
930
+ // Import from facebetter-core npm package
931
+ const wasmModule = await import('facebetter-core');
869
932
 
870
- // In browser runtime (Vite), try to use package exports
871
- if (typeof window !== 'undefined') {
872
- try {
873
- // Try to use package exports path (Vite will handle it at runtime)
874
- const pkgPath = 'facebetter/wasm.js';
875
- // Use Function constructor to avoid Rollup static analysis
876
- const dynamicImport = new Function('path', 'return import(path)');
877
- const wasmModule = await dynamicImport(pkgPath);
878
- if (wasmModule && (wasmModule.default || wasmModule.createFaceBetterModule)) {
879
- // ESM format: module.default is the factory function
880
- FaceBetterModuleFactory = wasmModule.default || wasmModule.createFaceBetterModule;
881
- if (FaceBetterModuleFactory) {
882
- // Configure locateFile for WASM and data files
883
- const moduleOptions = {
884
- locateFile: options.locateFile || function(path, prefix) {
885
- // Auto-resolve .wasm and .data files
886
- if (path.endsWith('.wasm') || path.endsWith('.data')) {
887
- try {
888
- if (typeof import.meta.resolve === 'function') {
889
- if (path.endsWith('.wasm')) {
890
- return import.meta.resolve('facebetter/wasm');
891
- } else if (path.endsWith('.data')) {
892
- const wasmUrl = import.meta.resolve('facebetter/wasm');
893
- return wasmUrl.replace(/\.wasm$/, '.data');
894
- }
895
- }
896
- } catch (e) {
897
- // Fallback
898
- }
899
- return resolvePath(path);
900
- }
901
- return prefix + path;
902
- },
903
- ...options
904
- };
905
- wasmModuleInstance = await FaceBetterModuleFactory(moduleOptions);
906
- wasmModuleInstance.ready = true;
907
- return wasmModuleInstance;
908
- }
909
- }
910
- } catch (e) {
911
- // Fallback to direct import below
912
- }
913
- }
933
+ // Get the factory function (default export or named export)
934
+ FaceBetterModuleFactory = wasmModule.default || wasmModule.createFaceBetterModule;
914
935
  }
915
-
916
- // Load WASM module using dynamic import (ESM format)
917
- // Emscripten with MODULARIZE=1 and EXPORT_ES6=1 generates:
918
- // export default function createFaceBetterModule(options) { ... }
919
- const wasmModule = await import(wasmModuleUrl);
920
-
921
- // Get the factory function (default export or named export)
922
- FaceBetterModuleFactory = wasmModule.default || wasmModule.createFaceBetterModule;
923
936
 
924
937
  if (!FaceBetterModuleFactory) {
925
938
  throw new Error('WASM module does not export createFaceBetterModule function');
@@ -928,25 +941,13 @@ async function loadWasmModule(options = {}) {
928
941
  throw new FacebetterError(`Failed to load WASM module: ${e.message}`);
929
942
  }
930
943
 
931
- // Configure locateFile for WASM and data files
944
+ // Configure module options
945
+ // With SINGLE_FILE=1, WASM binary and data files are embedded in the JS file
946
+ // No need to handle .wasm and .data file paths separately
932
947
  const moduleOptions = {
933
948
  locateFile: options.locateFile || function(path, prefix) {
934
- // Auto-resolve .wasm and .data files
935
- if (path.endsWith('.wasm') || path.endsWith('.data')) {
936
- try {
937
- if (typeof import.meta.resolve === 'function') {
938
- if (path.endsWith('.wasm')) {
939
- return import.meta.resolve('facebetter/wasm');
940
- } else if (path.endsWith('.data')) {
941
- const wasmUrl = import.meta.resolve('facebetter/wasm');
942
- return wasmUrl.replace(/\.wasm$/, '.data');
943
- }
944
- }
945
- } catch (e) {
946
- // Fallback
947
- }
948
- return resolvePath(path);
949
- }
949
+ // With SINGLE_FILE=1, Emscripten handles embedded files automatically
950
+ // Custom locateFile is only needed if user provides one
950
951
  return prefix + path;
951
952
  },
952
953
  ...options