@scalemule/nextjs 0.1.2 → 0.1.4
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/dist/index.d.mts +68 -2
- package/dist/index.d.ts +68 -2
- package/dist/index.js +204 -4
- package/dist/index.mjs +204 -5
- package/package.json +2 -2
package/dist/index.d.mts
CHANGED
|
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import { ReactNode, ReactElement } from 'react';
|
|
3
3
|
import { MoneyClient } from '@scalemule/money';
|
|
4
4
|
export { MoneyClient, MoneyClientConfig, createMoneyClient } from '@scalemule/money';
|
|
5
|
-
import { RealtimeService, StorageService, PhotoService, ApiError as ApiError$1 } from '@scalemule/sdk';
|
|
5
|
+
import { RealtimeService, StorageService, PhotoService, VideoService, ApiError as ApiError$1, FileStatus } from '@scalemule/sdk';
|
|
6
6
|
import { ScaleMuleClient } from './client.mjs';
|
|
7
7
|
export { ClientConfig, RequestOptions, createClient } from './client.mjs';
|
|
8
8
|
import { S as ScaleMuleConfig, U as User, L as LoginResponse, A as ApiError, a as UseAuthReturn, b as UseBillingReturn, c as ListFilesParams, d as UseContentReturn, e as UseUserReturn, f as UseAnalyticsOptions, g as UseAnalyticsReturn } from './index-BIIUrnPr.mjs';
|
|
@@ -19,6 +19,8 @@ interface ScaleMuleContextValue {
|
|
|
19
19
|
storage: StorageService;
|
|
20
20
|
/** Base SDK photo service — exposed for `useMedia()` and `photo.uploadViaStorage()` */
|
|
21
21
|
photo: PhotoService;
|
|
22
|
+
/** Base SDK video service — exposed for `useMedia()` and `video.uploadViaStorage()` */
|
|
23
|
+
video: VideoService;
|
|
22
24
|
/** Current authenticated user */
|
|
23
25
|
user: User | null;
|
|
24
26
|
/** Set the current user */
|
|
@@ -266,6 +268,70 @@ interface UseMediaReturn {
|
|
|
266
268
|
*/
|
|
267
269
|
declare function useMedia(): UseMediaReturn;
|
|
268
270
|
|
|
271
|
+
interface UseFileStatusOptions {
|
|
272
|
+
/** Storage file_id to read status for. */
|
|
273
|
+
fileId: string | null | undefined;
|
|
274
|
+
/**
|
|
275
|
+
* Optional poll interval in milliseconds. If set, the hook re-fetches
|
|
276
|
+
* status every `pollIntervalMs`. Useful while waiting for transcode /
|
|
277
|
+
* optimization to complete. Pass `null` (default) for a one-shot read.
|
|
278
|
+
*
|
|
279
|
+
* Polling stops automatically once `scan.status === 'clean'` AND the
|
|
280
|
+
* caller's expected pipeline is done (`urls.optimized` returns 200 for
|
|
281
|
+
* images, `urls.hls` returns 200 for videos). Today the hook can only
|
|
282
|
+
* detect scan clean — broader pipeline-done detection lands when Phase 3
|
|
283
|
+
* enriches the optimize/transcode response fields.
|
|
284
|
+
*/
|
|
285
|
+
pollIntervalMs?: number | null;
|
|
286
|
+
/**
|
|
287
|
+
* Disable the hook (don't fetch). Useful when `fileId` is conditional.
|
|
288
|
+
*/
|
|
289
|
+
disabled?: boolean;
|
|
290
|
+
}
|
|
291
|
+
interface UseFileStatusReturn {
|
|
292
|
+
/** The latest status response, or null on first render / disabled. */
|
|
293
|
+
status: FileStatus | null;
|
|
294
|
+
/** True while a fetch is in flight. */
|
|
295
|
+
loading: boolean;
|
|
296
|
+
/** Last error, or null. */
|
|
297
|
+
error: ApiError$1 | null;
|
|
298
|
+
/**
|
|
299
|
+
* Convenience: scan is clean. For images/videos, this means the file
|
|
300
|
+
* is *safe to render*; the optimized / HLS variants may still be
|
|
301
|
+
* processing. The caller should attempt the constructed URLs and
|
|
302
|
+
* fall back to `urls.original` if the pipeline-specific URL 404s.
|
|
303
|
+
*/
|
|
304
|
+
isReady: boolean;
|
|
305
|
+
/** Force-refresh the status. Promise resolves when the new state is committed. */
|
|
306
|
+
refresh: () => Promise<void>;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Subscribes to {@link FileStatus} for a single file. Today this is a
|
|
310
|
+
* pull-only hook — single fetch by default, optional polling.
|
|
311
|
+
*
|
|
312
|
+
* Phase 3 of the realtime-chat media pipeline ADR will add a push variant
|
|
313
|
+
* for chat surfaces — `useFileStatus({ messageId })` will subscribe to
|
|
314
|
+
* `file.status` events on the existing per-conversation realtime channel
|
|
315
|
+
* via the `scalemule-chat` translation bridge (P5'). Until that lands,
|
|
316
|
+
* customers using this hook from chat surfaces should pass `pollIntervalMs`
|
|
317
|
+
* in the 1–3 second range while a media pipeline is expected to be running,
|
|
318
|
+
* then drop the polling once `isReady` is true.
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```tsx
|
|
322
|
+
* function ChatImage({ fileId }: { fileId: string }) {
|
|
323
|
+
* const { status, isReady } = useFileStatus({
|
|
324
|
+
* fileId,
|
|
325
|
+
* pollIntervalMs: 2000,
|
|
326
|
+
* });
|
|
327
|
+
* if (!isReady) return <div>Scanning…</div>;
|
|
328
|
+
* const src = status?.urls.optimized ?? status?.urls.original;
|
|
329
|
+
* return <img src={src} />;
|
|
330
|
+
* }
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
declare function useFileStatus(options: UseFileStatusOptions): UseFileStatusReturn;
|
|
334
|
+
|
|
269
335
|
declare const useMoney: typeof useMoneyClient;
|
|
270
336
|
|
|
271
337
|
/**
|
|
@@ -749,4 +815,4 @@ declare function createSafeLogger(prefix: string): {
|
|
|
749
815
|
error: (message: string, data?: unknown) => void;
|
|
750
816
|
};
|
|
751
817
|
|
|
752
|
-
export { ApiError, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaUploadResult, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
|
|
818
|
+
export { ApiError, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaUploadResult, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFileStatusOptions, type UseFileStatusReturn, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
|
2
2
|
import { ReactNode, ReactElement } from 'react';
|
|
3
3
|
import { MoneyClient } from '@scalemule/money';
|
|
4
4
|
export { MoneyClient, MoneyClientConfig, createMoneyClient } from '@scalemule/money';
|
|
5
|
-
import { RealtimeService, StorageService, PhotoService, ApiError as ApiError$1 } from '@scalemule/sdk';
|
|
5
|
+
import { RealtimeService, StorageService, PhotoService, VideoService, ApiError as ApiError$1, FileStatus } from '@scalemule/sdk';
|
|
6
6
|
import { ScaleMuleClient } from './client.js';
|
|
7
7
|
export { ClientConfig, RequestOptions, createClient } from './client.js';
|
|
8
8
|
import { S as ScaleMuleConfig, U as User, L as LoginResponse, A as ApiError, a as UseAuthReturn, b as UseBillingReturn, c as ListFilesParams, d as UseContentReturn, e as UseUserReturn, f as UseAnalyticsOptions, g as UseAnalyticsReturn } from './index-BIIUrnPr.js';
|
|
@@ -19,6 +19,8 @@ interface ScaleMuleContextValue {
|
|
|
19
19
|
storage: StorageService;
|
|
20
20
|
/** Base SDK photo service — exposed for `useMedia()` and `photo.uploadViaStorage()` */
|
|
21
21
|
photo: PhotoService;
|
|
22
|
+
/** Base SDK video service — exposed for `useMedia()` and `video.uploadViaStorage()` */
|
|
23
|
+
video: VideoService;
|
|
22
24
|
/** Current authenticated user */
|
|
23
25
|
user: User | null;
|
|
24
26
|
/** Set the current user */
|
|
@@ -266,6 +268,70 @@ interface UseMediaReturn {
|
|
|
266
268
|
*/
|
|
267
269
|
declare function useMedia(): UseMediaReturn;
|
|
268
270
|
|
|
271
|
+
interface UseFileStatusOptions {
|
|
272
|
+
/** Storage file_id to read status for. */
|
|
273
|
+
fileId: string | null | undefined;
|
|
274
|
+
/**
|
|
275
|
+
* Optional poll interval in milliseconds. If set, the hook re-fetches
|
|
276
|
+
* status every `pollIntervalMs`. Useful while waiting for transcode /
|
|
277
|
+
* optimization to complete. Pass `null` (default) for a one-shot read.
|
|
278
|
+
*
|
|
279
|
+
* Polling stops automatically once `scan.status === 'clean'` AND the
|
|
280
|
+
* caller's expected pipeline is done (`urls.optimized` returns 200 for
|
|
281
|
+
* images, `urls.hls` returns 200 for videos). Today the hook can only
|
|
282
|
+
* detect scan clean — broader pipeline-done detection lands when Phase 3
|
|
283
|
+
* enriches the optimize/transcode response fields.
|
|
284
|
+
*/
|
|
285
|
+
pollIntervalMs?: number | null;
|
|
286
|
+
/**
|
|
287
|
+
* Disable the hook (don't fetch). Useful when `fileId` is conditional.
|
|
288
|
+
*/
|
|
289
|
+
disabled?: boolean;
|
|
290
|
+
}
|
|
291
|
+
interface UseFileStatusReturn {
|
|
292
|
+
/** The latest status response, or null on first render / disabled. */
|
|
293
|
+
status: FileStatus | null;
|
|
294
|
+
/** True while a fetch is in flight. */
|
|
295
|
+
loading: boolean;
|
|
296
|
+
/** Last error, or null. */
|
|
297
|
+
error: ApiError$1 | null;
|
|
298
|
+
/**
|
|
299
|
+
* Convenience: scan is clean. For images/videos, this means the file
|
|
300
|
+
* is *safe to render*; the optimized / HLS variants may still be
|
|
301
|
+
* processing. The caller should attempt the constructed URLs and
|
|
302
|
+
* fall back to `urls.original` if the pipeline-specific URL 404s.
|
|
303
|
+
*/
|
|
304
|
+
isReady: boolean;
|
|
305
|
+
/** Force-refresh the status. Promise resolves when the new state is committed. */
|
|
306
|
+
refresh: () => Promise<void>;
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Subscribes to {@link FileStatus} for a single file. Today this is a
|
|
310
|
+
* pull-only hook — single fetch by default, optional polling.
|
|
311
|
+
*
|
|
312
|
+
* Phase 3 of the realtime-chat media pipeline ADR will add a push variant
|
|
313
|
+
* for chat surfaces — `useFileStatus({ messageId })` will subscribe to
|
|
314
|
+
* `file.status` events on the existing per-conversation realtime channel
|
|
315
|
+
* via the `scalemule-chat` translation bridge (P5'). Until that lands,
|
|
316
|
+
* customers using this hook from chat surfaces should pass `pollIntervalMs`
|
|
317
|
+
* in the 1–3 second range while a media pipeline is expected to be running,
|
|
318
|
+
* then drop the polling once `isReady` is true.
|
|
319
|
+
*
|
|
320
|
+
* @example
|
|
321
|
+
* ```tsx
|
|
322
|
+
* function ChatImage({ fileId }: { fileId: string }) {
|
|
323
|
+
* const { status, isReady } = useFileStatus({
|
|
324
|
+
* fileId,
|
|
325
|
+
* pollIntervalMs: 2000,
|
|
326
|
+
* });
|
|
327
|
+
* if (!isReady) return <div>Scanning…</div>;
|
|
328
|
+
* const src = status?.urls.optimized ?? status?.urls.original;
|
|
329
|
+
* return <img src={src} />;
|
|
330
|
+
* }
|
|
331
|
+
* ```
|
|
332
|
+
*/
|
|
333
|
+
declare function useFileStatus(options: UseFileStatusOptions): UseFileStatusReturn;
|
|
334
|
+
|
|
269
335
|
declare const useMoney: typeof useMoneyClient;
|
|
270
336
|
|
|
271
337
|
/**
|
|
@@ -749,4 +815,4 @@ declare function createSafeLogger(prefix: string): {
|
|
|
749
815
|
error: (message: string, data?: unknown) => void;
|
|
750
816
|
};
|
|
751
817
|
|
|
752
|
-
export { ApiError, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaUploadResult, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
|
|
818
|
+
export { ApiError, type FeatureFlagEvaluation, type FeatureFlagEvaluation as FeatureFlagResult, type FeedbackItem, type FeedbackItemInput, type FeedbackPriority, type FeedbackStatus, type FeedbackType, FeedbackWidget, type FeedbackWidgetConfig, type FeedbackWidgetProps, ListFilesParams, LoginResponse, type MediaUploadResult, type PasswordValidationResult, type PhoneCountry, type PhoneValidationResult, type RealtimeEvent, type RealtimeMessage, type RealtimeStatus, ScaleMuleClient, ScaleMuleConfig, ScaleMuleProvider, type ScaleMuleProviderProps, UseAnalyticsOptions, UseAnalyticsReturn, UseAuthReturn, UseBillingReturn, UseContentReturn, type UseFeatureFlagsOptions, type UseFeatureFlagsReturn, type UseFeedbackOptions, type UseFeedbackResult, type UseFileStatusOptions, type UseFileStatusReturn, type UseFeatureFlagsOptions as UseFlagsOptions, type UseFeatureFlagsReturn as UseFlagsReturn, type UseMediaReturn, type UseMediaUploadOptions, type UsePushNotificationsOptions, type UsePushNotificationsReturn, type UseRealtimeOptions, type UseRealtimeReturn, type UseShareOptions, type UseShareReturn, UseUserReturn, User, type UsernameValidationResult, composePhone, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
|
package/dist/index.js
CHANGED
|
@@ -2707,6 +2707,31 @@ var StorageService = class extends ServiceModule {
|
|
|
2707
2707
|
async getInfo(fileId, options) {
|
|
2708
2708
|
return this._get(`/files/${fileId}/info`, options);
|
|
2709
2709
|
}
|
|
2710
|
+
/**
|
|
2711
|
+
* Aggregate status for a file — single call returns scan + reserved
|
|
2712
|
+
* optimize/transcode slots + the canonical view URL paths for image
|
|
2713
|
+
* (transform endpoint) and video (HLS playlist) MIME types.
|
|
2714
|
+
*
|
|
2715
|
+
* Foundational primitive for the chat / progressive media read side.
|
|
2716
|
+
* `useFileStatus()` (in `@scalemule/nextjs`) consumes this for both
|
|
2717
|
+
* first-paint and refresh-on-demand scenarios.
|
|
2718
|
+
*
|
|
2719
|
+
* `optimize` and `transcode` are reserved for Phase 3 enrichment. Until
|
|
2720
|
+
* the photo/video services expose internal status endpoints, callers
|
|
2721
|
+
* should attempt the constructed URLs directly to discover readiness
|
|
2722
|
+
* (404 means the pipeline is still running).
|
|
2723
|
+
*
|
|
2724
|
+
* @example
|
|
2725
|
+
* ```ts
|
|
2726
|
+
* const r = await client.storage.getFileStatus(fileId);
|
|
2727
|
+
* if (r.data?.scan.status === 'clean' && r.data.urls.optimized) {
|
|
2728
|
+
* // image: try transform URL for an optimized variant
|
|
2729
|
+
* }
|
|
2730
|
+
* ```
|
|
2731
|
+
*/
|
|
2732
|
+
async getFileStatus(fileId, options) {
|
|
2733
|
+
return this._get(`/files/${fileId}/status`, options);
|
|
2734
|
+
}
|
|
2710
2735
|
/**
|
|
2711
2736
|
* Get a signed view URL for inline display (img src, thumbnails).
|
|
2712
2737
|
* Returns CloudFront signed URL (fast, ~1us) or S3 presigned fallback.
|
|
@@ -3505,8 +3530,13 @@ var RealtimeService = class extends ServiceModule {
|
|
|
3505
3530
|
var DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;
|
|
3506
3531
|
var MIN_CHUNK_SIZE = 5 * 1024 * 1024;
|
|
3507
3532
|
var VideoService = class extends ServiceModule {
|
|
3508
|
-
|
|
3509
|
-
|
|
3533
|
+
/**
|
|
3534
|
+
* @param storage Required for {@link uploadViaStorage}. Wired up by the
|
|
3535
|
+
* top-level {@link ScaleMule} constructor.
|
|
3536
|
+
*/
|
|
3537
|
+
constructor(client, storage) {
|
|
3538
|
+
super(client);
|
|
3539
|
+
this.storage = storage;
|
|
3510
3540
|
this.basePath = "/v1/videos";
|
|
3511
3541
|
}
|
|
3512
3542
|
// --------------------------------------------------------------------------
|
|
@@ -3580,6 +3610,115 @@ var VideoService = class extends ServiceModule {
|
|
|
3580
3610
|
async get(videoId, options) {
|
|
3581
3611
|
return super._get(`/${videoId}`, options);
|
|
3582
3612
|
}
|
|
3613
|
+
/**
|
|
3614
|
+
* Register a video from a file already uploaded to scalemule-storage.
|
|
3615
|
+
*
|
|
3616
|
+
* The synchronous handshake for storage-uploaded videos. Verifies the
|
|
3617
|
+
* caller's app owns the storage `file_id` and reads scan status:
|
|
3618
|
+
* - clean → 201, video record created with status='uploading',
|
|
3619
|
+
* transcode worker advances on `scan.file.completed`
|
|
3620
|
+
* - pending → 202, video record created with status='pending_scan',
|
|
3621
|
+
* worker advances when scan completes
|
|
3622
|
+
* - threat → 409 (rejected; row not created)
|
|
3623
|
+
*
|
|
3624
|
+
* Idempotent: calling twice with the same file_id returns the existing
|
|
3625
|
+
* row's data (the underlying INSERT uses ON DUPLICATE KEY UPDATE).
|
|
3626
|
+
*
|
|
3627
|
+
* For storage-backed videos `video_id == file_id` — the response's
|
|
3628
|
+
* `video_id` and `file_id` are identical, and either can be used with
|
|
3629
|
+
* `getStreamUrl()` / `get()`.
|
|
3630
|
+
*
|
|
3631
|
+
* Most callers should use {@link uploadViaStorage} which composes
|
|
3632
|
+
* `storage.uploadPrivate()` + `register()` in a single call.
|
|
3633
|
+
*/
|
|
3634
|
+
async register(request, options) {
|
|
3635
|
+
return this.post(
|
|
3636
|
+
"/register",
|
|
3637
|
+
{
|
|
3638
|
+
file_id: request.fileId,
|
|
3639
|
+
sm_user_id: request.userId
|
|
3640
|
+
},
|
|
3641
|
+
options
|
|
3642
|
+
);
|
|
3643
|
+
}
|
|
3644
|
+
/**
|
|
3645
|
+
* Upload a video to storage (browser → S3 direct, private, no
|
|
3646
|
+
* compression) and register it with the video service so transcoding
|
|
3647
|
+
* + HLS streaming work.
|
|
3648
|
+
*
|
|
3649
|
+
* The canonical chat-attachment / progressive-video upload primitive.
|
|
3650
|
+
* Same private-direct-to-S3 path as `storage.uploadPrivate()`, plus a
|
|
3651
|
+
* follow-up `video.register()` call so HLS playback URLs become live
|
|
3652
|
+
* once the transcoder finishes.
|
|
3653
|
+
*
|
|
3654
|
+
* The returned `hls_url_promise` resolves once the video's status
|
|
3655
|
+
* flips to `ready` (or after a 30s timeout — caller can still serve
|
|
3656
|
+
* `original_view_url` as the fallback). Phase 3 of the realtime-chat
|
|
3657
|
+
* media pipeline ADR replaces the poll with a realtime subscription.
|
|
3658
|
+
*
|
|
3659
|
+
* If `register()` fails after a successful storage upload, the file
|
|
3660
|
+
* is *not* lost: the returned `file_id` is still valid as a generic
|
|
3661
|
+
* storage file. SDK logs a warning and resolves `hls_url_promise`
|
|
3662
|
+
* to `null`.
|
|
3663
|
+
*/
|
|
3664
|
+
async uploadViaStorage(file, uploadOptions, requestOptions) {
|
|
3665
|
+
const uploadResult = await this.storage.uploadPrivate(file, {
|
|
3666
|
+
filename: uploadOptions?.filename,
|
|
3667
|
+
metadata: uploadOptions?.metadata,
|
|
3668
|
+
onProgress: uploadOptions?.onProgress,
|
|
3669
|
+
signal: uploadOptions?.signal
|
|
3670
|
+
});
|
|
3671
|
+
if (uploadResult.error || !uploadResult.data) {
|
|
3672
|
+
return { data: null, error: uploadResult.error };
|
|
3673
|
+
}
|
|
3674
|
+
const fileInfo = uploadResult.data;
|
|
3675
|
+
const fileId = fileInfo.id;
|
|
3676
|
+
const originalViewUrl = fileInfo.url ?? null;
|
|
3677
|
+
const registerResult = await this.register({ fileId, userId: uploadOptions?.userId }, requestOptions);
|
|
3678
|
+
if (registerResult.error || !registerResult.data) {
|
|
3679
|
+
console.warn(
|
|
3680
|
+
"[scalemule-sdk] video.register() failed after storage upload; HLS variants unavailable.",
|
|
3681
|
+
registerResult.error
|
|
3682
|
+
);
|
|
3683
|
+
return {
|
|
3684
|
+
data: {
|
|
3685
|
+
file_id: fileId,
|
|
3686
|
+
video_id: fileId,
|
|
3687
|
+
original_view_url: originalViewUrl,
|
|
3688
|
+
hls_url_promise: Promise.resolve(null)
|
|
3689
|
+
},
|
|
3690
|
+
error: null
|
|
3691
|
+
};
|
|
3692
|
+
}
|
|
3693
|
+
const videoId = registerResult.data.video_id;
|
|
3694
|
+
const hlsUrlPromise = this.pollTranscodeComplete(videoId, requestOptions);
|
|
3695
|
+
return {
|
|
3696
|
+
data: {
|
|
3697
|
+
file_id: fileId,
|
|
3698
|
+
video_id: videoId,
|
|
3699
|
+
original_view_url: originalViewUrl,
|
|
3700
|
+
hls_url_promise: hlsUrlPromise
|
|
3701
|
+
},
|
|
3702
|
+
error: null
|
|
3703
|
+
};
|
|
3704
|
+
}
|
|
3705
|
+
/**
|
|
3706
|
+
* Poll {@link get} until the video's status is `ready` or the 30s
|
|
3707
|
+
* timeout fires. Resolves to the HLS master playlist URL on success;
|
|
3708
|
+
* `null` on timeout (caller should fall back to `original_view_url`).
|
|
3709
|
+
*/
|
|
3710
|
+
async pollTranscodeComplete(videoId, requestOptions) {
|
|
3711
|
+
const intervalMs = 1e3;
|
|
3712
|
+
const maxAttempts = 30;
|
|
3713
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
3714
|
+
const result = await this.get(videoId, requestOptions);
|
|
3715
|
+
if (result.data?.status === "ready") {
|
|
3716
|
+
return `${this.client.getBaseUrl()}${this.basePath}/${videoId}/playlist.m3u8`;
|
|
3717
|
+
}
|
|
3718
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
3719
|
+
}
|
|
3720
|
+
return null;
|
|
3721
|
+
}
|
|
3583
3722
|
/**
|
|
3584
3723
|
* Get the HLS master playlist URL for streaming.
|
|
3585
3724
|
* Returns the playlist URL that can be passed to a video player.
|
|
@@ -6310,7 +6449,7 @@ var ScaleMule = class {
|
|
|
6310
6449
|
this.auth = new AuthService(this._client);
|
|
6311
6450
|
this.storage = new StorageService(this._client);
|
|
6312
6451
|
this.realtime = new RealtimeService(this._client);
|
|
6313
|
-
this.video = new VideoService(this._client);
|
|
6452
|
+
this.video = new VideoService(this._client, this.storage);
|
|
6314
6453
|
this.data = new DataService(this._client);
|
|
6315
6454
|
this.chat = new ChatService(this._client);
|
|
6316
6455
|
this.conference = new ConferenceService(this._client);
|
|
@@ -7424,6 +7563,7 @@ function ScaleMuleProvider({
|
|
|
7424
7563
|
realtime: baseClient.realtime,
|
|
7425
7564
|
storage: baseClient.storage,
|
|
7426
7565
|
photo: baseClient.photo,
|
|
7566
|
+
video: baseClient.video,
|
|
7427
7567
|
user,
|
|
7428
7568
|
setUser: handleSetUser,
|
|
7429
7569
|
initializing,
|
|
@@ -8737,7 +8877,7 @@ function useContent(options = {}) {
|
|
|
8737
8877
|
);
|
|
8738
8878
|
}
|
|
8739
8879
|
function useMedia() {
|
|
8740
|
-
const { storage, photo } = useScaleMule();
|
|
8880
|
+
const { storage, photo, video } = useScaleMule();
|
|
8741
8881
|
const [uploading, setUploading] = react.useState(false);
|
|
8742
8882
|
const [error, setError] = react.useState(null);
|
|
8743
8883
|
const upload = react.useCallback(
|
|
@@ -8768,6 +8908,21 @@ function useMedia() {
|
|
|
8768
8908
|
is_public: false
|
|
8769
8909
|
};
|
|
8770
8910
|
}
|
|
8911
|
+
if (mimeType.startsWith("video/") && !isPublic) {
|
|
8912
|
+
const r2 = await video.uploadViaStorage(file, sharedOpts);
|
|
8913
|
+
if (r2.error || !r2.data) {
|
|
8914
|
+
throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
|
|
8915
|
+
}
|
|
8916
|
+
return {
|
|
8917
|
+
file_id: r2.data.file_id,
|
|
8918
|
+
photo_id: null,
|
|
8919
|
+
original_view_url: r2.data.original_view_url,
|
|
8920
|
+
optimized_url_promise: Promise.resolve(null),
|
|
8921
|
+
hls_url_promise: r2.data.hls_url_promise,
|
|
8922
|
+
mime_type: mimeType,
|
|
8923
|
+
is_public: false
|
|
8924
|
+
};
|
|
8925
|
+
}
|
|
8771
8926
|
if (mimeType.startsWith("image/") && isPublic) {
|
|
8772
8927
|
const r2 = await storage.upload(file, {
|
|
8773
8928
|
...sharedOpts,
|
|
@@ -8831,6 +8986,50 @@ function useMedia() {
|
|
|
8831
8986
|
);
|
|
8832
8987
|
return { upload, cancelUpload, error, uploading };
|
|
8833
8988
|
}
|
|
8989
|
+
function useFileStatus(options) {
|
|
8990
|
+
const { storage } = useScaleMule();
|
|
8991
|
+
const { fileId, pollIntervalMs = null, disabled = false } = options;
|
|
8992
|
+
const [status, setStatus] = react.useState(null);
|
|
8993
|
+
const [loading, setLoading] = react.useState(false);
|
|
8994
|
+
const [error, setError] = react.useState(null);
|
|
8995
|
+
const requestSeqRef = react.useRef(0);
|
|
8996
|
+
const fetchStatus = react.useCallback(async () => {
|
|
8997
|
+
if (!fileId || disabled) return;
|
|
8998
|
+
const seq = ++requestSeqRef.current;
|
|
8999
|
+
setLoading(true);
|
|
9000
|
+
setError(null);
|
|
9001
|
+
try {
|
|
9002
|
+
const r = await storage.getFileStatus(fileId);
|
|
9003
|
+
if (seq !== requestSeqRef.current) return;
|
|
9004
|
+
if (r.error || !r.data) {
|
|
9005
|
+
setError(r.error);
|
|
9006
|
+
return;
|
|
9007
|
+
}
|
|
9008
|
+
setStatus(r.data);
|
|
9009
|
+
} catch (err) {
|
|
9010
|
+
if (seq !== requestSeqRef.current) return;
|
|
9011
|
+
setError(err);
|
|
9012
|
+
} finally {
|
|
9013
|
+
if (seq === requestSeqRef.current) {
|
|
9014
|
+
setLoading(false);
|
|
9015
|
+
}
|
|
9016
|
+
}
|
|
9017
|
+
}, [storage, fileId, disabled]);
|
|
9018
|
+
react.useEffect(() => {
|
|
9019
|
+
void fetchStatus();
|
|
9020
|
+
}, [fetchStatus]);
|
|
9021
|
+
react.useEffect(() => {
|
|
9022
|
+
if (!pollIntervalMs || disabled || !fileId) return;
|
|
9023
|
+
const isClean = status?.scan.status === "clean";
|
|
9024
|
+
if (isClean) return;
|
|
9025
|
+
const id = setInterval(() => {
|
|
9026
|
+
void fetchStatus();
|
|
9027
|
+
}, pollIntervalMs);
|
|
9028
|
+
return () => clearInterval(id);
|
|
9029
|
+
}, [pollIntervalMs, disabled, fileId, status?.scan.status, fetchStatus]);
|
|
9030
|
+
const isReady = status?.scan.status === "clean";
|
|
9031
|
+
return { status, loading, error, isReady, refresh: fetchStatus };
|
|
9032
|
+
}
|
|
8834
9033
|
|
|
8835
9034
|
// src/hooks/useMoney.ts
|
|
8836
9035
|
var useMoney = useMoneyClient;
|
|
@@ -10641,6 +10840,7 @@ exports.useBilling = useBilling;
|
|
|
10641
10840
|
exports.useContent = useContent;
|
|
10642
10841
|
exports.useFeatureFlags = useFeatureFlags;
|
|
10643
10842
|
exports.useFeedback = useFeedback;
|
|
10843
|
+
exports.useFileStatus = useFileStatus;
|
|
10644
10844
|
exports.useMedia = useMedia;
|
|
10645
10845
|
exports.useMoney = useMoney;
|
|
10646
10846
|
exports.useMoneyClient = useMoneyClient;
|
package/dist/index.mjs
CHANGED
|
@@ -2706,6 +2706,31 @@ var StorageService = class extends ServiceModule {
|
|
|
2706
2706
|
async getInfo(fileId, options) {
|
|
2707
2707
|
return this._get(`/files/${fileId}/info`, options);
|
|
2708
2708
|
}
|
|
2709
|
+
/**
|
|
2710
|
+
* Aggregate status for a file — single call returns scan + reserved
|
|
2711
|
+
* optimize/transcode slots + the canonical view URL paths for image
|
|
2712
|
+
* (transform endpoint) and video (HLS playlist) MIME types.
|
|
2713
|
+
*
|
|
2714
|
+
* Foundational primitive for the chat / progressive media read side.
|
|
2715
|
+
* `useFileStatus()` (in `@scalemule/nextjs`) consumes this for both
|
|
2716
|
+
* first-paint and refresh-on-demand scenarios.
|
|
2717
|
+
*
|
|
2718
|
+
* `optimize` and `transcode` are reserved for Phase 3 enrichment. Until
|
|
2719
|
+
* the photo/video services expose internal status endpoints, callers
|
|
2720
|
+
* should attempt the constructed URLs directly to discover readiness
|
|
2721
|
+
* (404 means the pipeline is still running).
|
|
2722
|
+
*
|
|
2723
|
+
* @example
|
|
2724
|
+
* ```ts
|
|
2725
|
+
* const r = await client.storage.getFileStatus(fileId);
|
|
2726
|
+
* if (r.data?.scan.status === 'clean' && r.data.urls.optimized) {
|
|
2727
|
+
* // image: try transform URL for an optimized variant
|
|
2728
|
+
* }
|
|
2729
|
+
* ```
|
|
2730
|
+
*/
|
|
2731
|
+
async getFileStatus(fileId, options) {
|
|
2732
|
+
return this._get(`/files/${fileId}/status`, options);
|
|
2733
|
+
}
|
|
2709
2734
|
/**
|
|
2710
2735
|
* Get a signed view URL for inline display (img src, thumbnails).
|
|
2711
2736
|
* Returns CloudFront signed URL (fast, ~1us) or S3 presigned fallback.
|
|
@@ -3504,8 +3529,13 @@ var RealtimeService = class extends ServiceModule {
|
|
|
3504
3529
|
var DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;
|
|
3505
3530
|
var MIN_CHUNK_SIZE = 5 * 1024 * 1024;
|
|
3506
3531
|
var VideoService = class extends ServiceModule {
|
|
3507
|
-
|
|
3508
|
-
|
|
3532
|
+
/**
|
|
3533
|
+
* @param storage Required for {@link uploadViaStorage}. Wired up by the
|
|
3534
|
+
* top-level {@link ScaleMule} constructor.
|
|
3535
|
+
*/
|
|
3536
|
+
constructor(client, storage) {
|
|
3537
|
+
super(client);
|
|
3538
|
+
this.storage = storage;
|
|
3509
3539
|
this.basePath = "/v1/videos";
|
|
3510
3540
|
}
|
|
3511
3541
|
// --------------------------------------------------------------------------
|
|
@@ -3579,6 +3609,115 @@ var VideoService = class extends ServiceModule {
|
|
|
3579
3609
|
async get(videoId, options) {
|
|
3580
3610
|
return super._get(`/${videoId}`, options);
|
|
3581
3611
|
}
|
|
3612
|
+
/**
|
|
3613
|
+
* Register a video from a file already uploaded to scalemule-storage.
|
|
3614
|
+
*
|
|
3615
|
+
* The synchronous handshake for storage-uploaded videos. Verifies the
|
|
3616
|
+
* caller's app owns the storage `file_id` and reads scan status:
|
|
3617
|
+
* - clean → 201, video record created with status='uploading',
|
|
3618
|
+
* transcode worker advances on `scan.file.completed`
|
|
3619
|
+
* - pending → 202, video record created with status='pending_scan',
|
|
3620
|
+
* worker advances when scan completes
|
|
3621
|
+
* - threat → 409 (rejected; row not created)
|
|
3622
|
+
*
|
|
3623
|
+
* Idempotent: calling twice with the same file_id returns the existing
|
|
3624
|
+
* row's data (the underlying INSERT uses ON DUPLICATE KEY UPDATE).
|
|
3625
|
+
*
|
|
3626
|
+
* For storage-backed videos `video_id == file_id` — the response's
|
|
3627
|
+
* `video_id` and `file_id` are identical, and either can be used with
|
|
3628
|
+
* `getStreamUrl()` / `get()`.
|
|
3629
|
+
*
|
|
3630
|
+
* Most callers should use {@link uploadViaStorage} which composes
|
|
3631
|
+
* `storage.uploadPrivate()` + `register()` in a single call.
|
|
3632
|
+
*/
|
|
3633
|
+
async register(request, options) {
|
|
3634
|
+
return this.post(
|
|
3635
|
+
"/register",
|
|
3636
|
+
{
|
|
3637
|
+
file_id: request.fileId,
|
|
3638
|
+
sm_user_id: request.userId
|
|
3639
|
+
},
|
|
3640
|
+
options
|
|
3641
|
+
);
|
|
3642
|
+
}
|
|
3643
|
+
/**
|
|
3644
|
+
* Upload a video to storage (browser → S3 direct, private, no
|
|
3645
|
+
* compression) and register it with the video service so transcoding
|
|
3646
|
+
* + HLS streaming work.
|
|
3647
|
+
*
|
|
3648
|
+
* The canonical chat-attachment / progressive-video upload primitive.
|
|
3649
|
+
* Same private-direct-to-S3 path as `storage.uploadPrivate()`, plus a
|
|
3650
|
+
* follow-up `video.register()` call so HLS playback URLs become live
|
|
3651
|
+
* once the transcoder finishes.
|
|
3652
|
+
*
|
|
3653
|
+
* The returned `hls_url_promise` resolves once the video's status
|
|
3654
|
+
* flips to `ready` (or after a 30s timeout — caller can still serve
|
|
3655
|
+
* `original_view_url` as the fallback). Phase 3 of the realtime-chat
|
|
3656
|
+
* media pipeline ADR replaces the poll with a realtime subscription.
|
|
3657
|
+
*
|
|
3658
|
+
* If `register()` fails after a successful storage upload, the file
|
|
3659
|
+
* is *not* lost: the returned `file_id` is still valid as a generic
|
|
3660
|
+
* storage file. SDK logs a warning and resolves `hls_url_promise`
|
|
3661
|
+
* to `null`.
|
|
3662
|
+
*/
|
|
3663
|
+
async uploadViaStorage(file, uploadOptions, requestOptions) {
|
|
3664
|
+
const uploadResult = await this.storage.uploadPrivate(file, {
|
|
3665
|
+
filename: uploadOptions?.filename,
|
|
3666
|
+
metadata: uploadOptions?.metadata,
|
|
3667
|
+
onProgress: uploadOptions?.onProgress,
|
|
3668
|
+
signal: uploadOptions?.signal
|
|
3669
|
+
});
|
|
3670
|
+
if (uploadResult.error || !uploadResult.data) {
|
|
3671
|
+
return { data: null, error: uploadResult.error };
|
|
3672
|
+
}
|
|
3673
|
+
const fileInfo = uploadResult.data;
|
|
3674
|
+
const fileId = fileInfo.id;
|
|
3675
|
+
const originalViewUrl = fileInfo.url ?? null;
|
|
3676
|
+
const registerResult = await this.register({ fileId, userId: uploadOptions?.userId }, requestOptions);
|
|
3677
|
+
if (registerResult.error || !registerResult.data) {
|
|
3678
|
+
console.warn(
|
|
3679
|
+
"[scalemule-sdk] video.register() failed after storage upload; HLS variants unavailable.",
|
|
3680
|
+
registerResult.error
|
|
3681
|
+
);
|
|
3682
|
+
return {
|
|
3683
|
+
data: {
|
|
3684
|
+
file_id: fileId,
|
|
3685
|
+
video_id: fileId,
|
|
3686
|
+
original_view_url: originalViewUrl,
|
|
3687
|
+
hls_url_promise: Promise.resolve(null)
|
|
3688
|
+
},
|
|
3689
|
+
error: null
|
|
3690
|
+
};
|
|
3691
|
+
}
|
|
3692
|
+
const videoId = registerResult.data.video_id;
|
|
3693
|
+
const hlsUrlPromise = this.pollTranscodeComplete(videoId, requestOptions);
|
|
3694
|
+
return {
|
|
3695
|
+
data: {
|
|
3696
|
+
file_id: fileId,
|
|
3697
|
+
video_id: videoId,
|
|
3698
|
+
original_view_url: originalViewUrl,
|
|
3699
|
+
hls_url_promise: hlsUrlPromise
|
|
3700
|
+
},
|
|
3701
|
+
error: null
|
|
3702
|
+
};
|
|
3703
|
+
}
|
|
3704
|
+
/**
|
|
3705
|
+
* Poll {@link get} until the video's status is `ready` or the 30s
|
|
3706
|
+
* timeout fires. Resolves to the HLS master playlist URL on success;
|
|
3707
|
+
* `null` on timeout (caller should fall back to `original_view_url`).
|
|
3708
|
+
*/
|
|
3709
|
+
async pollTranscodeComplete(videoId, requestOptions) {
|
|
3710
|
+
const intervalMs = 1e3;
|
|
3711
|
+
const maxAttempts = 30;
|
|
3712
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
3713
|
+
const result = await this.get(videoId, requestOptions);
|
|
3714
|
+
if (result.data?.status === "ready") {
|
|
3715
|
+
return `${this.client.getBaseUrl()}${this.basePath}/${videoId}/playlist.m3u8`;
|
|
3716
|
+
}
|
|
3717
|
+
await new Promise((r) => setTimeout(r, intervalMs));
|
|
3718
|
+
}
|
|
3719
|
+
return null;
|
|
3720
|
+
}
|
|
3582
3721
|
/**
|
|
3583
3722
|
* Get the HLS master playlist URL for streaming.
|
|
3584
3723
|
* Returns the playlist URL that can be passed to a video player.
|
|
@@ -6309,7 +6448,7 @@ var ScaleMule = class {
|
|
|
6309
6448
|
this.auth = new AuthService(this._client);
|
|
6310
6449
|
this.storage = new StorageService(this._client);
|
|
6311
6450
|
this.realtime = new RealtimeService(this._client);
|
|
6312
|
-
this.video = new VideoService(this._client);
|
|
6451
|
+
this.video = new VideoService(this._client, this.storage);
|
|
6313
6452
|
this.data = new DataService(this._client);
|
|
6314
6453
|
this.chat = new ChatService(this._client);
|
|
6315
6454
|
this.conference = new ConferenceService(this._client);
|
|
@@ -7423,6 +7562,7 @@ function ScaleMuleProvider({
|
|
|
7423
7562
|
realtime: baseClient.realtime,
|
|
7424
7563
|
storage: baseClient.storage,
|
|
7425
7564
|
photo: baseClient.photo,
|
|
7565
|
+
video: baseClient.video,
|
|
7426
7566
|
user,
|
|
7427
7567
|
setUser: handleSetUser,
|
|
7428
7568
|
initializing,
|
|
@@ -8736,7 +8876,7 @@ function useContent(options = {}) {
|
|
|
8736
8876
|
);
|
|
8737
8877
|
}
|
|
8738
8878
|
function useMedia() {
|
|
8739
|
-
const { storage, photo } = useScaleMule();
|
|
8879
|
+
const { storage, photo, video } = useScaleMule();
|
|
8740
8880
|
const [uploading, setUploading] = useState(false);
|
|
8741
8881
|
const [error, setError] = useState(null);
|
|
8742
8882
|
const upload = useCallback(
|
|
@@ -8767,6 +8907,21 @@ function useMedia() {
|
|
|
8767
8907
|
is_public: false
|
|
8768
8908
|
};
|
|
8769
8909
|
}
|
|
8910
|
+
if (mimeType.startsWith("video/") && !isPublic) {
|
|
8911
|
+
const r2 = await video.uploadViaStorage(file, sharedOpts);
|
|
8912
|
+
if (r2.error || !r2.data) {
|
|
8913
|
+
throw r2.error ?? { code: "upload_error", message: "Upload failed", status: 0 };
|
|
8914
|
+
}
|
|
8915
|
+
return {
|
|
8916
|
+
file_id: r2.data.file_id,
|
|
8917
|
+
photo_id: null,
|
|
8918
|
+
original_view_url: r2.data.original_view_url,
|
|
8919
|
+
optimized_url_promise: Promise.resolve(null),
|
|
8920
|
+
hls_url_promise: r2.data.hls_url_promise,
|
|
8921
|
+
mime_type: mimeType,
|
|
8922
|
+
is_public: false
|
|
8923
|
+
};
|
|
8924
|
+
}
|
|
8770
8925
|
if (mimeType.startsWith("image/") && isPublic) {
|
|
8771
8926
|
const r2 = await storage.upload(file, {
|
|
8772
8927
|
...sharedOpts,
|
|
@@ -8830,6 +8985,50 @@ function useMedia() {
|
|
|
8830
8985
|
);
|
|
8831
8986
|
return { upload, cancelUpload, error, uploading };
|
|
8832
8987
|
}
|
|
8988
|
+
function useFileStatus(options) {
|
|
8989
|
+
const { storage } = useScaleMule();
|
|
8990
|
+
const { fileId, pollIntervalMs = null, disabled = false } = options;
|
|
8991
|
+
const [status, setStatus] = useState(null);
|
|
8992
|
+
const [loading, setLoading] = useState(false);
|
|
8993
|
+
const [error, setError] = useState(null);
|
|
8994
|
+
const requestSeqRef = useRef(0);
|
|
8995
|
+
const fetchStatus = useCallback(async () => {
|
|
8996
|
+
if (!fileId || disabled) return;
|
|
8997
|
+
const seq = ++requestSeqRef.current;
|
|
8998
|
+
setLoading(true);
|
|
8999
|
+
setError(null);
|
|
9000
|
+
try {
|
|
9001
|
+
const r = await storage.getFileStatus(fileId);
|
|
9002
|
+
if (seq !== requestSeqRef.current) return;
|
|
9003
|
+
if (r.error || !r.data) {
|
|
9004
|
+
setError(r.error);
|
|
9005
|
+
return;
|
|
9006
|
+
}
|
|
9007
|
+
setStatus(r.data);
|
|
9008
|
+
} catch (err) {
|
|
9009
|
+
if (seq !== requestSeqRef.current) return;
|
|
9010
|
+
setError(err);
|
|
9011
|
+
} finally {
|
|
9012
|
+
if (seq === requestSeqRef.current) {
|
|
9013
|
+
setLoading(false);
|
|
9014
|
+
}
|
|
9015
|
+
}
|
|
9016
|
+
}, [storage, fileId, disabled]);
|
|
9017
|
+
useEffect(() => {
|
|
9018
|
+
void fetchStatus();
|
|
9019
|
+
}, [fetchStatus]);
|
|
9020
|
+
useEffect(() => {
|
|
9021
|
+
if (!pollIntervalMs || disabled || !fileId) return;
|
|
9022
|
+
const isClean = status?.scan.status === "clean";
|
|
9023
|
+
if (isClean) return;
|
|
9024
|
+
const id = setInterval(() => {
|
|
9025
|
+
void fetchStatus();
|
|
9026
|
+
}, pollIntervalMs);
|
|
9027
|
+
return () => clearInterval(id);
|
|
9028
|
+
}, [pollIntervalMs, disabled, fileId, status?.scan.status, fetchStatus]);
|
|
9029
|
+
const isReady = status?.scan.status === "clean";
|
|
9030
|
+
return { status, loading, error, isReady, refresh: fetchStatus };
|
|
9031
|
+
}
|
|
8833
9032
|
|
|
8834
9033
|
// src/hooks/useMoney.ts
|
|
8835
9034
|
var useMoney = useMoneyClient;
|
|
@@ -10616,4 +10815,4 @@ function createSafeLogger(prefix) {
|
|
|
10616
10815
|
};
|
|
10617
10816
|
}
|
|
10618
10817
|
|
|
10619
|
-
export { FeedbackWidget, ScaleMuleApiError, ScaleMuleClient2 as ScaleMuleClient, ScaleMuleProvider, composePhone, createClient, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
|
|
10818
|
+
export { FeedbackWidget, ScaleMuleApiError, ScaleMuleClient2 as ScaleMuleClient, ScaleMuleProvider, composePhone, createClient, createSafeLogger, normalizePhone, phoneCountries, sanitizeForLog, useAnalytics, useAuth, useBilling, useContent, useFeatureFlags, useFeedback, useFileStatus, useMedia, useMoney, useMoneyClient, usePushNotifications, useRealtime, useScaleMule, useScaleMuleClient, useShare, useUser, validateForm, validators };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@scalemule/nextjs",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"description": "ScaleMule SDK for Next.js applications - authentication, storage, and user management",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@scalemule/money": "^0.0.1",
|
|
56
|
-
"@scalemule/sdk": "^0.0.
|
|
56
|
+
"@scalemule/sdk": "^0.0.41"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
59
|
"next": ">=14.0.0",
|