@take-out/native-hot-update 0.0.36

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.
Files changed (50) hide show
  1. package/README.md +171 -0
  2. package/dist/cjs/createHotUpdater.cjs +183 -0
  3. package/dist/cjs/createHotUpdater.js +146 -0
  4. package/dist/cjs/createHotUpdater.js.map +6 -0
  5. package/dist/cjs/createHotUpdater.native.js +213 -0
  6. package/dist/cjs/createHotUpdater.native.js.map +1 -0
  7. package/dist/cjs/index.cjs +26 -0
  8. package/dist/cjs/index.js +21 -0
  9. package/dist/cjs/index.js.map +6 -0
  10. package/dist/cjs/index.native.js +29 -0
  11. package/dist/cjs/index.native.js.map +1 -0
  12. package/dist/cjs/mmkv.cjs +32 -0
  13. package/dist/cjs/mmkv.js +27 -0
  14. package/dist/cjs/mmkv.js.map +6 -0
  15. package/dist/cjs/mmkv.native.js +41 -0
  16. package/dist/cjs/mmkv.native.js.map +1 -0
  17. package/dist/cjs/types.cjs +16 -0
  18. package/dist/cjs/types.js +14 -0
  19. package/dist/cjs/types.js.map +6 -0
  20. package/dist/cjs/types.native.js +19 -0
  21. package/dist/cjs/types.native.js.map +1 -0
  22. package/dist/esm/createHotUpdater.js +129 -0
  23. package/dist/esm/createHotUpdater.js.map +6 -0
  24. package/dist/esm/createHotUpdater.mjs +149 -0
  25. package/dist/esm/createHotUpdater.mjs.map +1 -0
  26. package/dist/esm/createHotUpdater.native.js +176 -0
  27. package/dist/esm/createHotUpdater.native.js.map +1 -0
  28. package/dist/esm/index.js +5 -0
  29. package/dist/esm/index.js.map +6 -0
  30. package/dist/esm/index.mjs +3 -0
  31. package/dist/esm/index.mjs.map +1 -0
  32. package/dist/esm/index.native.js +3 -0
  33. package/dist/esm/index.native.js.map +1 -0
  34. package/dist/esm/mmkv.js +11 -0
  35. package/dist/esm/mmkv.js.map +6 -0
  36. package/dist/esm/mmkv.mjs +9 -0
  37. package/dist/esm/mmkv.mjs.map +1 -0
  38. package/dist/esm/mmkv.native.js +15 -0
  39. package/dist/esm/mmkv.native.js.map +1 -0
  40. package/dist/esm/types.js +1 -0
  41. package/dist/esm/types.js.map +6 -0
  42. package/dist/esm/types.mjs +2 -0
  43. package/dist/esm/types.mjs.map +1 -0
  44. package/dist/esm/types.native.js +2 -0
  45. package/dist/esm/types.native.js.map +1 -0
  46. package/package.json +63 -0
  47. package/src/createHotUpdater.ts +240 -0
  48. package/src/index.ts +2 -0
  49. package/src/mmkv.ts +20 -0
  50. package/src/types.ts +62 -0
@@ -0,0 +1,240 @@
1
+ import {
2
+ getUpdateSource,
3
+ HotUpdater,
4
+ useHotUpdaterStore,
5
+ } from '@hot-updater/react-native'
6
+ import * as Application from 'expo-application'
7
+ import { useEffect, useRef, useState } from 'react'
8
+ import { Alert } from 'react-native'
9
+
10
+ import type { HotUpdaterConfig, HotUpdaterInstance, UpdateInfo } from './types'
11
+
12
+ const INITIAL_OTA_ID = '00000000-0000-0000-0000-000000000000'
13
+ const BUNDLE_ID_KEY_PREFIX = 'hotUpdater.bundleId'
14
+ const PRE_RELEASE_BUNDLE_ID_KEY = 'hotUpdater.preReleaseBundleId'
15
+
16
+ export function createHotUpdater(config: HotUpdaterConfig): HotUpdaterInstance {
17
+ const { serverUrl, storage, updateStrategy = 'appVersion' } = config
18
+
19
+ let isUpdatePending = false
20
+
21
+ const getAppliedOta = (): string | null => {
22
+ const id = HotUpdater.getBundleId()
23
+ if (id === INITIAL_OTA_ID) return null
24
+ if (id === HotUpdater.getMinBundleId()) return null
25
+ return id
26
+ }
27
+
28
+ const getShortOtaId = (): string | null => {
29
+ const fullId = getAppliedOta()
30
+ return fullId ? fullId.slice(24) : null
31
+ }
32
+
33
+ const getIsUpdatePending = (): boolean => {
34
+ return isUpdatePending
35
+ }
36
+
37
+ const getPreReleaseBundleId = (): string | undefined => {
38
+ return storage.get(PRE_RELEASE_BUNDLE_ID_KEY)
39
+ }
40
+
41
+ const handleUpdateDownloaded = (
42
+ id: string,
43
+ options: { isPreRelease?: boolean } = {}
44
+ ) => {
45
+ const appVersion = Application.nativeApplicationVersion
46
+ if (appVersion) {
47
+ storage.set(`${BUNDLE_ID_KEY_PREFIX}.${appVersion}`, id)
48
+ }
49
+
50
+ if (options.isPreRelease) {
51
+ storage.set(PRE_RELEASE_BUNDLE_ID_KEY, id)
52
+ } else {
53
+ storage.delete(PRE_RELEASE_BUNDLE_ID_KEY)
54
+ }
55
+
56
+ isUpdatePending = true
57
+ }
58
+
59
+ const useOtaUpdater = (
60
+ options: {
61
+ enabled?: boolean
62
+ onUpdateDownloaded?: (info: UpdateInfo) => void
63
+ onError?: (error: unknown) => void
64
+ } = {}
65
+ ) => {
66
+ const {
67
+ enabled = true,
68
+ onUpdateDownloaded: onUpdateDownloadedCallback,
69
+ onError,
70
+ } = options
71
+
72
+ const progress = useHotUpdaterStore((state) => state.progress)
73
+ const isUpdating = progress > 0.01
74
+
75
+ const [isUserClearedForAccess, setIsUserClearedForAccess] = useState(false)
76
+ const isUserClearedForAccessRef = useRef(isUserClearedForAccess)
77
+ isUserClearedForAccessRef.current = isUserClearedForAccess
78
+
79
+ // 5s timeout if no update started
80
+ useEffect(() => {
81
+ if (!isUpdating) {
82
+ const timer = setTimeout(() => {
83
+ if (!isUserClearedForAccessRef.current) {
84
+ setIsUserClearedForAccess(true)
85
+ }
86
+ }, 5000)
87
+ return () => clearTimeout(timer)
88
+ }
89
+ }, [isUpdating])
90
+
91
+ // 20s hard timeout
92
+ useEffect(() => {
93
+ const timer = setTimeout(() => {
94
+ if (!isUserClearedForAccessRef.current) {
95
+ setIsUserClearedForAccess(true)
96
+ }
97
+ }, 20000)
98
+ return () => clearTimeout(timer)
99
+ }, [])
100
+
101
+ // Update check
102
+ useEffect(() => {
103
+ let shouldContinue = true
104
+
105
+ ;(async () => {
106
+ try {
107
+ if (!enabled) {
108
+ setIsUserClearedForAccess(true)
109
+ return
110
+ }
111
+
112
+ if (!shouldContinue) return
113
+
114
+ const updateInfo = await HotUpdater.checkForUpdate({
115
+ source: getUpdateSource(serverUrl, { updateStrategy }),
116
+ })
117
+
118
+ if (!updateInfo) {
119
+ setIsUserClearedForAccess(true)
120
+ return
121
+ }
122
+
123
+ if (!shouldContinue) return
124
+
125
+ // Handle rollback for pre-release bundles
126
+ if (updateInfo.status === 'ROLLBACK') {
127
+ const preReleaseBundleId = getPreReleaseBundleId()
128
+ const currentBundleId = HotUpdater.getBundleId()
129
+
130
+ if (
131
+ preReleaseBundleId &&
132
+ preReleaseBundleId === currentBundleId &&
133
+ currentBundleId > updateInfo.id
134
+ ) {
135
+ Alert.alert(
136
+ 'Update Skipped',
137
+ 'Skipped rollback because you are using a newer pre-release bundle.'
138
+ )
139
+ setIsUserClearedForAccess(true)
140
+ return
141
+ }
142
+ }
143
+
144
+ if (!updateInfo.shouldForceUpdate) {
145
+ setIsUserClearedForAccess(true)
146
+ }
147
+
148
+ if (!shouldContinue) return
149
+
150
+ await updateInfo.updateBundle()
151
+ handleUpdateDownloaded(updateInfo.id)
152
+
153
+ const info: UpdateInfo = {
154
+ id: updateInfo.id,
155
+ isCriticalUpdate: updateInfo.shouldForceUpdate,
156
+ fileUrl: updateInfo.fileUrl,
157
+ message: updateInfo.message,
158
+ }
159
+ onUpdateDownloadedCallback?.(info)
160
+
161
+ if (!shouldContinue) return
162
+
163
+ if (updateInfo.shouldForceUpdate) {
164
+ if (!isUserClearedForAccessRef.current) {
165
+ HotUpdater.reload()
166
+ return
167
+ }
168
+ Alert.alert(
169
+ 'Update Downloaded',
170
+ 'An important update has been downloaded. Reload now to apply it?',
171
+ [
172
+ { text: 'Later', style: 'cancel' },
173
+ { text: 'Reload Now', onPress: () => HotUpdater.reload() },
174
+ ]
175
+ )
176
+ }
177
+ } catch (error) {
178
+ onError?.(error)
179
+ setIsUserClearedForAccess(true)
180
+ } finally {
181
+ if (!isUserClearedForAccessRef.current) {
182
+ setIsUserClearedForAccess(true)
183
+ }
184
+ }
185
+ })()
186
+
187
+ return () => {
188
+ shouldContinue = false
189
+ }
190
+ }, [enabled, onUpdateDownloadedCallback, onError])
191
+
192
+ return {
193
+ userClearedForAccess: isUserClearedForAccess,
194
+ progress,
195
+ isUpdatePending: getIsUpdatePending(),
196
+ }
197
+ }
198
+
199
+ const checkForUpdate = async (
200
+ options: { channel?: string; isPreRelease?: boolean } = {}
201
+ ) => {
202
+ const { channel, isPreRelease = false } = options
203
+
204
+ const updateSource = getUpdateSource(serverUrl, { updateStrategy })
205
+
206
+ const finalSource = channel
207
+ ? (params: any) => updateSource({ ...params, channel })
208
+ : updateSource
209
+
210
+ const updateInfo = await HotUpdater.checkForUpdate({
211
+ source: finalSource,
212
+ })
213
+
214
+ if (updateInfo) {
215
+ await updateInfo.updateBundle()
216
+ handleUpdateDownloaded(updateInfo.id, { isPreRelease })
217
+
218
+ return {
219
+ id: updateInfo.id,
220
+ isCriticalUpdate: updateInfo.shouldForceUpdate,
221
+ fileUrl: updateInfo.fileUrl,
222
+ message: updateInfo.message,
223
+ }
224
+ }
225
+
226
+ return null
227
+ }
228
+
229
+ return {
230
+ useOtaUpdater,
231
+ checkForUpdate,
232
+ getAppliedOta,
233
+ getShortOtaId,
234
+ getIsUpdatePending,
235
+ reload: () => HotUpdater.reload(),
236
+ getBundleId: () => HotUpdater.getBundleId(),
237
+ getMinBundleId: () => HotUpdater.getMinBundleId(),
238
+ getChannel: () => HotUpdater.getChannel(),
239
+ }
240
+ }
package/src/index.ts ADDED
@@ -0,0 +1,2 @@
1
+ export { createHotUpdater } from './createHotUpdater'
2
+ export type * from './types'
package/src/mmkv.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { HotUpdateStorage } from './types'
2
+ import type { MMKV } from 'react-native-mmkv'
3
+
4
+ /**
5
+ * Create a storage adapter for MMKV.
6
+ *
7
+ * @example
8
+ * import { MMKV } from 'react-native-mmkv'
9
+ * import { createMMKVStorage } from '@take-out/native-hot-update/mmkv'
10
+ *
11
+ * const mmkv = new MMKV({ id: 'hot-updater' })
12
+ * const storage = createMMKVStorage(mmkv)
13
+ */
14
+ export function createMMKVStorage(mmkv: MMKV): HotUpdateStorage {
15
+ return {
16
+ get: (key: string) => mmkv.getString(key),
17
+ set: (key: string, value: string) => mmkv.set(key, value),
18
+ delete: (key: string) => mmkv.delete(key),
19
+ }
20
+ }
package/src/types.ts ADDED
@@ -0,0 +1,62 @@
1
+ /** Simple key-value storage interface */
2
+ export interface HotUpdateStorage {
3
+ get(key: string): string | undefined
4
+ set(key: string, value: string): void
5
+ delete(key: string): void
6
+ }
7
+
8
+ export interface HotUpdaterConfig {
9
+ /** Hot update server URL */
10
+ serverUrl: string
11
+ /** Storage for persisting update state */
12
+ storage: HotUpdateStorage
13
+ /** Update strategy (default: 'appVersion') */
14
+ updateStrategy?: 'appVersion' | 'fingerprint'
15
+ }
16
+
17
+ export interface UpdateInfo {
18
+ id: string
19
+ isCriticalUpdate: boolean
20
+ fileUrl: string | null
21
+ message: string | null
22
+ }
23
+
24
+ export interface HotUpdaterInstance {
25
+ /** React hook for automatic update checking */
26
+ useOtaUpdater: (options?: {
27
+ enabled?: boolean
28
+ onUpdateDownloaded?: (info: UpdateInfo) => void
29
+ onError?: (error: unknown) => void
30
+ }) => {
31
+ userClearedForAccess: boolean
32
+ progress: number
33
+ isUpdatePending: boolean
34
+ }
35
+
36
+ /** Manually check for updates (for dev/testing) */
37
+ checkForUpdate: (options?: {
38
+ channel?: string
39
+ isPreRelease?: boolean
40
+ }) => Promise<UpdateInfo | null>
41
+
42
+ /** Get currently applied OTA bundle ID (null if native) */
43
+ getAppliedOta: () => string | null
44
+
45
+ /** Get short version of OTA ID */
46
+ getShortOtaId: () => string | null
47
+
48
+ /** Check if update is pending (will apply on restart) */
49
+ getIsUpdatePending: () => boolean
50
+
51
+ /** Reload the app to apply update */
52
+ reload: () => void
53
+
54
+ /** Get current bundle ID */
55
+ getBundleId: () => string
56
+
57
+ /** Get minimum bundle ID (native build time) */
58
+ getMinBundleId: () => string
59
+
60
+ /** Get current channel */
61
+ getChannel: () => string
62
+ }