@nebula-rn/host-apis 0.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.
@@ -0,0 +1,103 @@
1
+ import {
2
+ NebulaAPI,
3
+ createHostApiFailure,
4
+ createHostApiSuccess,
5
+ } from '@nebula-rn/sdk';
6
+ import type { NebulaApiInvokeResult } from '@nebula-rn/sdk';
7
+
8
+ export type HostApiEventMessage<TPayload = unknown> = {
9
+ __nebulaApiEvent: 'v1';
10
+ apiName: string;
11
+ channel: string;
12
+ subscriptionId?: string;
13
+ taskId?: string;
14
+ payload?: TPayload;
15
+ };
16
+
17
+ export function createHostBridgeId(prefix: string): string {
18
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
19
+ }
20
+
21
+ export async function emitHostApiEvent<TPayload>(
22
+ appId: string,
23
+ message: HostApiEventMessage<TPayload>,
24
+ ): Promise<void> {
25
+ await NebulaAPI.postMessageToMiniApp(
26
+ appId,
27
+ message as Record<string, unknown>,
28
+ );
29
+ }
30
+
31
+ export function createSubscriptionStartResult(subscriptionId: string) {
32
+ return createHostApiSuccess({
33
+ subscriptionId,
34
+ });
35
+ }
36
+
37
+ export function createTaskStartResult(taskId: string) {
38
+ return createHostApiSuccess({
39
+ taskId,
40
+ });
41
+ }
42
+
43
+ export function emitSubscriptionEvent<TPayload>(
44
+ appId: string,
45
+ apiName: string,
46
+ subscriptionId: string,
47
+ payload: TPayload,
48
+ ): Promise<void> {
49
+ return emitHostApiEvent(appId, {
50
+ __nebulaApiEvent: 'v1',
51
+ apiName,
52
+ channel: 'subscription',
53
+ subscriptionId,
54
+ payload,
55
+ });
56
+ }
57
+
58
+ export function emitTaskProgress<TPayload>(
59
+ appId: string,
60
+ apiName: string,
61
+ taskId: string,
62
+ payload: TPayload,
63
+ ): Promise<void> {
64
+ return emitHostApiEvent(appId, {
65
+ __nebulaApiEvent: 'v1',
66
+ apiName,
67
+ channel: 'task.progress',
68
+ taskId,
69
+ payload,
70
+ });
71
+ }
72
+
73
+ export function emitTaskHeaders<TPayload>(
74
+ appId: string,
75
+ apiName: string,
76
+ taskId: string,
77
+ payload: TPayload,
78
+ ): Promise<void> {
79
+ return emitHostApiEvent(appId, {
80
+ __nebulaApiEvent: 'v1',
81
+ apiName,
82
+ channel: 'task.headers',
83
+ taskId,
84
+ payload,
85
+ });
86
+ }
87
+
88
+ export function emitTaskResult<TPayload>(
89
+ appId: string,
90
+ apiName: string,
91
+ taskId: string,
92
+ result: NebulaApiInvokeResult<TPayload>,
93
+ ): Promise<void> {
94
+ return emitHostApiEvent(appId, {
95
+ __nebulaApiEvent: 'v1',
96
+ apiName,
97
+ channel: 'task.result',
98
+ taskId,
99
+ payload: result,
100
+ });
101
+ }
102
+
103
+ export { createHostApiFailure, createHostApiSuccess };
package/src/index.ts ADDED
@@ -0,0 +1,23 @@
1
+ import { previewImageHostApi } from './PreviewImageHostBridge';
2
+ import { scanCodeHostApi } from './ScanCodeHostBridge';
3
+ import { coreHostApis, saveMediaHost } from './coreHostApis';
4
+ import {
5
+ downloadFileHostApi,
6
+ downloadFileHost,
7
+ uploadFileHostApi,
8
+ uploadFileHost,
9
+ } from './taskHosts';
10
+
11
+ export { previewImageHostApi, scanCodeHostApi, coreHostApis, saveMediaHost };
12
+ export {
13
+ downloadFileHostApi,
14
+ downloadFileHost,
15
+ uploadFileHostApi,
16
+ uploadFileHost,
17
+ };
18
+
19
+ export const defaultHostApis = [
20
+ scanCodeHostApi,
21
+ previewImageHostApi,
22
+ ...coreHostApis,
23
+ ];
@@ -0,0 +1,12 @@
1
+ import { createHostModalChannel } from '@nebula-rn/sdk';
2
+
3
+ export type PreviewImageRequest = {
4
+ urls: string[];
5
+ current?: string;
6
+ showMenu?: boolean;
7
+ saveMediaText?: string;
8
+ cancelText?: string;
9
+ };
10
+
11
+ export const previewImageChannel =
12
+ createHostModalChannel<PreviewImageRequest>();
@@ -0,0 +1,8 @@
1
+ import { createHostModalChannel } from '@nebula-rn/sdk';
2
+
3
+ export type ScanCodeRequest = {
4
+ onlyFromCamera?: boolean;
5
+ scanTypes: string[];
6
+ };
7
+
8
+ export const scanCodeChannel = createHostModalChannel<ScanCodeRequest>();
@@ -0,0 +1,269 @@
1
+ import * as RNFS from '@dr.pogodin/react-native-fs';
2
+ import { createHostApiFeature } from '@nebula-rn/sdk';
3
+ import type { NebulaHostApiDescriptionMap } from '@nebula-rn/sdk';
4
+ import {
5
+ createTaskStartResult,
6
+ emitTaskHeaders,
7
+ emitTaskProgress,
8
+ emitTaskResult,
9
+ } from './hostApiBridge';
10
+
11
+ type DownloadFileHostOption = {
12
+ url: string;
13
+ header?: Record<string, string>;
14
+ timeout?: number;
15
+ filePath?: string;
16
+ };
17
+
18
+ type DownloadFileHostResult = {
19
+ tempFilePath: string;
20
+ statusCode: number;
21
+ };
22
+
23
+ type UploadFileHostOption = {
24
+ url: string;
25
+ filePath: string;
26
+ name: string;
27
+ header?: Record<string, string>;
28
+ formData?: Record<string, string>;
29
+ timeout?: number;
30
+ };
31
+
32
+ type UploadFileHostResult = {
33
+ data: string;
34
+ statusCode: number;
35
+ };
36
+
37
+ const activeDownloads = new Map<string, number>();
38
+ const activeUploads = new Map<string, number>();
39
+
40
+ const TASK_API_DESCRIPTIONS: NebulaHostApiDescriptionMap = {
41
+ 'downloadFile.start': {
42
+ summary: 'Start a host-managed file download task with progress events.',
43
+ tags: ['file', 'task', 'network'],
44
+ },
45
+ 'downloadFile.abort': {
46
+ summary: 'Abort an in-flight host download task by task id.',
47
+ tags: ['file', 'task', 'network'],
48
+ },
49
+ 'uploadFile.start': {
50
+ summary: 'Start a host-managed file upload task with progress events.',
51
+ tags: ['file', 'task', 'network'],
52
+ },
53
+ 'uploadFile.abort': {
54
+ summary: 'Abort an in-flight host upload task by task id.',
55
+ tags: ['file', 'task', 'network'],
56
+ },
57
+ };
58
+
59
+ export const downloadFileHost = async (
60
+ options: DownloadFileHostOption,
61
+ ): Promise<DownloadFileHostResult> => {
62
+ const { url, header = {}, timeout = 60000, filePath } = options;
63
+ const destPath =
64
+ filePath || `${RNFS.TemporaryDirectoryPath}/${Date.now()}_download`;
65
+
66
+ const result = await RNFS.downloadFile({
67
+ fromUrl: url,
68
+ toFile: destPath,
69
+ headers: header,
70
+ connectionTimeout: timeout,
71
+ readTimeout: timeout,
72
+ }).promise;
73
+
74
+ return {
75
+ tempFilePath: destPath,
76
+ statusCode: result.statusCode,
77
+ };
78
+ };
79
+
80
+ export const uploadFileHost = async (
81
+ options: UploadFileHostOption,
82
+ ): Promise<UploadFileHostResult> => {
83
+ const nativeTask = RNFS.uploadFiles({
84
+ toUrl: options.url,
85
+ files: [
86
+ {
87
+ name: options.name,
88
+ filename: options.filePath.split('/').pop() || 'file',
89
+ filepath: options.filePath,
90
+ },
91
+ ],
92
+ headers: options.header ?? {},
93
+ fields: options.formData ?? {},
94
+ method: 'POST',
95
+ });
96
+
97
+ const result = await nativeTask.promise;
98
+ return {
99
+ data: result.body,
100
+ statusCode: result.statusCode,
101
+ };
102
+ };
103
+
104
+ export const downloadFileHostApi = [
105
+ createHostApiFeature({
106
+ apiName: 'downloadFile.start',
107
+ description: TASK_API_DESCRIPTIONS['downloadFile.start'],
108
+ async handle(payload, context) {
109
+ const taskId = `download-${Date.now()}-${Math.random()
110
+ .toString(36)
111
+ .slice(2, 10)}`;
112
+ const options = payload as DownloadFileHostOption;
113
+ const destPath =
114
+ options.filePath ||
115
+ `${RNFS.TemporaryDirectoryPath}/${Date.now()}_download`;
116
+
117
+ const nativeTask = RNFS.downloadFile({
118
+ fromUrl: options.url,
119
+ toFile: destPath,
120
+ headers: options.header ?? {},
121
+ connectionTimeout: options.timeout ?? 60000,
122
+ readTimeout: options.timeout ?? 60000,
123
+ begin: res => {
124
+ emitTaskHeaders(context.appId, 'downloadFile', taskId, {
125
+ header: res.headers as Record<string, string>,
126
+ }).catch(() => {});
127
+ },
128
+ progress: res => {
129
+ emitTaskProgress(context.appId, 'downloadFile', taskId, {
130
+ progress: Math.floor((res.bytesWritten / res.contentLength) * 100),
131
+ totalBytesWritten: res.bytesWritten,
132
+ totalBytesExpectedToWrite: res.contentLength,
133
+ }).catch(() => {});
134
+ },
135
+ });
136
+
137
+ activeDownloads.set(taskId, nativeTask.jobId);
138
+ nativeTask.promise
139
+ .then(res =>
140
+ emitTaskResult(context.appId, 'downloadFile', taskId, {
141
+ ok: true,
142
+ data: {
143
+ tempFilePath: destPath,
144
+ statusCode: res.statusCode,
145
+ },
146
+ }),
147
+ )
148
+ .catch(error =>
149
+ emitTaskResult(context.appId, 'downloadFile', taskId, {
150
+ ok: false,
151
+ error: {
152
+ code: 'DOWNLOAD_FAILED',
153
+ message:
154
+ error instanceof Error
155
+ ? error.message
156
+ : 'downloadFile:fail host download failed',
157
+ },
158
+ }),
159
+ )
160
+ .finally(() => {
161
+ activeDownloads.delete(taskId);
162
+ });
163
+
164
+ return createTaskStartResult(taskId);
165
+ },
166
+ }),
167
+ createHostApiFeature({
168
+ apiName: 'downloadFile.abort',
169
+ description: TASK_API_DESCRIPTIONS['downloadFile.abort'],
170
+ async handle(payload) {
171
+ const taskId = String(payload.taskId ?? '');
172
+ const jobId = activeDownloads.get(taskId);
173
+ if (jobId) {
174
+ RNFS.stopDownload(jobId);
175
+ activeDownloads.delete(taskId);
176
+ }
177
+ return {
178
+ ok: true,
179
+ data: null,
180
+ };
181
+ },
182
+ }),
183
+ ];
184
+
185
+ export const uploadFileHostApi = [
186
+ createHostApiFeature({
187
+ apiName: 'uploadFile.start',
188
+ description: TASK_API_DESCRIPTIONS['uploadFile.start'],
189
+ async handle(payload, context) {
190
+ const taskId = `upload-${Date.now()}-${Math.random()
191
+ .toString(36)
192
+ .slice(2, 10)}`;
193
+ const options = payload as UploadFileHostOption;
194
+
195
+ const nativeTask = RNFS.uploadFiles({
196
+ toUrl: options.url,
197
+ files: [
198
+ {
199
+ name: options.name,
200
+ filename: options.filePath.split('/').pop() || 'file',
201
+ filepath: options.filePath,
202
+ },
203
+ ],
204
+ headers: options.header ?? {},
205
+ fields: options.formData ?? {},
206
+ method: 'POST',
207
+ progress: res => {
208
+ emitTaskProgress(context.appId, 'uploadFile', taskId, {
209
+ progress: Math.floor(
210
+ (res.totalBytesSent / res.totalBytesExpectedToSend) * 100,
211
+ ),
212
+ totalBytesSent: res.totalBytesSent,
213
+ totalBytesExpectedToSend: res.totalBytesExpectedToSend,
214
+ }).catch(() => {});
215
+ },
216
+ });
217
+
218
+ activeUploads.set(taskId, nativeTask.jobId);
219
+ nativeTask.promise
220
+ .then(res =>
221
+ Promise.all([
222
+ emitTaskHeaders(context.appId, 'uploadFile', taskId, {
223
+ header: res.headers as Record<string, string>,
224
+ }),
225
+ emitTaskResult(context.appId, 'uploadFile', taskId, {
226
+ ok: true,
227
+ data: {
228
+ data: res.body,
229
+ statusCode: res.statusCode,
230
+ },
231
+ }),
232
+ ]),
233
+ )
234
+ .catch(error =>
235
+ emitTaskResult(context.appId, 'uploadFile', taskId, {
236
+ ok: false,
237
+ error: {
238
+ code: 'UPLOAD_FAILED',
239
+ message:
240
+ error instanceof Error
241
+ ? error.message
242
+ : 'uploadFile:fail host upload failed',
243
+ },
244
+ }),
245
+ )
246
+ .finally(() => {
247
+ activeUploads.delete(taskId);
248
+ });
249
+
250
+ return createTaskStartResult(taskId);
251
+ },
252
+ }),
253
+ createHostApiFeature({
254
+ apiName: 'uploadFile.abort',
255
+ description: TASK_API_DESCRIPTIONS['uploadFile.abort'],
256
+ async handle(payload) {
257
+ const taskId = String(payload.taskId ?? '');
258
+ const jobId = activeUploads.get(taskId);
259
+ if (jobId) {
260
+ RNFS.stopUpload(jobId);
261
+ activeUploads.delete(taskId);
262
+ }
263
+ return {
264
+ ok: true,
265
+ data: null,
266
+ };
267
+ },
268
+ }),
269
+ ];