@hot-updater/react-native 1.0.0-rc.17 → 1.0.0-rc.21
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.
- package/lib/commonjs/checkForUpdate.js +7 -9
- package/lib/commonjs/checkForUpdate.js.map +1 -1
- package/lib/commonjs/clientPlugin.js +7 -43
- package/lib/commonjs/clientPlugin.js.map +1 -1
- package/lib/commonjs/httpClient.js +7 -9
- package/lib/commonjs/httpClient.js.map +1 -1
- package/lib/commonjs/index.js +10 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/native.js +4 -4
- package/lib/commonjs/native.js.map +1 -1
- package/lib/commonjs/pluginHost.js +26 -190
- package/lib/commonjs/pluginHost.js.map +1 -1
- package/lib/commonjs/releaseCatalogCache.js +7 -59
- package/lib/commonjs/releaseCatalogCache.js.map +1 -1
- package/lib/commonjs/sdkVersion.js +1 -1
- package/lib/module/checkForUpdate.js +1 -3
- package/lib/module/checkForUpdate.js.map +1 -1
- package/lib/module/clientPlugin.js +1 -41
- package/lib/module/clientPlugin.js.map +1 -1
- package/lib/module/httpClient.js +1 -3
- package/lib/module/httpClient.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native.js +1 -1
- package/lib/module/native.js.map +1 -1
- package/lib/module/pluginHost.js +22 -187
- package/lib/module/pluginHost.js.map +1 -1
- package/lib/module/releaseCatalogCache.js +3 -54
- package/lib/module/releaseCatalogCache.js.map +1 -1
- package/lib/module/sdkVersion.js +1 -1
- package/lib/typescript/commonjs/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/commonjs/clientPlugin.d.ts +1 -184
- package/lib/typescript/commonjs/clientPlugin.d.ts.map +1 -1
- package/lib/typescript/commonjs/httpClient.d.ts +1 -2
- package/lib/typescript/commonjs/httpClient.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +1 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/native.d.ts +1 -1
- package/lib/typescript/commonjs/native.d.ts.map +1 -1
- package/lib/typescript/commonjs/pluginHost.d.ts +3 -28
- package/lib/typescript/commonjs/pluginHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts +1 -13
- package/lib/typescript/commonjs/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/commonjs/sdkVersion.d.ts +1 -1
- package/lib/typescript/commonjs/types.d.ts +1 -1
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/wrap.types.d.ts +2 -2
- package/lib/typescript/module/checkForUpdate.d.ts.map +1 -1
- package/lib/typescript/module/clientPlugin.d.ts +1 -184
- package/lib/typescript/module/clientPlugin.d.ts.map +1 -1
- package/lib/typescript/module/httpClient.d.ts +1 -2
- package/lib/typescript/module/httpClient.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +1 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/native.d.ts +1 -1
- package/lib/typescript/module/native.d.ts.map +1 -1
- package/lib/typescript/module/pluginHost.d.ts +3 -28
- package/lib/typescript/module/pluginHost.d.ts.map +1 -1
- package/lib/typescript/module/releaseCatalogCache.d.ts +1 -13
- package/lib/typescript/module/releaseCatalogCache.d.ts.map +1 -1
- package/lib/typescript/module/sdkVersion.d.ts +1 -1
- package/lib/typescript/module/types.d.ts +1 -1
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/wrap.types.d.ts +2 -2
- package/package.json +7 -16
- package/src/checkForUpdate.ts +4 -6
- package/src/clientPlugin.ts +17 -231
- package/src/httpClient.ts +4 -4
- package/src/index.ts +7 -0
- package/src/native.ts +1 -1
- package/src/pluginHost.ts +24 -241
- package/src/releaseCatalogCache.ts +7 -136
- package/src/sdkVersion.ts +1 -1
- package/src/types.ts +1 -1
- package/src/wrap.types.ts +2 -2
- package/android/src/test/java/com/hotupdater/BsdiffPatchTest.kt +0 -46
- package/android/src/test/java/com/hotupdater/BundleFileStorageServiceTest.kt +0 -2236
- package/android/src/test/java/com/hotupdater/HotUpdaterExceptionTest.kt +0 -220
- package/android/src/test/java/com/hotupdater/HotUpdaterImplTest.kt +0 -264
- package/android/src/test/java/com/hotupdater/HotUpdaterRecoveryStateTest.kt +0 -97
- package/android/src/test/java/com/hotupdater/InstallIdentityServiceTest.kt +0 -71
- package/android/src/test/java/com/hotupdater/KeyValueStorageServiceTest.kt +0 -106
- package/android/src/test/java/com/hotupdater/NativeConfigUtilsTest.kt +0 -20
- package/android/src/test/java/com/hotupdater/NoBackupContext.kt +0 -11
- package/android/src/test/java/com/hotupdater/OkHttpDownloadServiceTest.kt +0 -397
- package/android/src/test/java/com/hotupdater/PendingCrashRecoveryTest.kt +0 -44
- package/android/src/test/java/com/hotupdater/PreviousProcessExitTest.kt +0 -57
- package/android/src/test/java/com/hotupdater/RelativePathResolverTest.kt +0 -41
- package/android/src/test/java/com/hotupdater/ReleaseCatalogCacheServiceTest.kt +0 -118
- package/android/src/test/java/com/hotupdater/TarBrArchiveExtractorTest.kt +0 -206
- package/lib/commonjs/appReady.spec.js +0 -360
- package/lib/commonjs/appReady.spec.js.map +0 -1
- package/lib/commonjs/appReady.test-utils.js +0 -25
- package/lib/commonjs/appReady.test-utils.js.map +0 -1
- package/lib/commonjs/baseURL.js +0 -16
- package/lib/commonjs/baseURL.js.map +0 -1
- package/lib/commonjs/catalogCacheNative.spec.js +0 -31
- package/lib/commonjs/catalogCacheNative.spec.js.map +0 -1
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js +0 -936
- package/lib/commonjs/checkForUpdate.releaseCatalog.spec.js.map +0 -1
- package/lib/commonjs/entryImportGraph.spec.js +0 -50
- package/lib/commonjs/entryImportGraph.spec.js.map +0 -1
- package/lib/commonjs/httpClient.spec.js +0 -171
- package/lib/commonjs/httpClient.spec.js.map +0 -1
- package/lib/commonjs/index.spec.js +0 -395
- package/lib/commonjs/index.spec.js.map +0 -1
- package/lib/commonjs/iosInterop.spec.js +0 -18
- package/lib/commonjs/iosInterop.spec.js.map +0 -1
- package/lib/commonjs/native.spec.js +0 -910
- package/lib/commonjs/native.spec.js.map +0 -1
- package/lib/commonjs/pluginHost.spec.js +0 -292
- package/lib/commonjs/pluginHost.spec.js.map +0 -1
- package/lib/commonjs/plugins/insights/eventId.js +0 -129
- package/lib/commonjs/plugins/insights/eventId.js.map +0 -1
- package/lib/commonjs/plugins/insights/eventId.spec.js +0 -47
- package/lib/commonjs/plugins/insights/eventId.spec.js.map +0 -1
- package/lib/commonjs/plugins/insights/index.js +0 -272
- package/lib/commonjs/plugins/insights/index.js.map +0 -1
- package/lib/commonjs/plugins/insights/insights.spec.js +0 -640
- package/lib/commonjs/plugins/insights/insights.spec.js.map +0 -1
- package/lib/commonjs/plugins/insights/sender.js +0 -128
- package/lib/commonjs/plugins/insights/sender.js.map +0 -1
- package/lib/commonjs/plugins/insights/sender.spec.js +0 -265
- package/lib/commonjs/plugins/insights/sender.spec.js.map +0 -1
- package/lib/commonjs/plugins/insights/state.js +0 -145
- package/lib/commonjs/plugins/insights/state.js.map +0 -1
- package/lib/commonjs/releaseCatalogCache.spec.js +0 -213
- package/lib/commonjs/releaseCatalogCache.spec.js.map +0 -1
- package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js +0 -64
- package/lib/commonjs/releaseCatalogCacheNativeContract.spec.js.map +0 -1
- package/lib/commonjs/store.notification.spec.js +0 -84
- package/lib/commonjs/store.notification.spec.js.map +0 -1
- package/lib/commonjs/store.spec.js +0 -182
- package/lib/commonjs/store.spec.js.map +0 -1
- package/lib/commonjs/updateError.spec.js +0 -149
- package/lib/commonjs/updateError.spec.js.map +0 -1
- package/lib/commonjs/wrap.progress.spec.js +0 -195
- package/lib/commonjs/wrap.progress.spec.js.map +0 -1
- package/lib/commonjs/wrap.spec.js +0 -232
- package/lib/commonjs/wrap.spec.js.map +0 -1
- package/lib/module/appReady.spec.js +0 -359
- package/lib/module/appReady.spec.js.map +0 -1
- package/lib/module/appReady.test-utils.js +0 -19
- package/lib/module/appReady.test-utils.js.map +0 -1
- package/lib/module/baseURL.js +0 -11
- package/lib/module/baseURL.js.map +0 -1
- package/lib/module/catalogCacheNative.spec.js +0 -31
- package/lib/module/catalogCacheNative.spec.js.map +0 -1
- package/lib/module/checkForUpdate.releaseCatalog.spec.js +0 -935
- package/lib/module/checkForUpdate.releaseCatalog.spec.js.map +0 -1
- package/lib/module/entryImportGraph.spec.js +0 -50
- package/lib/module/entryImportGraph.spec.js.map +0 -1
- package/lib/module/httpClient.spec.js +0 -171
- package/lib/module/httpClient.spec.js.map +0 -1
- package/lib/module/index.spec.js +0 -394
- package/lib/module/index.spec.js.map +0 -1
- package/lib/module/iosInterop.spec.js +0 -18
- package/lib/module/iosInterop.spec.js.map +0 -1
- package/lib/module/native.spec.js +0 -909
- package/lib/module/native.spec.js.map +0 -1
- package/lib/module/pluginHost.spec.js +0 -291
- package/lib/module/pluginHost.spec.js.map +0 -1
- package/lib/module/plugins/insights/eventId.js +0 -122
- package/lib/module/plugins/insights/eventId.js.map +0 -1
- package/lib/module/plugins/insights/eventId.spec.js +0 -47
- package/lib/module/plugins/insights/eventId.spec.js.map +0 -1
- package/lib/module/plugins/insights/index.js +0 -268
- package/lib/module/plugins/insights/index.js.map +0 -1
- package/lib/module/plugins/insights/insights.spec.js +0 -639
- package/lib/module/plugins/insights/insights.spec.js.map +0 -1
- package/lib/module/plugins/insights/sender.js +0 -123
- package/lib/module/plugins/insights/sender.js.map +0 -1
- package/lib/module/plugins/insights/sender.spec.js +0 -265
- package/lib/module/plugins/insights/sender.spec.js.map +0 -1
- package/lib/module/plugins/insights/state.js +0 -139
- package/lib/module/plugins/insights/state.js.map +0 -1
- package/lib/module/releaseCatalogCache.spec.js +0 -213
- package/lib/module/releaseCatalogCache.spec.js.map +0 -1
- package/lib/module/releaseCatalogCacheNativeContract.spec.js +0 -64
- package/lib/module/releaseCatalogCacheNativeContract.spec.js.map +0 -1
- package/lib/module/store.notification.spec.js +0 -85
- package/lib/module/store.notification.spec.js.map +0 -1
- package/lib/module/store.spec.js +0 -181
- package/lib/module/store.spec.js.map +0 -1
- package/lib/module/updateError.spec.js +0 -149
- package/lib/module/updateError.spec.js.map +0 -1
- package/lib/module/wrap.progress.spec.js +0 -196
- package/lib/module/wrap.progress.spec.js.map +0 -1
- package/lib/module/wrap.spec.js +0 -233
- package/lib/module/wrap.spec.js.map +0 -1
- package/lib/typescript/commonjs/appReady.test-utils.d.ts +0 -9
- package/lib/typescript/commonjs/appReady.test-utils.d.ts.map +0 -1
- package/lib/typescript/commonjs/baseURL.d.ts +0 -4
- package/lib/typescript/commonjs/baseURL.d.ts.map +0 -1
- package/lib/typescript/commonjs/plugins/insights/eventId.d.ts +0 -18
- package/lib/typescript/commonjs/plugins/insights/eventId.d.ts.map +0 -1
- package/lib/typescript/commonjs/plugins/insights/index.d.ts +0 -40
- package/lib/typescript/commonjs/plugins/insights/index.d.ts.map +0 -1
- package/lib/typescript/commonjs/plugins/insights/sender.d.ts +0 -74
- package/lib/typescript/commonjs/plugins/insights/sender.d.ts.map +0 -1
- package/lib/typescript/commonjs/plugins/insights/state.d.ts +0 -46
- package/lib/typescript/commonjs/plugins/insights/state.d.ts.map +0 -1
- package/lib/typescript/module/appReady.test-utils.d.ts +0 -9
- package/lib/typescript/module/appReady.test-utils.d.ts.map +0 -1
- package/lib/typescript/module/baseURL.d.ts +0 -4
- package/lib/typescript/module/baseURL.d.ts.map +0 -1
- package/lib/typescript/module/plugins/insights/eventId.d.ts +0 -18
- package/lib/typescript/module/plugins/insights/eventId.d.ts.map +0 -1
- package/lib/typescript/module/plugins/insights/index.d.ts +0 -40
- package/lib/typescript/module/plugins/insights/index.d.ts.map +0 -1
- package/lib/typescript/module/plugins/insights/sender.d.ts +0 -74
- package/lib/typescript/module/plugins/insights/sender.d.ts.map +0 -1
- package/lib/typescript/module/plugins/insights/state.d.ts +0 -46
- package/lib/typescript/module/plugins/insights/state.d.ts.map +0 -1
- package/plugins/insights/package.json +0 -8
- package/src/appReady.spec.ts +0 -393
- package/src/appReady.test-utils.ts +0 -31
- package/src/baseURL.ts +0 -15
- package/src/catalogCacheNative.spec.ts +0 -63
- package/src/checkForUpdate.releaseCatalog.spec.ts +0 -966
- package/src/entryImportGraph.spec.ts +0 -65
- package/src/httpClient.spec.ts +0 -216
- package/src/index.spec.ts +0 -434
- package/src/iosInterop.spec.ts +0 -30
- package/src/native.spec.ts +0 -978
- package/src/pluginHost.spec.ts +0 -297
- package/src/plugins/insights/eventId.spec.ts +0 -60
- package/src/plugins/insights/eventId.ts +0 -171
- package/src/plugins/insights/index.ts +0 -367
- package/src/plugins/insights/insights.spec.ts +0 -732
- package/src/plugins/insights/sender.spec.ts +0 -287
- package/src/plugins/insights/sender.ts +0 -221
- package/src/plugins/insights/state.ts +0 -184
- package/src/releaseCatalogCache.spec.ts +0 -304
- package/src/releaseCatalogCacheNativeContract.spec.ts +0 -80
- package/src/store.notification.spec.ts +0 -102
- package/src/store.spec.ts +0 -215
- package/src/updateError.spec.ts +0 -144
- package/src/wrap.progress.spec.ts +0 -232
- package/src/wrap.spec.ts +0 -262
package/src/pluginHost.spec.ts
DELETED
|
@@ -1,297 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
|
|
3
|
-
import type {
|
|
4
|
-
HotUpdaterClientContext,
|
|
5
|
-
HotUpdaterClientPlugin,
|
|
6
|
-
} from "./clientPlugin";
|
|
7
|
-
|
|
8
|
-
vi.mock("react-native", () => ({
|
|
9
|
-
Platform: { OS: "android" },
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
const mocks = vi.hoisted(() => {
|
|
13
|
-
Reflect.set(globalThis, "HotUpdater", { SDK_VERSION: "test-sdk-version" });
|
|
14
|
-
return {
|
|
15
|
-
getAppVersion: vi.fn<() => string | null>(() => "1.2.3"),
|
|
16
|
-
getBundleId: vi.fn(() => "bundle-id"),
|
|
17
|
-
getChannel: vi.fn(() => "production"),
|
|
18
|
-
getCohort: vi.fn(() => "123"),
|
|
19
|
-
getFingerprintHash: vi.fn<() => string | null>(() => null),
|
|
20
|
-
getInstallId: vi.fn(() => "install-id"),
|
|
21
|
-
getStorageItem: vi.fn<(key: string) => string | null>(() => null),
|
|
22
|
-
setStorageItem: vi.fn<(key: string, value: string | null) => void>(),
|
|
23
|
-
};
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
vi.mock("./native", () => mocks);
|
|
27
|
-
|
|
28
|
-
const importHost = () => import("./pluginHost");
|
|
29
|
-
|
|
30
|
-
const capture = (id = "example") => {
|
|
31
|
-
let context!: HotUpdaterClientContext;
|
|
32
|
-
const setup = vi.fn((pluginContext: HotUpdaterClientContext) => {
|
|
33
|
-
context = pluginContext;
|
|
34
|
-
});
|
|
35
|
-
const plugin: HotUpdaterClientPlugin = { id, setup };
|
|
36
|
-
return { plugin, setup, context: () => context };
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
describe("client plugin host", () => {
|
|
40
|
-
beforeEach(() => {
|
|
41
|
-
vi.resetModules();
|
|
42
|
-
for (const mock of Object.values(mocks)) mock.mockClear();
|
|
43
|
-
mocks.getStorageItem.mockReturnValue(null);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
afterEach(() => {
|
|
47
|
-
vi.useRealTimers();
|
|
48
|
-
vi.unstubAllGlobals();
|
|
49
|
-
vi.restoreAllMocks();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("throws at configuration for two plugins with one id", async () => {
|
|
53
|
-
const { configurePlugins } = await importHost();
|
|
54
|
-
|
|
55
|
-
expect(() =>
|
|
56
|
-
configurePlugins([capture("same").plugin, capture("same").plugin], {
|
|
57
|
-
baseURL: "https://updates.example.com",
|
|
58
|
-
}),
|
|
59
|
-
).toThrow('Two plugins use the id "same"');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it("sets a plugin up once and stops calling a plugin left out", async () => {
|
|
63
|
-
const { configurePlugins, emitPluginHook } = await importHost();
|
|
64
|
-
const onAppReady = vi.fn();
|
|
65
|
-
const plugin: HotUpdaterClientPlugin = {
|
|
66
|
-
id: "example",
|
|
67
|
-
setup: vi.fn(() => ({ onAppReady })),
|
|
68
|
-
};
|
|
69
|
-
const config = { baseURL: "https://updates.example.com" };
|
|
70
|
-
|
|
71
|
-
configurePlugins([plugin], config);
|
|
72
|
-
configurePlugins([plugin], config);
|
|
73
|
-
emitPluginHook("onAppReady", () => ({
|
|
74
|
-
status: "UNCHANGED",
|
|
75
|
-
channel: "production",
|
|
76
|
-
bundleId: "bundle-id",
|
|
77
|
-
releaseId: null,
|
|
78
|
-
previousProcessExit: null,
|
|
79
|
-
}));
|
|
80
|
-
configurePlugins([], config);
|
|
81
|
-
emitPluginHook("onAppReady", () => ({
|
|
82
|
-
status: "UNCHANGED",
|
|
83
|
-
channel: "production",
|
|
84
|
-
bundleId: "bundle-id",
|
|
85
|
-
releaseId: null,
|
|
86
|
-
previousProcessExit: null,
|
|
87
|
-
}));
|
|
88
|
-
|
|
89
|
-
expect(plugin.setup).toHaveBeenCalledOnce();
|
|
90
|
-
expect(onAppReady).toHaveBeenCalledOnce();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("warns without onError when setup throws, and keeps other plugins", async () => {
|
|
94
|
-
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
95
|
-
const { configurePlugins } = await importHost();
|
|
96
|
-
const healthy = capture("healthy");
|
|
97
|
-
|
|
98
|
-
configurePlugins(
|
|
99
|
-
[
|
|
100
|
-
{
|
|
101
|
-
id: "broken",
|
|
102
|
-
setup: () => {
|
|
103
|
-
throw new Error("setup failed");
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
healthy.plugin,
|
|
107
|
-
],
|
|
108
|
-
{ baseURL: "https://updates.example.com" },
|
|
109
|
-
);
|
|
110
|
-
|
|
111
|
-
expect(healthy.setup).toHaveBeenCalledOnce();
|
|
112
|
-
expect(warn).toHaveBeenCalledWith(
|
|
113
|
-
'[HotUpdater] Plugin "broken" failed in setup',
|
|
114
|
-
expect.objectContaining({ message: "setup failed" }),
|
|
115
|
-
);
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("never waits for a hook", async () => {
|
|
119
|
-
const { configurePlugins, emitPluginHook } = await importHost();
|
|
120
|
-
const onUpdateCheck = vi.fn(() => new Promise<void>(() => {}));
|
|
121
|
-
configurePlugins([{ id: "slow", setup: () => ({ onUpdateCheck }) }], {
|
|
122
|
-
baseURL: "https://updates.example.com",
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
const result = emitPluginHook("onUpdateCheck", () => ({
|
|
126
|
-
status: "UNCHANGED",
|
|
127
|
-
channel: "production",
|
|
128
|
-
bundleId: "bundle-id",
|
|
129
|
-
releaseId: null,
|
|
130
|
-
previousReleaseId: null,
|
|
131
|
-
}));
|
|
132
|
-
|
|
133
|
-
expect(result).toBeUndefined();
|
|
134
|
-
expect(onUpdateCheck).toHaveBeenCalledOnce();
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it("builds an event only when a plugin listens", async () => {
|
|
138
|
-
const { configurePlugins, emitPluginHook } = await importHost();
|
|
139
|
-
configurePlugins([capture().plugin], {
|
|
140
|
-
baseURL: "https://updates.example.com",
|
|
141
|
-
});
|
|
142
|
-
const createPayload = vi.fn(() => null);
|
|
143
|
-
|
|
144
|
-
emitPluginHook("onUpdateError", createPayload);
|
|
145
|
-
|
|
146
|
-
expect(createPayload).not.toHaveBeenCalled();
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it("exposes the app's identity and environment", async () => {
|
|
150
|
-
vi.stubGlobal("__DEV__", true);
|
|
151
|
-
vi.spyOn(Date, "now").mockReturnValue(1_700_000_000_000);
|
|
152
|
-
const { configurePlugins } = await importHost();
|
|
153
|
-
const plugin = capture();
|
|
154
|
-
configurePlugins([plugin.plugin], {
|
|
155
|
-
baseURL: "https://updates.example.com",
|
|
156
|
-
});
|
|
157
|
-
const context = plugin.context();
|
|
158
|
-
|
|
159
|
-
expect({
|
|
160
|
-
appVersion: context.appVersion,
|
|
161
|
-
bundleId: context.getBundleId(),
|
|
162
|
-
channel: context.getChannel(),
|
|
163
|
-
cohort: context.getCohort(),
|
|
164
|
-
fingerprintHash: context.getFingerprintHash(),
|
|
165
|
-
installId: context.installId,
|
|
166
|
-
isDebugBuild: context.isDebugBuild,
|
|
167
|
-
now: context.now(),
|
|
168
|
-
platform: context.platform,
|
|
169
|
-
sdkVersion: context.sdkVersion,
|
|
170
|
-
}).toEqual({
|
|
171
|
-
appVersion: "1.2.3",
|
|
172
|
-
bundleId: "bundle-id",
|
|
173
|
-
channel: "production",
|
|
174
|
-
cohort: "123",
|
|
175
|
-
fingerprintHash: null,
|
|
176
|
-
installId: "install-id",
|
|
177
|
-
isDebugBuild: true,
|
|
178
|
-
now: 1_700_000_000_000,
|
|
179
|
-
platform: "android",
|
|
180
|
-
sdkVersion: "test-sdk-version",
|
|
181
|
-
});
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
describe("fetch", () => {
|
|
185
|
-
it("requests a path under the resolved baseURL with the SDK's headers", async () => {
|
|
186
|
-
const fetchMock = vi.fn<typeof fetch>(
|
|
187
|
-
async () => new Response(null, { status: 204 }),
|
|
188
|
-
);
|
|
189
|
-
vi.stubGlobal("fetch", fetchMock);
|
|
190
|
-
const { configurePlugins } = await importHost();
|
|
191
|
-
const plugin = capture();
|
|
192
|
-
configurePlugins([plugin.plugin], {
|
|
193
|
-
baseURL: async () => "https://updates.example.com/hot-updater/",
|
|
194
|
-
requestHeaders: { "x-api-key": "client-key", "X-Shared": "sdk" },
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
await plugin.context().fetch("/events", {
|
|
198
|
-
headers: { "Content-Type": "application/json", "X-Shared": "plugin" },
|
|
199
|
-
method: "POST",
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
const [url, init] = fetchMock.mock.calls[0]!;
|
|
203
|
-
expect(url).toBe("https://updates.example.com/hot-updater/events");
|
|
204
|
-
expect(init?.method).toBe("POST");
|
|
205
|
-
const headers = new Headers(init?.headers);
|
|
206
|
-
expect(headers.get("x-api-key")).toBe("client-key");
|
|
207
|
-
expect(headers.get("content-type")).toBe("application/json");
|
|
208
|
-
expect(headers.get("x-shared")).toBe("plugin");
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("rejects with Request timed out after the SDK's timeout", async () => {
|
|
212
|
-
vi.useFakeTimers();
|
|
213
|
-
vi.stubGlobal(
|
|
214
|
-
"fetch",
|
|
215
|
-
vi.fn(
|
|
216
|
-
(_input: unknown, init?: RequestInit) =>
|
|
217
|
-
new Promise((_resolve, reject) => {
|
|
218
|
-
init?.signal?.addEventListener("abort", () => {
|
|
219
|
-
reject(
|
|
220
|
-
Object.assign(new Error("aborted"), { name: "AbortError" }),
|
|
221
|
-
);
|
|
222
|
-
});
|
|
223
|
-
}),
|
|
224
|
-
),
|
|
225
|
-
);
|
|
226
|
-
const { configurePlugins } = await importHost();
|
|
227
|
-
const plugin = capture();
|
|
228
|
-
configurePlugins([plugin.plugin], {
|
|
229
|
-
baseURL: "https://updates.example.com",
|
|
230
|
-
requestTimeout: 2_000,
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
const request = plugin.context().fetch("events");
|
|
234
|
-
const settled = expect(request).rejects.toThrow("Request timed out");
|
|
235
|
-
await vi.advanceTimersByTimeAsync(2_000);
|
|
236
|
-
await settled;
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("refuses a URL outside baseURL", async () => {
|
|
240
|
-
const { configurePlugins } = await importHost();
|
|
241
|
-
const plugin = capture();
|
|
242
|
-
configurePlugins([plugin.plugin], {
|
|
243
|
-
baseURL: "https://updates.example.com",
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
await expect(
|
|
247
|
-
plugin.context().fetch("https://elsewhere.example.com/events"),
|
|
248
|
-
).rejects.toThrow("relative to baseURL");
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
describe("storage", () => {
|
|
253
|
-
it("scopes keys to the plugin and reads its own writes", async () => {
|
|
254
|
-
mocks.getStorageItem.mockReturnValue("from-native");
|
|
255
|
-
const { configurePlugins } = await importHost();
|
|
256
|
-
const plugin = capture("example");
|
|
257
|
-
configurePlugins([plugin.plugin], {
|
|
258
|
-
baseURL: "https://updates.example.com",
|
|
259
|
-
});
|
|
260
|
-
const { storage } = plugin.context();
|
|
261
|
-
|
|
262
|
-
expect(storage.get("state")).toBe("from-native");
|
|
263
|
-
expect(storage.get("state")).toBe("from-native");
|
|
264
|
-
storage.set("state", "next");
|
|
265
|
-
expect(storage.get("state")).toBe("next");
|
|
266
|
-
storage.set("state", null);
|
|
267
|
-
expect(storage.get("state")).toBeNull();
|
|
268
|
-
|
|
269
|
-
expect(mocks.getStorageItem).toHaveBeenCalledOnce();
|
|
270
|
-
expect(mocks.getStorageItem).toHaveBeenCalledWith(
|
|
271
|
-
"plugins/example/state",
|
|
272
|
-
);
|
|
273
|
-
expect(mocks.setStorageItem.mock.calls).toEqual([
|
|
274
|
-
["plugins/example/state", "next"],
|
|
275
|
-
["plugins/example/state", null],
|
|
276
|
-
]);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it("refuses empty keys, non-string values, and values over 64 KB", async () => {
|
|
280
|
-
const { configurePlugins } = await importHost();
|
|
281
|
-
const plugin = capture();
|
|
282
|
-
configurePlugins([plugin.plugin], {
|
|
283
|
-
baseURL: "https://updates.example.com",
|
|
284
|
-
});
|
|
285
|
-
const { storage } = plugin.context();
|
|
286
|
-
|
|
287
|
-
expect(() => storage.get("")).toThrow("non-empty strings");
|
|
288
|
-
expect(() => storage.set("key", 1 as unknown as string)).toThrow(
|
|
289
|
-
"strings or null",
|
|
290
|
-
);
|
|
291
|
-
expect(() => storage.set("key", "x".repeat(64 * 1024 + 1))).toThrow(
|
|
292
|
-
"at most 64 KB",
|
|
293
|
-
);
|
|
294
|
-
expect(mocks.setStorageItem).not.toHaveBeenCalled();
|
|
295
|
-
});
|
|
296
|
-
});
|
|
297
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
|
|
3
|
-
import { isUUIDv7 } from "@hot-updater/core";
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
-
|
|
6
|
-
import { createKeyedUUIDv7, createUUIDv7, sha256 } from "./eventId";
|
|
7
|
-
|
|
8
|
-
describe("createUUIDv7", () => {
|
|
9
|
-
afterEach(() => {
|
|
10
|
-
vi.useRealTimers();
|
|
11
|
-
vi.unstubAllGlobals();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it.each([
|
|
15
|
-
{ label: "Web Crypto", crypto: globalThis.crypto },
|
|
16
|
-
{ label: "no Web Crypto, as in React Native", crypto: undefined },
|
|
17
|
-
])("leads with the 48-bit millisecond time using $label", ({ crypto }) => {
|
|
18
|
-
vi.useFakeTimers({ now: 0x0192_3456_789a });
|
|
19
|
-
vi.stubGlobal("crypto", crypto);
|
|
20
|
-
|
|
21
|
-
const id = createUUIDv7();
|
|
22
|
-
|
|
23
|
-
expect(isUUIDv7(id)).toBe(true);
|
|
24
|
-
expect(id.startsWith("01923456-789a-7")).toBe(true);
|
|
25
|
-
expect(createUUIDv7()).not.toBe(id);
|
|
26
|
-
});
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe("sha256", () => {
|
|
30
|
-
it.each([
|
|
31
|
-
"",
|
|
32
|
-
"abc",
|
|
33
|
-
"a".repeat(55),
|
|
34
|
-
"a".repeat(56),
|
|
35
|
-
"a".repeat(64),
|
|
36
|
-
"a".repeat(1_000),
|
|
37
|
-
"install-id\n2026-09-30\ndownload hash_mismatch bundle-id",
|
|
38
|
-
"한글 및 이모지 😀",
|
|
39
|
-
])("matches Node's SHA-256 for %j", (value) => {
|
|
40
|
-
expect(Buffer.from(sha256(value)).toString("hex")).toBe(
|
|
41
|
-
createHash("sha256").update(value, "utf8").digest("hex"),
|
|
42
|
-
);
|
|
43
|
-
});
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe("createKeyedUUIDv7", () => {
|
|
47
|
-
it("gives one UUIDv7 per key and time, led by that time", () => {
|
|
48
|
-
const midnight = Date.UTC(2026, 8, 30);
|
|
49
|
-
|
|
50
|
-
const id = createKeyedUUIDv7("install-id\nfailure", midnight);
|
|
51
|
-
|
|
52
|
-
expect(isUUIDv7(id)).toBe(true);
|
|
53
|
-
expect(parseInt(id.replace(/-/g, "").slice(0, 12), 16)).toBe(midnight);
|
|
54
|
-
expect(createKeyedUUIDv7("install-id\nfailure", midnight)).toBe(id);
|
|
55
|
-
expect(createKeyedUUIDv7("install-id\nother", midnight)).not.toBe(id);
|
|
56
|
-
expect(
|
|
57
|
-
createKeyedUUIDv7("install-id\nfailure", midnight + 86_400_000),
|
|
58
|
-
).not.toBe(id);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
type RandomValuesSource = {
|
|
2
|
-
readonly getRandomValues?: (array: Uint8Array) => Uint8Array;
|
|
3
|
-
};
|
|
4
|
-
|
|
5
|
-
const formatUUIDv7 = (bytes: Uint8Array, timestamp: number): string => {
|
|
6
|
-
// Division, not bit shifts: shifts truncate to 32 bits, and a millisecond
|
|
7
|
-
// timestamp already needs 41.
|
|
8
|
-
let time = timestamp;
|
|
9
|
-
for (let index = 5; index >= 0; index -= 1) {
|
|
10
|
-
bytes[index] = time % 256;
|
|
11
|
-
time = Math.floor(time / 256);
|
|
12
|
-
}
|
|
13
|
-
bytes[6] = 0x70 | (bytes[6]! & 0x0f);
|
|
14
|
-
bytes[8] = 0x80 | (bytes[8]! & 0x3f);
|
|
15
|
-
|
|
16
|
-
const hex = Array.from(bytes, (byte) =>
|
|
17
|
-
byte.toString(16).padStart(2, "0"),
|
|
18
|
-
).join("");
|
|
19
|
-
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Creates an RFC 9562 UUIDv7: a 48-bit millisecond timestamp, then random bits.
|
|
24
|
-
*
|
|
25
|
-
* `createUUIDv7` from `@hot-updater/plugin-core` calls `crypto.getRandomValues`
|
|
26
|
-
* unconditionally, but React Native has no Web Crypto unless the app installs a
|
|
27
|
-
* polyfill. An Insights event ID only has to tell one event from another so a
|
|
28
|
-
* retried POST is counted once, so `Math.random` is enough without one.
|
|
29
|
-
*/
|
|
30
|
-
export const createUUIDv7 = (): string => {
|
|
31
|
-
const bytes = new Uint8Array(16);
|
|
32
|
-
const { crypto } = globalThis as { crypto?: RandomValuesSource };
|
|
33
|
-
if (typeof crypto?.getRandomValues === "function") {
|
|
34
|
-
crypto.getRandomValues(bytes);
|
|
35
|
-
} else {
|
|
36
|
-
for (let index = 0; index < bytes.length; index += 1) {
|
|
37
|
-
bytes[index] = Math.floor(Math.random() * 256);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return formatUUIDv7(bytes, Date.now());
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const SHA256_K = [
|
|
44
|
-
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1,
|
|
45
|
-
0x923f82a4, 0xab1c5ed5, 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
|
46
|
-
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, 0xe49b69c1, 0xefbe4786,
|
|
47
|
-
0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
48
|
-
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147,
|
|
49
|
-
0x06ca6351, 0x14292967, 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
|
50
|
-
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, 0xa2bfe8a1, 0xa81a664b,
|
|
51
|
-
0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
52
|
-
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a,
|
|
53
|
-
0x5b9cca4f, 0x682e6ff3, 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
|
54
|
-
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
const encodeUtf8 = (value: string): number[] => {
|
|
58
|
-
const bytes: number[] = [];
|
|
59
|
-
for (const character of value) {
|
|
60
|
-
const code = character.codePointAt(0)!;
|
|
61
|
-
if (code < 0x80) {
|
|
62
|
-
bytes.push(code);
|
|
63
|
-
} else if (code < 0x800) {
|
|
64
|
-
bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f));
|
|
65
|
-
} else if (code < 0x10000) {
|
|
66
|
-
bytes.push(
|
|
67
|
-
0xe0 | (code >> 12),
|
|
68
|
-
0x80 | ((code >> 6) & 0x3f),
|
|
69
|
-
0x80 | (code & 0x3f),
|
|
70
|
-
);
|
|
71
|
-
} else {
|
|
72
|
-
bytes.push(
|
|
73
|
-
0xf0 | (code >> 18),
|
|
74
|
-
0x80 | ((code >> 12) & 0x3f),
|
|
75
|
-
0x80 | ((code >> 6) & 0x3f),
|
|
76
|
-
0x80 | (code & 0x3f),
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
return bytes;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/** SHA-256 of a string's UTF-8 bytes, since React Native has no Web Crypto. */
|
|
84
|
-
export const sha256 = (value: string): Uint8Array => {
|
|
85
|
-
const bytes = encodeUtf8(value);
|
|
86
|
-
const bitLength = bytes.length * 8;
|
|
87
|
-
bytes.push(0x80);
|
|
88
|
-
while (bytes.length % 64 !== 56) bytes.push(0);
|
|
89
|
-
for (let shift = 56; shift >= 0; shift -= 8) {
|
|
90
|
-
// The length's high word, as division, since shifts truncate to 32 bits.
|
|
91
|
-
bytes.push(
|
|
92
|
-
shift >= 32
|
|
93
|
-
? Math.floor(bitLength / 2 ** shift) & 0xff
|
|
94
|
-
: (bitLength >>> shift) & 0xff,
|
|
95
|
-
);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const hash = [
|
|
99
|
-
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a, 0x510e527f, 0x9b05688c,
|
|
100
|
-
0x1f83d9ab, 0x5be0cd19,
|
|
101
|
-
];
|
|
102
|
-
const words: number[] = Array.from({ length: 64 }, () => 0);
|
|
103
|
-
const rotate = (word: number, bits: number) =>
|
|
104
|
-
(word >>> bits) | (word << (32 - bits));
|
|
105
|
-
|
|
106
|
-
for (let offset = 0; offset < bytes.length; offset += 64) {
|
|
107
|
-
for (let index = 0; index < 16; index += 1) {
|
|
108
|
-
const at = offset + index * 4;
|
|
109
|
-
words[index] =
|
|
110
|
-
(bytes[at]! << 24) |
|
|
111
|
-
(bytes[at + 1]! << 16) |
|
|
112
|
-
(bytes[at + 2]! << 8) |
|
|
113
|
-
bytes[at + 3]!;
|
|
114
|
-
}
|
|
115
|
-
for (let index = 16; index < 64; index += 1) {
|
|
116
|
-
const low = words[index - 15]!;
|
|
117
|
-
const high = words[index - 2]!;
|
|
118
|
-
const s0 = rotate(low, 7) ^ rotate(low, 18) ^ (low >>> 3);
|
|
119
|
-
const s1 = rotate(high, 17) ^ rotate(high, 19) ^ (high >>> 10);
|
|
120
|
-
words[index] = (words[index - 16]! + s0 + words[index - 7]! + s1) | 0;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
let [a, b, c, d, e, f, g, h] = hash as [
|
|
124
|
-
number,
|
|
125
|
-
number,
|
|
126
|
-
number,
|
|
127
|
-
number,
|
|
128
|
-
number,
|
|
129
|
-
number,
|
|
130
|
-
number,
|
|
131
|
-
number,
|
|
132
|
-
];
|
|
133
|
-
for (let index = 0; index < 64; index += 1) {
|
|
134
|
-
const s1 = rotate(e, 6) ^ rotate(e, 11) ^ rotate(e, 25);
|
|
135
|
-
const choose = (e & f) ^ (~e & g);
|
|
136
|
-
const temp1 = (h + s1 + choose + SHA256_K[index]! + words[index]!) | 0;
|
|
137
|
-
const s0 = rotate(a, 2) ^ rotate(a, 13) ^ rotate(a, 22);
|
|
138
|
-
const majority = (a & b) ^ (a & c) ^ (b & c);
|
|
139
|
-
const temp2 = (s0 + majority) | 0;
|
|
140
|
-
h = g;
|
|
141
|
-
g = f;
|
|
142
|
-
f = e;
|
|
143
|
-
e = (d + temp1) | 0;
|
|
144
|
-
d = c;
|
|
145
|
-
c = b;
|
|
146
|
-
b = a;
|
|
147
|
-
a = (temp1 + temp2) | 0;
|
|
148
|
-
}
|
|
149
|
-
const results = [a, b, c, d, e, f, g, h];
|
|
150
|
-
for (let index = 0; index < 8; index += 1) {
|
|
151
|
-
hash[index] = (hash[index]! + results[index]!) | 0;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const digest = new Uint8Array(32);
|
|
156
|
-
hash.forEach((word, index) => {
|
|
157
|
-
digest[index * 4] = (word >>> 24) & 0xff;
|
|
158
|
-
digest[index * 4 + 1] = (word >>> 16) & 0xff;
|
|
159
|
-
digest[index * 4 + 2] = (word >>> 8) & 0xff;
|
|
160
|
-
digest[index * 4 + 3] = word & 0xff;
|
|
161
|
-
});
|
|
162
|
-
return digest;
|
|
163
|
-
};
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* A UUIDv7 derived from a key: its time field is `timestamp` and its random
|
|
167
|
-
* bits are the key's SHA-256, so the same key and time always give the same
|
|
168
|
-
* id, and a server that stores reports by id counts it once.
|
|
169
|
-
*/
|
|
170
|
-
export const createKeyedUUIDv7 = (key: string, timestamp: number): string =>
|
|
171
|
-
formatUUIDv7(sha256(key).slice(0, 16), timestamp);
|