@scalemule/nextjs 0.1.3 → 0.1.5

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.mjs CHANGED
@@ -1,7 +1,8 @@
1
+ import * as React from 'react';
1
2
  import { createContext, useState, useMemo, useEffect, useCallback, useContext, useRef } from 'react';
2
3
  import { createMoneyClient } from '@scalemule/money';
3
4
  export { MoneyClient, createMoneyClient } from '@scalemule/money';
4
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
5
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
5
6
 
6
7
  var __defProp = Object.defineProperty;
7
8
  var __getOwnPropNames = Object.getOwnPropertyNames;
@@ -2706,6 +2707,31 @@ var StorageService = class extends ServiceModule {
2706
2707
  async getInfo(fileId, options) {
2707
2708
  return this._get(`/files/${fileId}/info`, options);
2708
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
+ }
2709
2735
  /**
2710
2736
  * Get a signed view URL for inline display (img src, thumbnails).
2711
2737
  * Returns CloudFront signed URL (fast, ~1us) or S3 presigned fallback.
@@ -8960,6 +8986,220 @@ function useMedia() {
8960
8986
  );
8961
8987
  return { upload, cancelUpload, error, uploading };
8962
8988
  }
8989
+ function useFileStatus(options) {
8990
+ const { storage } = useScaleMule();
8991
+ const { fileId, pollIntervalMs = null, disabled = false } = options;
8992
+ const [status, setStatus] = useState(null);
8993
+ const [loading, setLoading] = useState(false);
8994
+ const [error, setError] = useState(null);
8995
+ const requestSeqRef = useRef(0);
8996
+ const fetchStatus = 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
+ useEffect(() => {
9019
+ void fetchStatus();
9020
+ }, [fetchStatus]);
9021
+ 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
+ }
9033
+ function ScaleMuleMedia(props) {
9034
+ const {
9035
+ fileId,
9036
+ mimeType,
9037
+ blobPreview,
9038
+ width,
9039
+ height,
9040
+ className,
9041
+ style,
9042
+ alt,
9043
+ pollIntervalMs = 2e3,
9044
+ renderPlaceholder,
9045
+ renderBlocked,
9046
+ renderOverride
9047
+ } = props;
9048
+ const { status, isReady } = useFileStatus({ fileId, pollIntervalMs });
9049
+ const isImage = mimeType.startsWith("image/");
9050
+ const isVideo = mimeType.startsWith("video/");
9051
+ const isAudio = mimeType.startsWith("audio/");
9052
+ const src = useMemo(() => {
9053
+ const scan = status?.scan.status;
9054
+ if (scan === "threat" || scan === "quarantined") return null;
9055
+ if (status && isReady) {
9056
+ if (isImage && status.urls.optimized) return status.urls.optimized;
9057
+ if (isVideo && status.urls.hls) return status.urls.hls;
9058
+ return status.urls.original ?? null;
9059
+ }
9060
+ if (blobPreview) return blobPreview;
9061
+ return null;
9062
+ }, [status, isReady, isImage, isVideo, blobPreview]);
9063
+ const renderState = useMemo(() => {
9064
+ const scan = status?.scan.status;
9065
+ if (scan === "threat" || scan === "quarantined") return "blocked";
9066
+ if (isReady) return "ready";
9067
+ if (blobPreview) return "preview";
9068
+ return "pending";
9069
+ }, [status?.scan.status, isReady, blobPreview]);
9070
+ const videoRef = React.useRef(null);
9071
+ useEffect(() => {
9072
+ if (!isVideo) return;
9073
+ if (!src) return;
9074
+ if (!src.endsWith(".m3u8")) return;
9075
+ const el = videoRef.current;
9076
+ if (!el) return;
9077
+ if (el.canPlayType("application/vnd.apple.mpegurl")) {
9078
+ el.src = src;
9079
+ return;
9080
+ }
9081
+ let cancelled = false;
9082
+ let hls = null;
9083
+ void (async () => {
9084
+ try {
9085
+ const moduleName = "hls.js";
9086
+ const mod = await import(
9087
+ /* @vite-ignore */
9088
+ /* webpackIgnore: true */
9089
+ moduleName
9090
+ ).catch(
9091
+ () => null
9092
+ );
9093
+ if (cancelled || !mod) return;
9094
+ const Hls = mod.default ?? mod;
9095
+ if (Hls.isSupported()) {
9096
+ hls = new Hls();
9097
+ hls.loadSource(src);
9098
+ hls.attachMedia(el);
9099
+ } else {
9100
+ el.src = src;
9101
+ }
9102
+ } catch {
9103
+ }
9104
+ })();
9105
+ return () => {
9106
+ cancelled = true;
9107
+ if (hls) hls.destroy();
9108
+ };
9109
+ }, [isVideo, src]);
9110
+ if (renderOverride) {
9111
+ return /* @__PURE__ */ jsx(Fragment, { children: renderOverride({ src, state: renderState }) });
9112
+ }
9113
+ if (renderState === "blocked") {
9114
+ return /* @__PURE__ */ jsx(Fragment, { children: renderBlocked ? renderBlocked() : /* @__PURE__ */ jsx(
9115
+ "div",
9116
+ {
9117
+ role: "alert",
9118
+ className,
9119
+ style: { padding: 12, color: "#a00", ...style },
9120
+ children: "This file was blocked."
9121
+ }
9122
+ ) });
9123
+ }
9124
+ if (renderState === "pending" && !src) {
9125
+ return /* @__PURE__ */ jsx(Fragment, { children: renderPlaceholder ? renderPlaceholder() : /* @__PURE__ */ jsx(
9126
+ "div",
9127
+ {
9128
+ role: "status",
9129
+ "aria-busy": true,
9130
+ className,
9131
+ style: { padding: 12, color: "#666", ...style },
9132
+ children: "Loading\u2026"
9133
+ }
9134
+ ) });
9135
+ }
9136
+ if (isImage) {
9137
+ const finalSrc = src && status?.urls.optimized && (width || height) ? appendQuery(src, {
9138
+ width: width ? String(width) : void 0,
9139
+ height: height ? String(height) : void 0,
9140
+ fit: "cover"
9141
+ }) : src;
9142
+ return /* @__PURE__ */ jsx(
9143
+ "img",
9144
+ {
9145
+ src: finalSrc ?? void 0,
9146
+ alt: alt ?? "",
9147
+ width,
9148
+ height,
9149
+ className,
9150
+ style
9151
+ }
9152
+ );
9153
+ }
9154
+ if (isVideo) {
9155
+ return /* @__PURE__ */ jsx(
9156
+ "video",
9157
+ {
9158
+ ref: videoRef,
9159
+ src: src && !src.endsWith(".m3u8") ? src : void 0,
9160
+ controls: true,
9161
+ playsInline: true,
9162
+ preload: "metadata",
9163
+ className,
9164
+ style,
9165
+ "aria-label": alt,
9166
+ width,
9167
+ height
9168
+ }
9169
+ );
9170
+ }
9171
+ if (isAudio) {
9172
+ return /* @__PURE__ */ jsx(
9173
+ "audio",
9174
+ {
9175
+ src: src ?? void 0,
9176
+ controls: true,
9177
+ preload: "metadata",
9178
+ className,
9179
+ style,
9180
+ "aria-label": alt
9181
+ }
9182
+ );
9183
+ }
9184
+ return /* @__PURE__ */ jsx(
9185
+ "a",
9186
+ {
9187
+ href: src ?? "#",
9188
+ target: "_blank",
9189
+ rel: "noopener noreferrer",
9190
+ className,
9191
+ style,
9192
+ children: alt ?? "Download file"
9193
+ }
9194
+ );
9195
+ }
9196
+ function appendQuery(url, params) {
9197
+ const filtered = Object.entries(params).filter(([, v]) => v != null && v !== "");
9198
+ if (filtered.length === 0) return url;
9199
+ const sep = url.includes("?") ? "&" : "?";
9200
+ const qs = new URLSearchParams(filtered).toString();
9201
+ return url + sep + qs;
9202
+ }
8963
9203
 
8964
9204
  // src/hooks/useMoney.ts
8965
9205
  var useMoney = useMoneyClient;
@@ -10746,4 +10986,4 @@ function createSafeLogger(prefix) {
10746
10986
  };
10747
10987
  }
10748
10988
 
10749
- 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 };
10989
+ export { FeedbackWidget, ScaleMuleApiError, ScaleMuleClient2 as ScaleMuleClient, ScaleMuleMedia, 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",
3
+ "version": "0.1.5",
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.39"
56
+ "@scalemule/sdk": "^0.0.41"
57
57
  },
58
58
  "peerDependencies": {
59
59
  "next": ">=14.0.0",