@nebula-rn/sdk 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,364 @@
1
+ import React from 'react';
2
+ import { AppRegistry } from 'react-native';
3
+ import { NebulaAPI } from './NebulaAPI';
4
+ import type {
5
+ NebulaApiInvokeResult,
6
+ NebulaHostApiDescription,
7
+ NebulaHostApiHandler,
8
+ } from './NebulaAPI';
9
+
10
+ export function createHostApiFailure(
11
+ code: string,
12
+ message: string,
13
+ ): NebulaApiInvokeResult {
14
+ return {
15
+ ok: false,
16
+ error: {
17
+ code,
18
+ message,
19
+ },
20
+ };
21
+ }
22
+
23
+ export function createHostApiSuccess<TData>(
24
+ data: TData,
25
+ ): NebulaApiInvokeResult<TData> {
26
+ return {
27
+ ok: true,
28
+ data,
29
+ };
30
+ }
31
+
32
+ type PendingRequest<TRequest extends object> = TRequest & {
33
+ resolve: (result: NebulaApiInvokeResult) => void;
34
+ };
35
+
36
+ type Listener<TRequest extends object> = (request: TRequest | null) => void;
37
+
38
+ export type HostModalChannel<TRequest extends object> = {
39
+ clear: () => void;
40
+ getCurrent: () => TRequest | null;
41
+ open: (request: TRequest) => Promise<NebulaApiInvokeResult>;
42
+ settle: (result: NebulaApiInvokeResult) => void;
43
+ subscribe: (listener: Listener<TRequest>) => () => void;
44
+ };
45
+
46
+ export type NebulaHostFeature = {
47
+ name: string;
48
+ description?: NebulaHostApiDescription;
49
+ register: () => void | (() => void);
50
+ };
51
+
52
+ export function createHostModalChannel<
53
+ TRequest extends object,
54
+ >(): HostModalChannel<TRequest> {
55
+ let pendingRequest: PendingRequest<TRequest> | null = null;
56
+ const listeners = new Set<Listener<TRequest>>();
57
+
58
+ const notify = () => {
59
+ listeners.forEach(listener => listener(pendingRequest));
60
+ };
61
+
62
+ return {
63
+ clear() {
64
+ pendingRequest = null;
65
+ notify();
66
+ },
67
+ getCurrent() {
68
+ return pendingRequest;
69
+ },
70
+ open(request) {
71
+ return new Promise(resolve => {
72
+ pendingRequest = {
73
+ ...request,
74
+ resolve: result => {
75
+ pendingRequest = null;
76
+ notify();
77
+ resolve(result);
78
+ },
79
+ };
80
+ notify();
81
+ });
82
+ },
83
+ settle(result) {
84
+ pendingRequest?.resolve(result);
85
+ },
86
+ subscribe(listener) {
87
+ listeners.add(listener);
88
+ listener(pendingRequest);
89
+ return () => {
90
+ listeners.delete(listener);
91
+ };
92
+ },
93
+ };
94
+ }
95
+
96
+ type HostModalApiOptions<TPayload extends object, TRequest extends object> = {
97
+ apiName: string;
98
+ description?: NebulaHostApiDescription;
99
+ component: React.ComponentType<any>;
100
+ channel: HostModalChannel<TRequest>;
101
+ createRequest: (payload: TPayload) => TRequest;
102
+ modalProps?:
103
+ | Record<string, unknown>
104
+ | ((payload: TPayload, request: TRequest) => Record<string, unknown>);
105
+ onBeforeOpen?: (
106
+ payload: TPayload,
107
+ ) => Promise<NebulaApiInvokeResult | null | void>;
108
+ onUnmountErrorMessage?: string;
109
+ version?: string;
110
+ };
111
+
112
+ export type RegisterHostModalApiOptions<
113
+ TPayload extends object,
114
+ TRequest extends object,
115
+ > = HostModalApiOptions<TPayload, TRequest>;
116
+
117
+ export type RegisterHostApiOptions = {
118
+ apiName: string;
119
+ supported?: NebulaHostApiHandler['supported'];
120
+ version?: string;
121
+ description?: NebulaHostApiDescription;
122
+ handle: NebulaHostApiHandler['handle'];
123
+ };
124
+
125
+ export type CreateMockHostApiOptions = {
126
+ apiName: string;
127
+ version?: string;
128
+ description?: NebulaHostApiDescription;
129
+ delayMs?: number;
130
+ result?:
131
+ | unknown
132
+ | ((payload: Record<string, unknown>) => unknown | Promise<unknown>);
133
+ error?:
134
+ | {
135
+ code: string;
136
+ message: string;
137
+ details?: Record<string, unknown>;
138
+ }
139
+ | ((payload: Record<string, unknown>) =>
140
+ | {
141
+ code: string;
142
+ message: string;
143
+ details?: Record<string, unknown>;
144
+ }
145
+ | Promise<{
146
+ code: string;
147
+ message: string;
148
+ details?: Record<string, unknown>;
149
+ }>);
150
+ };
151
+
152
+ function getHostModalModuleName(apiName: string): string {
153
+ const normalizedName = apiName.replace(/[^a-zA-Z0-9_]+/g, '_');
154
+ return `NebulaHostModal_${normalizedName}`;
155
+ }
156
+
157
+ function createHostModalApiHandler<
158
+ TPayload extends object,
159
+ TRequest extends object,
160
+ >({
161
+ apiName,
162
+ channel,
163
+ createRequest,
164
+ modalProps,
165
+ onBeforeOpen,
166
+ version = '1.0',
167
+ description,
168
+ }: Omit<HostModalApiOptions<TPayload, TRequest>, 'component'>) {
169
+ const modalModuleName = getHostModalModuleName(apiName);
170
+ let requestInFlight = false;
171
+
172
+ return {
173
+ version,
174
+ description,
175
+ handle: async (payload: Record<string, unknown>) => {
176
+ if (requestInFlight) {
177
+ return createHostApiFailure(
178
+ 'SCAN_FAILED',
179
+ `${apiName}:fail another request is already in progress`,
180
+ );
181
+ }
182
+
183
+ const typedPayload = payload as TPayload;
184
+ requestInFlight = true;
185
+
186
+ try {
187
+ const precheckResult = onBeforeOpen
188
+ ? await onBeforeOpen(typedPayload)
189
+ : null;
190
+ if (precheckResult) {
191
+ return precheckResult;
192
+ }
193
+
194
+ const request = createRequest(typedPayload);
195
+ const resultPromise = channel.open(request);
196
+ const resolvedModalProps =
197
+ typeof modalProps === 'function'
198
+ ? modalProps(typedPayload, request)
199
+ : (modalProps ?? {});
200
+
201
+ await NebulaAPI.presentHostModal(modalModuleName, {
202
+ __transparentBackground: true,
203
+ ...resolvedModalProps,
204
+ });
205
+
206
+ return await resultPromise;
207
+ } catch (error) {
208
+ channel.settle(
209
+ createHostApiFailure(
210
+ 'SCAN_FAILED',
211
+ `${apiName}:fail host modal flow interrupted`,
212
+ ),
213
+ );
214
+ return createHostApiFailure(
215
+ 'SCAN_FAILED',
216
+ error instanceof Error
217
+ ? error.message
218
+ : `${apiName}:fail unable to present host modal`,
219
+ );
220
+ } finally {
221
+ requestInFlight = false;
222
+ }
223
+ },
224
+ cleanup: () => {
225
+ requestInFlight = false;
226
+ },
227
+ };
228
+ }
229
+
230
+ export function registerHostModalApi<
231
+ TPayload extends object,
232
+ TRequest extends object,
233
+ >({
234
+ apiName,
235
+ component,
236
+ channel,
237
+ createRequest,
238
+ modalProps,
239
+ onBeforeOpen,
240
+ onUnmountErrorMessage,
241
+ version = '1.0',
242
+ description,
243
+ }: RegisterHostModalApiOptions<TPayload, TRequest>): () => void {
244
+ const modalModuleName = getHostModalModuleName(apiName);
245
+ AppRegistry.registerComponent(modalModuleName, () => component);
246
+
247
+ const handler = createHostModalApiHandler({
248
+ apiName,
249
+ channel,
250
+ createRequest,
251
+ modalProps,
252
+ onBeforeOpen,
253
+ version,
254
+ description,
255
+ });
256
+
257
+ NebulaAPI.registerApiHandler(apiName, {
258
+ version: handler.version,
259
+ description: handler.description,
260
+ handle: handler.handle,
261
+ });
262
+
263
+ return () => {
264
+ NebulaAPI.unregisterApiHandler(apiName);
265
+ handler.cleanup();
266
+ channel.settle(
267
+ createHostApiFailure(
268
+ 'SCAN_FAILED',
269
+ onUnmountErrorMessage ??
270
+ `${apiName}:fail host modal was unmounted before completion`,
271
+ ),
272
+ );
273
+ NebulaAPI.dismissHostModal().catch(() => {});
274
+ };
275
+ }
276
+
277
+ export function createHostApiFeature({
278
+ apiName,
279
+ supported,
280
+ version = '1.0',
281
+ description,
282
+ handle,
283
+ }: RegisterHostApiOptions): NebulaHostFeature {
284
+ return {
285
+ name: apiName,
286
+ description,
287
+ register() {
288
+ NebulaAPI.registerApiHandler(apiName, {
289
+ version,
290
+ supported,
291
+ description,
292
+ handle,
293
+ });
294
+
295
+ return () => {
296
+ NebulaAPI.unregisterApiHandler(apiName);
297
+ };
298
+ },
299
+ };
300
+ }
301
+
302
+ export function createMockHostApiFeature({
303
+ apiName,
304
+ version = '1.0.0',
305
+ description,
306
+ delayMs = 0,
307
+ result,
308
+ error,
309
+ }: CreateMockHostApiOptions): NebulaHostFeature {
310
+ return createHostApiFeature({
311
+ apiName,
312
+ version,
313
+ description,
314
+ handle: async payload => {
315
+ if (delayMs > 0) {
316
+ await new Promise(resolve => setTimeout(resolve, delayMs));
317
+ }
318
+
319
+ if (error) {
320
+ const resolvedError =
321
+ typeof error === 'function' ? await error(payload) : error;
322
+ return createHostApiFailure(resolvedError.code, resolvedError.message);
323
+ }
324
+
325
+ const resolvedResult =
326
+ typeof result === 'function' ? await result(payload) : result;
327
+ return createHostApiSuccess(
328
+ typeof resolvedResult === 'undefined' ? null : resolvedResult,
329
+ );
330
+ },
331
+ });
332
+ }
333
+
334
+ export function createHostModalApiFeature<
335
+ TPayload extends object,
336
+ TRequest extends object,
337
+ >(options: RegisterHostModalApiOptions<TPayload, TRequest>): NebulaHostFeature {
338
+ return {
339
+ name: options.apiName,
340
+ description: options.description,
341
+ register() {
342
+ return registerHostModalApi(options);
343
+ },
344
+ };
345
+ }
346
+
347
+ export function createHostModalApiBridge<
348
+ TPayload extends object,
349
+ TRequest extends object,
350
+ >(options: HostModalApiOptions<TPayload, TRequest>) {
351
+ return function HostModalApiBridge(): React.ReactElement | null {
352
+ React.useEffect(() => registerHostModalApi(options), []);
353
+
354
+ return null;
355
+ };
356
+ }
357
+
358
+ export function registerHostModalComponent<P extends object>(
359
+ moduleName: string,
360
+ component: React.ComponentType<P>,
361
+ ): string {
362
+ AppRegistry.registerComponent(moduleName, () => component);
363
+ return moduleName;
364
+ }
@@ -0,0 +1,209 @@
1
+ import type {
2
+ NebulaHostApiDescriptionMap,
3
+ NebulaHostApiHandler,
4
+ NebulaHostCapabilityMap,
5
+ NebulaProtocolResponse,
6
+ } from './nebulaTypes';
7
+ import {
8
+ addNebulaEventListener,
9
+ isProtocolRequest,
10
+ NEBULA_HOST_MESSAGE_EVENT,
11
+ } from './nebulaNative';
12
+
13
+ const hostApiRegistry = new Map<string, NebulaHostApiHandler>();
14
+ let hostApiServerStarted = false;
15
+ let hostApiServerUnsubscribe: (() => void) | null = null;
16
+
17
+ export async function resolveHostCapabilities(): Promise<NebulaHostCapabilityMap> {
18
+ const entries = await Promise.all(
19
+ Array.from(hostApiRegistry.entries()).map(async ([apiName, handler]) => {
20
+ const supported =
21
+ typeof handler.supported === 'function'
22
+ ? await handler.supported()
23
+ : (handler.supported ?? true);
24
+
25
+ return [
26
+ apiName,
27
+ {
28
+ version: handler.version,
29
+ supported,
30
+ },
31
+ ] as const;
32
+ }),
33
+ );
34
+
35
+ return entries.reduce<NebulaHostCapabilityMap>((capabilities, entry) => {
36
+ capabilities[entry[0]] = entry[1];
37
+ return capabilities;
38
+ }, {});
39
+ }
40
+
41
+ export function resolveHostApiDescriptions(): NebulaHostApiDescriptionMap {
42
+ return Array.from(
43
+ hostApiRegistry.entries(),
44
+ ).reduce<NebulaHostApiDescriptionMap>((descriptions, [apiName, handler]) => {
45
+ if (handler.description) {
46
+ descriptions[apiName] = handler.description;
47
+ }
48
+ return descriptions;
49
+ }, {});
50
+ }
51
+
52
+ export function startHostApiServer(
53
+ postMessageToMiniApp: (
54
+ appId: string,
55
+ message: NebulaProtocolResponse,
56
+ ) => Promise<{ errMsg: string }>,
57
+ ): void {
58
+ if (hostApiServerStarted) {
59
+ return;
60
+ }
61
+
62
+ const unsubscribe = addNebulaEventListener(
63
+ NEBULA_HOST_MESSAGE_EVENT,
64
+ event => {
65
+ const message = event.message;
66
+ if (!isProtocolRequest(message)) {
67
+ return;
68
+ }
69
+
70
+ const reply = async (response: NebulaProtocolResponse) => {
71
+ await postMessageToMiniApp(event.appId, response);
72
+ };
73
+
74
+ if (message.kind === 'getCapabilities') {
75
+ resolveHostCapabilities()
76
+ .then(capabilities =>
77
+ reply({
78
+ __nebulaProtocol: 'api.v1',
79
+ kind: 'capabilities',
80
+ requestId: message.requestId,
81
+ data: {
82
+ bridgeVersion: '1.0.0',
83
+ capabilities,
84
+ },
85
+ }),
86
+ )
87
+ .catch(error => {
88
+ console.error(
89
+ '[Nebula] Failed to resolve host capabilities',
90
+ error,
91
+ );
92
+ });
93
+ return;
94
+ }
95
+
96
+ if (message.kind === 'getApiDescriptions') {
97
+ reply({
98
+ __nebulaProtocol: 'api.v1',
99
+ kind: 'apiDescriptions',
100
+ requestId: message.requestId,
101
+ data: resolveHostApiDescriptions(),
102
+ }).catch(error => {
103
+ console.error(
104
+ '[Nebula] Failed to resolve host API descriptions',
105
+ error,
106
+ );
107
+ });
108
+ return;
109
+ }
110
+
111
+ if (message.kind !== 'invoke') {
112
+ return;
113
+ }
114
+
115
+ const handler = hostApiRegistry.get(message.api);
116
+ if (!handler) {
117
+ reply({
118
+ __nebulaProtocol: 'api.v1',
119
+ kind: 'invokeResult',
120
+ requestId: message.requestId,
121
+ result: {
122
+ ok: false,
123
+ error: {
124
+ code: 'UNSUPPORTED_API',
125
+ message: `${message.api} is not supported by the current host`,
126
+ },
127
+ },
128
+ }).catch(error => {
129
+ console.error('[Nebula] Failed to send unsupported API reply', error);
130
+ });
131
+ return;
132
+ }
133
+
134
+ const resolveSupported = async () =>
135
+ typeof handler.supported === 'function'
136
+ ? handler.supported()
137
+ : (handler.supported ?? true);
138
+
139
+ Promise.resolve(resolveSupported())
140
+ .then(async supported => {
141
+ if (!supported) {
142
+ await reply({
143
+ __nebulaProtocol: 'api.v1',
144
+ kind: 'invokeResult',
145
+ requestId: message.requestId,
146
+ result: {
147
+ ok: false,
148
+ error: {
149
+ code: 'UNSUPPORTED_API',
150
+ message: `${message.api} is not supported by the current host`,
151
+ },
152
+ },
153
+ });
154
+ return;
155
+ }
156
+
157
+ const result = await handler.handle(message.payload ?? {}, {
158
+ appId: event.appId,
159
+ requestId: message.requestId,
160
+ version: message.version,
161
+ });
162
+
163
+ await reply({
164
+ __nebulaProtocol: 'api.v1',
165
+ kind: 'invokeResult',
166
+ requestId: message.requestId,
167
+ result,
168
+ });
169
+ })
170
+ .catch(async error => {
171
+ await reply({
172
+ __nebulaProtocol: 'api.v1',
173
+ kind: 'invokeResult',
174
+ requestId: message.requestId,
175
+ result: {
176
+ ok: false,
177
+ error: {
178
+ code: 'INTERNAL_ERROR',
179
+ message:
180
+ error instanceof Error
181
+ ? error.message
182
+ : 'Unexpected host error',
183
+ },
184
+ },
185
+ });
186
+ });
187
+ },
188
+ );
189
+
190
+ hostApiServerUnsubscribe = unsubscribe ?? null;
191
+ hostApiServerStarted = true;
192
+ }
193
+
194
+ export function stopHostApiServer(): void {
195
+ hostApiServerUnsubscribe?.();
196
+ hostApiServerUnsubscribe = null;
197
+ hostApiServerStarted = false;
198
+ }
199
+
200
+ export function registerHostApiHandler(
201
+ apiName: string,
202
+ handler: NebulaHostApiHandler,
203
+ ): void {
204
+ hostApiRegistry.set(apiName, handler);
205
+ }
206
+
207
+ export function unregisterHostApiHandler(apiName: string): void {
208
+ hostApiRegistry.delete(apiName);
209
+ }
package/src/index.ts ADDED
@@ -0,0 +1,49 @@
1
+ export {
2
+ NebulaAPI,
3
+ Miniapp,
4
+ createMiniAppPage,
5
+ usePageOnHide,
6
+ usePageOnLoad,
7
+ usePageOnReady,
8
+ usePageOnShow,
9
+ usePageOnUnload,
10
+ } from './NebulaAPI';
11
+ export {
12
+ createHostApiFeature,
13
+ createMockHostApiFeature,
14
+ createHostApiFailure,
15
+ createHostApiSuccess,
16
+ createHostModalApiFeature,
17
+ registerHostModalApi,
18
+ createHostModalApiBridge,
19
+ createHostModalChannel,
20
+ registerHostModalComponent,
21
+ } from './hostApi';
22
+ export type {
23
+ HostModalChannel,
24
+ CreateMockHostApiOptions,
25
+ NebulaHostFeature,
26
+ RegisterHostApiOptions,
27
+ RegisterHostModalApiOptions,
28
+ } from './hostApi';
29
+ export { definePageConfig } from './pageConfig';
30
+ export type {
31
+ NebulaApiExampleDescriptor,
32
+ NebulaApiFieldDescriptor,
33
+ MiniappLoadingProps,
34
+ MiniappLoadingStatus,
35
+ MiniAppUpdateInfo,
36
+ MiniAppUpdateStrategy,
37
+ MiniAppVersionType,
38
+ NebulaApiError,
39
+ NebulaApiInvokeResult,
40
+ NebulaHostApiDescription,
41
+ NebulaHostApiDescriptionMap,
42
+ NebulaHostApiHandler,
43
+ NebulaHostCapabilityDescriptor,
44
+ NebulaHostCapabilityMap,
45
+ NebulaNativeCapabilitiesResult,
46
+ NebulaNativeCapabilityMap,
47
+ NebulaPageStyle,
48
+ } from './NebulaAPI';
49
+ export type { MiniAppPageConfig } from './pageConfig';