@hot-updater/react-native
Version:
React Native OTA solution for self-hosted
255 lines (238 loc) • 8.21 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.HotUpdaterConstants = void 0;
Object.defineProperty(exports, "HotUpdaterErrorCode", {
enumerable: true,
get: function () {
return _error.HotUpdaterErrorCode;
}
});
exports.getMinBundleId = exports.getFingerprintHash = exports.getCrashHistory = exports.getChannel = exports.getBundleId = exports.getBaseURL = exports.getAppVersion = exports.clearCrashHistory = exports.addListener = void 0;
Object.defineProperty(exports, "isHotUpdaterError", {
enumerable: true,
get: function () {
return _error.isHotUpdaterError;
}
});
exports.reload = exports.notifyAppReady = void 0;
exports.updateBundle = updateBundle;
var _reactNative = require("react-native");
var _error = require("./error.js");
var _NativeHotUpdater = _interopRequireDefault(require("./specs/NativeHotUpdater.js"));
function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; }
const NIL_UUID = "00000000-0000-0000-0000-000000000000";
const HotUpdaterConstants = exports.HotUpdaterConstants = {
HOT_UPDATER_BUNDLE_ID: __HOT_UPDATER_BUNDLE_ID || NIL_UUID
};
const addListener = (eventName, listener) => {
const eventEmitter = new _reactNative.NativeEventEmitter(_NativeHotUpdater.default);
const subscription = eventEmitter.addListener(eventName, listener);
return () => {
subscription.remove();
};
};
exports.addListener = addListener;
// In-flight update deduplication by bundleId (session-scoped).
const inflightUpdates = new Map();
// Tracks the last successfully installed bundleId for this session.
let lastInstalledBundleId = null;
/**
* Downloads files and applies them to the app.
*
* @param {UpdateParams} params - Parameters object required for bundle update
* @returns {Promise<boolean>} Resolves with true if download was successful
* @throws {Error} Rejects with error.code from HotUpdaterErrorCode enum and error.message
*/
/**
* @deprecated Use updateBundle(params: UpdateBundleParamsWithStatus) instead
*/
async function updateBundle(paramsOrBundleId, fileUrl) {
const updateBundleId = typeof paramsOrBundleId === "string" ? paramsOrBundleId : paramsOrBundleId.bundleId;
const status = typeof paramsOrBundleId === "string" ? "UPDATE" : paramsOrBundleId.status;
// If we have already installed this bundle in this session, skip re-download.
if (status === "UPDATE" && lastInstalledBundleId === updateBundleId) {
return true;
}
const currentBundleId = getBundleId();
// updateBundleId <= currentBundleId
if (status === "UPDATE" && updateBundleId.localeCompare(currentBundleId) <= 0) {
throw new Error("Update bundle id is the same as the current bundle id. Preventing infinite update loop.");
}
// In-flight guard: return the same promise if the same bundle is already updating.
const existing = inflightUpdates.get(updateBundleId);
if (existing) return existing;
const targetFileUrl = typeof paramsOrBundleId === "string" ? fileUrl ?? null : paramsOrBundleId.fileUrl;
const targetFileHash = typeof paramsOrBundleId === "string" ? undefined : paramsOrBundleId.fileHash;
const promise = (async () => {
try {
const ok = await _NativeHotUpdater.default.updateBundle({
bundleId: updateBundleId,
fileUrl: targetFileUrl,
fileHash: targetFileHash ?? null
});
if (ok) {
lastInstalledBundleId = updateBundleId;
}
return ok;
} finally {
inflightUpdates.delete(updateBundleId);
}
})();
inflightUpdates.set(updateBundleId, promise);
return promise;
}
/**
* Fetches the current app version.
*/
const getAppVersion = () => {
const constants = _NativeHotUpdater.default.getConstants();
return constants?.APP_VERSION ?? null;
};
/**
* Reloads the app.
*/
exports.getAppVersion = getAppVersion;
const reload = async () => {
await _NativeHotUpdater.default.reload();
};
/**
* Fetches the minimum bundle id, which represents the initial bundle of the app
* since it is created at build time.
*
* @returns {string} Resolves with the minimum bundle id or null if not available.
*/
exports.reload = reload;
const getMinBundleId = () => {
const constants = _NativeHotUpdater.default.getConstants();
return constants.MIN_BUNDLE_ID;
};
/**
* Fetches the current bundle version id.
*
* @async
* @returns {string} Resolves with the current version id or null if not available.
*/
exports.getMinBundleId = getMinBundleId;
const getBundleId = () => {
return HotUpdaterConstants.HOT_UPDATER_BUNDLE_ID === NIL_UUID ? getMinBundleId() : HotUpdaterConstants.HOT_UPDATER_BUNDLE_ID;
};
/**
* Fetches the channel for the app.
*
* @returns {string} Resolves with the channel or null if not available.
*/
exports.getBundleId = getBundleId;
const getChannel = () => {
const constants = _NativeHotUpdater.default.getConstants();
return constants.CHANNEL;
};
/**
* Fetches the fingerprint for the app.
*
* @returns {string | null} Resolves with the fingerprint hash
*/
exports.getChannel = getChannel;
const getFingerprintHash = () => {
const constants = _NativeHotUpdater.default.getConstants();
return constants.FINGERPRINT_HASH;
};
/**
* Result returned by notifyAppReady()
*/
exports.getFingerprintHash = getFingerprintHash;
/**
* Notifies the native side that the app has successfully started with the current bundle.
* If the bundle matches the staging bundle, it promotes to stable.
*
* This function is called automatically when the module loads.
*
* @returns {NotifyAppReadyResult} Bundle state information
* - `status: "PROMOTED"` - Staging bundle was promoted to stable (ACTIVE event)
* - `status: "RECOVERED"` - App recovered from crash, rollback occurred (ROLLBACK event)
* - `status: "STABLE"` - No changes, already stable
* - `crashedBundleId` - Present only when status is "RECOVERED"
*
* @example
* ```ts
* const result = HotUpdater.notifyAppReady();
*
* switch (result.status) {
* case "PROMOTED":
* // Send ACTIVE analytics event
* analytics.track('bundle_active', { bundleId: HotUpdater.getBundleId() });
* break;
* case "RECOVERED":
* // Send ROLLBACK analytics event
* analytics.track('bundle_rollback', { crashedBundleId: result.crashedBundleId });
* break;
* case "STABLE":
* // No special action needed
* break;
* }
* ```
*/
const notifyAppReady = () => {
const bundleId = getBundleId();
const result = _NativeHotUpdater.default.notifyAppReady({
bundleId
});
// Oldarch returns JSON string, newarch returns array
if (typeof result === "string") {
try {
return JSON.parse(result);
} catch {
return {
status: "STABLE"
};
}
}
return result;
};
/**
* Gets the list of bundle IDs that have been marked as crashed.
* These bundles will be rejected if attempted to install again.
*
* @returns {string[]} Array of crashed bundle IDs
*/
exports.notifyAppReady = notifyAppReady;
const getCrashHistory = () => {
const result = _NativeHotUpdater.default.getCrashHistory();
// Oldarch returns JSON string, newarch returns array
if (typeof result === "string") {
try {
return JSON.parse(result);
} catch {
return [];
}
}
return result;
};
/**
* Clears the crashed bundle history, allowing previously crashed bundles
* to be installed again.
*
* @returns {boolean} true if clearing was successful
*/
exports.getCrashHistory = getCrashHistory;
const clearCrashHistory = () => {
return _NativeHotUpdater.default.clearCrashHistory();
};
/**
* Gets the base URL for the current active bundle directory.
* Returns the file:// URL to the bundle directory without trailing slash.
* This is used for Expo DOM components to construct full asset paths.
*
* @returns {string | null} Base URL string (e.g., "file:///data/.../bundle-store/abc123") or null if not available
*/
exports.clearCrashHistory = clearCrashHistory;
const getBaseURL = () => {
const result = _NativeHotUpdater.default.getBaseURL();
if (typeof result === "string" && result !== "") {
return result;
}
return null;
};
exports.getBaseURL = getBaseURL;
//# sourceMappingURL=native.js.map