@scalemule/nextjs 0.1.31 → 0.1.33
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/client.js +263 -5
- package/dist/client.mjs +263 -5
- package/dist/index.d.mts +5 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +143 -32
- package/dist/index.mjs +143 -32
- package/dist/server/index.js +673 -636
- package/dist/server/index.mjs +674 -637
- package/package.json +2 -2
package/dist/client.js
CHANGED
|
@@ -1,16 +1,273 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// node_modules/@scalemule/sdk/dist/index.mjs
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
3
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
6
|
var __esm = (fn, res) => function __init() {
|
|
5
7
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
8
|
};
|
|
7
|
-
var
|
|
8
|
-
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
12
|
+
};
|
|
13
|
+
var upload_resume_exports = {};
|
|
14
|
+
__export(upload_resume_exports, {
|
|
15
|
+
UploadResumeStore: () => UploadResumeStore
|
|
16
|
+
});
|
|
17
|
+
var DB_NAME;
|
|
18
|
+
var STORE_NAME;
|
|
19
|
+
var DB_VERSION;
|
|
20
|
+
var MAX_AGE_MS;
|
|
21
|
+
var UploadResumeStore;
|
|
22
|
+
var init_upload_resume = __esm({
|
|
23
|
+
"src/services/upload-resume.ts"() {
|
|
24
|
+
DB_NAME = "sm_upload_sessions_v1";
|
|
25
|
+
STORE_NAME = "sessions";
|
|
26
|
+
DB_VERSION = 1;
|
|
27
|
+
MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
28
|
+
UploadResumeStore = class {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.db = null;
|
|
31
|
+
}
|
|
32
|
+
/** Generate a deterministic resume key from upload identity */
|
|
33
|
+
static async generateResumeKey(appId, userId, filename, size, lastModified) {
|
|
34
|
+
const raw = `${appId}:${userId}:${filename}:${size}:${lastModified ?? 0}`;
|
|
35
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
36
|
+
const buffer = new TextEncoder().encode(raw);
|
|
37
|
+
const hash2 = await crypto.subtle.digest("SHA-256", buffer);
|
|
38
|
+
return Array.from(new Uint8Array(hash2)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
39
|
+
}
|
|
40
|
+
let hash = 0;
|
|
41
|
+
for (let i = 0; i < raw.length; i++) {
|
|
42
|
+
const chr = raw.charCodeAt(i);
|
|
43
|
+
hash = (hash << 5) - hash + chr;
|
|
44
|
+
hash |= 0;
|
|
45
|
+
}
|
|
46
|
+
return `fallback_${Math.abs(hash).toString(36)}`;
|
|
47
|
+
}
|
|
48
|
+
/** Open the IndexedDB store. No-ops if IndexedDB is unavailable. */
|
|
49
|
+
async open() {
|
|
50
|
+
if (typeof indexedDB === "undefined") return;
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
53
|
+
request.onupgradeneeded = () => {
|
|
54
|
+
const db = request.result;
|
|
55
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
56
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "key" });
|
|
57
|
+
store.createIndex("updated_at", "updated_at");
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
request.onsuccess = () => {
|
|
61
|
+
this.db = request.result;
|
|
62
|
+
resolve();
|
|
63
|
+
};
|
|
64
|
+
request.onerror = () => {
|
|
65
|
+
reject(request.error);
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/** Get a resume session by key. Returns null if not found or stale. */
|
|
70
|
+
async get(key) {
|
|
71
|
+
if (!this.db) return null;
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
const tx = this.db.transaction(STORE_NAME, "readonly");
|
|
74
|
+
const store = tx.objectStore(STORE_NAME);
|
|
75
|
+
const request = store.get(key);
|
|
76
|
+
request.onsuccess = () => {
|
|
77
|
+
const entry = request.result;
|
|
78
|
+
if (!entry) {
|
|
79
|
+
resolve(null);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (Date.now() - entry.updated_at > MAX_AGE_MS) {
|
|
83
|
+
this.remove(key).catch(() => {
|
|
84
|
+
});
|
|
85
|
+
resolve(null);
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
resolve(entry.session);
|
|
89
|
+
};
|
|
90
|
+
request.onerror = () => resolve(null);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/** Save a new resume session. */
|
|
94
|
+
async save(key, session) {
|
|
95
|
+
if (!this.db) return;
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
98
|
+
const store = tx.objectStore(STORE_NAME);
|
|
99
|
+
const entry = {
|
|
100
|
+
key,
|
|
101
|
+
session: { ...session, created_at: Date.now() },
|
|
102
|
+
updated_at: Date.now()
|
|
103
|
+
};
|
|
104
|
+
const request = store.put(entry);
|
|
105
|
+
request.onsuccess = () => resolve();
|
|
106
|
+
request.onerror = () => reject(request.error);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/** Update a single completed part in an existing session. */
|
|
110
|
+
async updatePart(key, partNumber, etag) {
|
|
111
|
+
if (!this.db) return;
|
|
112
|
+
return new Promise((resolve) => {
|
|
113
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
114
|
+
const store = tx.objectStore(STORE_NAME);
|
|
115
|
+
const getRequest = store.get(key);
|
|
116
|
+
getRequest.onsuccess = () => {
|
|
117
|
+
const entry = getRequest.result;
|
|
118
|
+
if (!entry) {
|
|
119
|
+
resolve();
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const existing = entry.session.completed_parts.find((p) => p.part_number === partNumber);
|
|
123
|
+
if (!existing) {
|
|
124
|
+
entry.session.completed_parts.push({ part_number: partNumber, etag });
|
|
125
|
+
}
|
|
126
|
+
entry.updated_at = Date.now();
|
|
127
|
+
const putRequest = store.put(entry);
|
|
128
|
+
putRequest.onsuccess = () => resolve();
|
|
129
|
+
putRequest.onerror = () => resolve();
|
|
130
|
+
};
|
|
131
|
+
getRequest.onerror = () => resolve();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
/** Remove a resume session (e.g., after successful completion). */
|
|
135
|
+
async remove(key) {
|
|
136
|
+
if (!this.db) return;
|
|
137
|
+
return new Promise((resolve) => {
|
|
138
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
139
|
+
const store = tx.objectStore(STORE_NAME);
|
|
140
|
+
const request = store.delete(key);
|
|
141
|
+
request.onsuccess = () => resolve();
|
|
142
|
+
request.onerror = () => resolve();
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** Purge all stale entries (older than MAX_AGE_MS). */
|
|
146
|
+
async purgeStale() {
|
|
147
|
+
if (!this.db) return 0;
|
|
148
|
+
return new Promise((resolve) => {
|
|
149
|
+
const cutoff = Date.now() - MAX_AGE_MS;
|
|
150
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
151
|
+
const store = tx.objectStore(STORE_NAME);
|
|
152
|
+
const index = store.index("updated_at");
|
|
153
|
+
const range = IDBKeyRange.upperBound(cutoff);
|
|
154
|
+
const request = index.openCursor(range);
|
|
155
|
+
let count = 0;
|
|
156
|
+
request.onsuccess = () => {
|
|
157
|
+
const cursor = request.result;
|
|
158
|
+
if (cursor) {
|
|
159
|
+
cursor.delete();
|
|
160
|
+
count++;
|
|
161
|
+
cursor.continue();
|
|
162
|
+
} else {
|
|
163
|
+
resolve(count);
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
request.onerror = () => resolve(0);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
/** Close the database connection. */
|
|
170
|
+
close() {
|
|
171
|
+
if (this.db) {
|
|
172
|
+
this.db.close();
|
|
173
|
+
this.db = null;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
9
177
|
}
|
|
10
178
|
});
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
179
|
+
var upload_compression_exports = {};
|
|
180
|
+
__export(upload_compression_exports, {
|
|
181
|
+
maybeCompressImage: () => maybeCompressImage
|
|
182
|
+
});
|
|
183
|
+
async function maybeCompressImage(file, userConfig, sessionId, telemetry) {
|
|
184
|
+
const type = file.type?.toLowerCase() || "";
|
|
185
|
+
if (!type.startsWith("image/")) return null;
|
|
186
|
+
if (SKIP_TYPES.has(type)) {
|
|
187
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", { reason: "format", type });
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
if (!COMPRESSIBLE_TYPES.has(type)) {
|
|
191
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", { reason: "unsupported_type", type });
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
if (file.size < MIN_COMPRESS_SIZE) {
|
|
195
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", { reason: "too_small", size: file.size });
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
const networkType = getNetworkEffectiveType();
|
|
199
|
+
const defaultProfile = { maxWidth: 3840, maxHeight: 3840, quality: 0.85, maxSizeMB: 5 };
|
|
200
|
+
const networkProfile = NETWORK_PROFILES[networkType] ?? defaultProfile;
|
|
201
|
+
const config = {
|
|
202
|
+
maxWidth: userConfig?.maxWidth ?? networkProfile.maxWidth,
|
|
203
|
+
maxHeight: userConfig?.maxHeight ?? networkProfile.maxHeight,
|
|
204
|
+
quality: userConfig?.quality ?? networkProfile.quality,
|
|
205
|
+
maxSizeMB: userConfig?.maxSizeMB ?? networkProfile.maxSizeMB
|
|
206
|
+
};
|
|
207
|
+
telemetry?.emit(sessionId, "upload.compression.started", {
|
|
208
|
+
original_size: file.size,
|
|
209
|
+
network: networkType,
|
|
210
|
+
target_quality: config.quality
|
|
211
|
+
});
|
|
212
|
+
try {
|
|
213
|
+
const imageCompression = await loadImageCompression();
|
|
214
|
+
if (!imageCompression) {
|
|
215
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", { reason: "library_unavailable" });
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
const compressed = await imageCompression(file, {
|
|
219
|
+
maxSizeMB: config.maxSizeMB,
|
|
220
|
+
maxWidthOrHeight: Math.max(config.maxWidth, config.maxHeight),
|
|
221
|
+
initialQuality: config.quality,
|
|
222
|
+
useWebWorker: true,
|
|
223
|
+
fileType: type === "image/png" ? "image/webp" : void 0
|
|
224
|
+
});
|
|
225
|
+
if (compressed.size >= file.size * 0.95) {
|
|
226
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", {
|
|
227
|
+
reason: "no_size_reduction",
|
|
228
|
+
original_size: file.size,
|
|
229
|
+
compressed_size: compressed.size
|
|
230
|
+
});
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
telemetry?.emit(sessionId, "upload.compression.completed", {
|
|
234
|
+
original_size: file.size,
|
|
235
|
+
compressed_size: compressed.size,
|
|
236
|
+
ratio: (compressed.size / file.size).toFixed(2)
|
|
237
|
+
});
|
|
238
|
+
return compressed;
|
|
239
|
+
} catch (err) {
|
|
240
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", {
|
|
241
|
+
reason: "error",
|
|
242
|
+
error: err instanceof Error ? err.message : "Unknown compression error"
|
|
243
|
+
});
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
async function loadImageCompression() {
|
|
248
|
+
if (cachedImport === false) return null;
|
|
249
|
+
if (cachedImport) return cachedImport;
|
|
250
|
+
try {
|
|
251
|
+
const mod = await Function('return import("browser-image-compression")')();
|
|
252
|
+
cachedImport = mod.default || mod;
|
|
253
|
+
return cachedImport;
|
|
254
|
+
} catch {
|
|
255
|
+
cachedImport = false;
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
function getNetworkEffectiveType() {
|
|
260
|
+
if (typeof navigator !== "undefined" && "connection" in navigator) {
|
|
261
|
+
const conn = navigator.connection;
|
|
262
|
+
return conn?.effectiveType || "4g";
|
|
263
|
+
}
|
|
264
|
+
return "4g";
|
|
265
|
+
}
|
|
266
|
+
var MIN_COMPRESS_SIZE;
|
|
267
|
+
var COMPRESSIBLE_TYPES;
|
|
268
|
+
var SKIP_TYPES;
|
|
269
|
+
var NETWORK_PROFILES;
|
|
270
|
+
var cachedImport;
|
|
14
271
|
var STORAGE_KEYS = {
|
|
15
272
|
SESSION: "scalemule_session",
|
|
16
273
|
USER_ID: "scalemule_user_id",
|
|
@@ -137,6 +394,7 @@ var PHONE_COUNTRIES = [
|
|
|
137
394
|
{ code: "NZ", name: "New Zealand", dialCode: "+64" }
|
|
138
395
|
];
|
|
139
396
|
[...PHONE_COUNTRIES].sort((a, b) => b.dialCode.length - a.dialCode.length);
|
|
397
|
+
init_upload_resume();
|
|
140
398
|
|
|
141
399
|
// src/types/index.ts
|
|
142
400
|
var ScaleMuleApiError = class extends Error {
|
package/dist/client.mjs
CHANGED
|
@@ -1,14 +1,271 @@
|
|
|
1
|
+
// node_modules/@scalemule/sdk/dist/index.mjs
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
1
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
2
4
|
var __esm = (fn, res) => function __init() {
|
|
3
5
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
4
6
|
};
|
|
5
|
-
var
|
|
6
|
-
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var upload_resume_exports = {};
|
|
12
|
+
__export(upload_resume_exports, {
|
|
13
|
+
UploadResumeStore: () => UploadResumeStore
|
|
14
|
+
});
|
|
15
|
+
var DB_NAME;
|
|
16
|
+
var STORE_NAME;
|
|
17
|
+
var DB_VERSION;
|
|
18
|
+
var MAX_AGE_MS;
|
|
19
|
+
var UploadResumeStore;
|
|
20
|
+
var init_upload_resume = __esm({
|
|
21
|
+
"src/services/upload-resume.ts"() {
|
|
22
|
+
DB_NAME = "sm_upload_sessions_v1";
|
|
23
|
+
STORE_NAME = "sessions";
|
|
24
|
+
DB_VERSION = 1;
|
|
25
|
+
MAX_AGE_MS = 24 * 60 * 60 * 1e3;
|
|
26
|
+
UploadResumeStore = class {
|
|
27
|
+
constructor() {
|
|
28
|
+
this.db = null;
|
|
29
|
+
}
|
|
30
|
+
/** Generate a deterministic resume key from upload identity */
|
|
31
|
+
static async generateResumeKey(appId, userId, filename, size, lastModified) {
|
|
32
|
+
const raw = `${appId}:${userId}:${filename}:${size}:${lastModified ?? 0}`;
|
|
33
|
+
if (typeof crypto !== "undefined" && crypto.subtle) {
|
|
34
|
+
const buffer = new TextEncoder().encode(raw);
|
|
35
|
+
const hash2 = await crypto.subtle.digest("SHA-256", buffer);
|
|
36
|
+
return Array.from(new Uint8Array(hash2)).map((b) => b.toString(16).padStart(2, "0")).join("");
|
|
37
|
+
}
|
|
38
|
+
let hash = 0;
|
|
39
|
+
for (let i = 0; i < raw.length; i++) {
|
|
40
|
+
const chr = raw.charCodeAt(i);
|
|
41
|
+
hash = (hash << 5) - hash + chr;
|
|
42
|
+
hash |= 0;
|
|
43
|
+
}
|
|
44
|
+
return `fallback_${Math.abs(hash).toString(36)}`;
|
|
45
|
+
}
|
|
46
|
+
/** Open the IndexedDB store. No-ops if IndexedDB is unavailable. */
|
|
47
|
+
async open() {
|
|
48
|
+
if (typeof indexedDB === "undefined") return;
|
|
49
|
+
return new Promise((resolve, reject) => {
|
|
50
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION);
|
|
51
|
+
request.onupgradeneeded = () => {
|
|
52
|
+
const db = request.result;
|
|
53
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
54
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: "key" });
|
|
55
|
+
store.createIndex("updated_at", "updated_at");
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
request.onsuccess = () => {
|
|
59
|
+
this.db = request.result;
|
|
60
|
+
resolve();
|
|
61
|
+
};
|
|
62
|
+
request.onerror = () => {
|
|
63
|
+
reject(request.error);
|
|
64
|
+
};
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
/** Get a resume session by key. Returns null if not found or stale. */
|
|
68
|
+
async get(key) {
|
|
69
|
+
if (!this.db) return null;
|
|
70
|
+
return new Promise((resolve) => {
|
|
71
|
+
const tx = this.db.transaction(STORE_NAME, "readonly");
|
|
72
|
+
const store = tx.objectStore(STORE_NAME);
|
|
73
|
+
const request = store.get(key);
|
|
74
|
+
request.onsuccess = () => {
|
|
75
|
+
const entry = request.result;
|
|
76
|
+
if (!entry) {
|
|
77
|
+
resolve(null);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (Date.now() - entry.updated_at > MAX_AGE_MS) {
|
|
81
|
+
this.remove(key).catch(() => {
|
|
82
|
+
});
|
|
83
|
+
resolve(null);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
resolve(entry.session);
|
|
87
|
+
};
|
|
88
|
+
request.onerror = () => resolve(null);
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
/** Save a new resume session. */
|
|
92
|
+
async save(key, session) {
|
|
93
|
+
if (!this.db) return;
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
96
|
+
const store = tx.objectStore(STORE_NAME);
|
|
97
|
+
const entry = {
|
|
98
|
+
key,
|
|
99
|
+
session: { ...session, created_at: Date.now() },
|
|
100
|
+
updated_at: Date.now()
|
|
101
|
+
};
|
|
102
|
+
const request = store.put(entry);
|
|
103
|
+
request.onsuccess = () => resolve();
|
|
104
|
+
request.onerror = () => reject(request.error);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/** Update a single completed part in an existing session. */
|
|
108
|
+
async updatePart(key, partNumber, etag) {
|
|
109
|
+
if (!this.db) return;
|
|
110
|
+
return new Promise((resolve) => {
|
|
111
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
112
|
+
const store = tx.objectStore(STORE_NAME);
|
|
113
|
+
const getRequest = store.get(key);
|
|
114
|
+
getRequest.onsuccess = () => {
|
|
115
|
+
const entry = getRequest.result;
|
|
116
|
+
if (!entry) {
|
|
117
|
+
resolve();
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const existing = entry.session.completed_parts.find((p) => p.part_number === partNumber);
|
|
121
|
+
if (!existing) {
|
|
122
|
+
entry.session.completed_parts.push({ part_number: partNumber, etag });
|
|
123
|
+
}
|
|
124
|
+
entry.updated_at = Date.now();
|
|
125
|
+
const putRequest = store.put(entry);
|
|
126
|
+
putRequest.onsuccess = () => resolve();
|
|
127
|
+
putRequest.onerror = () => resolve();
|
|
128
|
+
};
|
|
129
|
+
getRequest.onerror = () => resolve();
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** Remove a resume session (e.g., after successful completion). */
|
|
133
|
+
async remove(key) {
|
|
134
|
+
if (!this.db) return;
|
|
135
|
+
return new Promise((resolve) => {
|
|
136
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
137
|
+
const store = tx.objectStore(STORE_NAME);
|
|
138
|
+
const request = store.delete(key);
|
|
139
|
+
request.onsuccess = () => resolve();
|
|
140
|
+
request.onerror = () => resolve();
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
/** Purge all stale entries (older than MAX_AGE_MS). */
|
|
144
|
+
async purgeStale() {
|
|
145
|
+
if (!this.db) return 0;
|
|
146
|
+
return new Promise((resolve) => {
|
|
147
|
+
const cutoff = Date.now() - MAX_AGE_MS;
|
|
148
|
+
const tx = this.db.transaction(STORE_NAME, "readwrite");
|
|
149
|
+
const store = tx.objectStore(STORE_NAME);
|
|
150
|
+
const index = store.index("updated_at");
|
|
151
|
+
const range = IDBKeyRange.upperBound(cutoff);
|
|
152
|
+
const request = index.openCursor(range);
|
|
153
|
+
let count = 0;
|
|
154
|
+
request.onsuccess = () => {
|
|
155
|
+
const cursor = request.result;
|
|
156
|
+
if (cursor) {
|
|
157
|
+
cursor.delete();
|
|
158
|
+
count++;
|
|
159
|
+
cursor.continue();
|
|
160
|
+
} else {
|
|
161
|
+
resolve(count);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
request.onerror = () => resolve(0);
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
/** Close the database connection. */
|
|
168
|
+
close() {
|
|
169
|
+
if (this.db) {
|
|
170
|
+
this.db.close();
|
|
171
|
+
this.db = null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
7
175
|
}
|
|
8
176
|
});
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
177
|
+
var upload_compression_exports = {};
|
|
178
|
+
__export(upload_compression_exports, {
|
|
179
|
+
maybeCompressImage: () => maybeCompressImage
|
|
180
|
+
});
|
|
181
|
+
async function maybeCompressImage(file, userConfig, sessionId, telemetry) {
|
|
182
|
+
const type = file.type?.toLowerCase() || "";
|
|
183
|
+
if (!type.startsWith("image/")) return null;
|
|
184
|
+
if (SKIP_TYPES.has(type)) {
|
|
185
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", { reason: "format", type });
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
if (!COMPRESSIBLE_TYPES.has(type)) {
|
|
189
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", { reason: "unsupported_type", type });
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
if (file.size < MIN_COMPRESS_SIZE) {
|
|
193
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", { reason: "too_small", size: file.size });
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
const networkType = getNetworkEffectiveType();
|
|
197
|
+
const defaultProfile = { maxWidth: 3840, maxHeight: 3840, quality: 0.85, maxSizeMB: 5 };
|
|
198
|
+
const networkProfile = NETWORK_PROFILES[networkType] ?? defaultProfile;
|
|
199
|
+
const config = {
|
|
200
|
+
maxWidth: userConfig?.maxWidth ?? networkProfile.maxWidth,
|
|
201
|
+
maxHeight: userConfig?.maxHeight ?? networkProfile.maxHeight,
|
|
202
|
+
quality: userConfig?.quality ?? networkProfile.quality,
|
|
203
|
+
maxSizeMB: userConfig?.maxSizeMB ?? networkProfile.maxSizeMB
|
|
204
|
+
};
|
|
205
|
+
telemetry?.emit(sessionId, "upload.compression.started", {
|
|
206
|
+
original_size: file.size,
|
|
207
|
+
network: networkType,
|
|
208
|
+
target_quality: config.quality
|
|
209
|
+
});
|
|
210
|
+
try {
|
|
211
|
+
const imageCompression = await loadImageCompression();
|
|
212
|
+
if (!imageCompression) {
|
|
213
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", { reason: "library_unavailable" });
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
const compressed = await imageCompression(file, {
|
|
217
|
+
maxSizeMB: config.maxSizeMB,
|
|
218
|
+
maxWidthOrHeight: Math.max(config.maxWidth, config.maxHeight),
|
|
219
|
+
initialQuality: config.quality,
|
|
220
|
+
useWebWorker: true,
|
|
221
|
+
fileType: type === "image/png" ? "image/webp" : void 0
|
|
222
|
+
});
|
|
223
|
+
if (compressed.size >= file.size * 0.95) {
|
|
224
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", {
|
|
225
|
+
reason: "no_size_reduction",
|
|
226
|
+
original_size: file.size,
|
|
227
|
+
compressed_size: compressed.size
|
|
228
|
+
});
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
telemetry?.emit(sessionId, "upload.compression.completed", {
|
|
232
|
+
original_size: file.size,
|
|
233
|
+
compressed_size: compressed.size,
|
|
234
|
+
ratio: (compressed.size / file.size).toFixed(2)
|
|
235
|
+
});
|
|
236
|
+
return compressed;
|
|
237
|
+
} catch (err) {
|
|
238
|
+
telemetry?.emit(sessionId, "upload.compression.skipped", {
|
|
239
|
+
reason: "error",
|
|
240
|
+
error: err instanceof Error ? err.message : "Unknown compression error"
|
|
241
|
+
});
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
async function loadImageCompression() {
|
|
246
|
+
if (cachedImport === false) return null;
|
|
247
|
+
if (cachedImport) return cachedImport;
|
|
248
|
+
try {
|
|
249
|
+
const mod = await Function('return import("browser-image-compression")')();
|
|
250
|
+
cachedImport = mod.default || mod;
|
|
251
|
+
return cachedImport;
|
|
252
|
+
} catch {
|
|
253
|
+
cachedImport = false;
|
|
254
|
+
return null;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
function getNetworkEffectiveType() {
|
|
258
|
+
if (typeof navigator !== "undefined" && "connection" in navigator) {
|
|
259
|
+
const conn = navigator.connection;
|
|
260
|
+
return conn?.effectiveType || "4g";
|
|
261
|
+
}
|
|
262
|
+
return "4g";
|
|
263
|
+
}
|
|
264
|
+
var MIN_COMPRESS_SIZE;
|
|
265
|
+
var COMPRESSIBLE_TYPES;
|
|
266
|
+
var SKIP_TYPES;
|
|
267
|
+
var NETWORK_PROFILES;
|
|
268
|
+
var cachedImport;
|
|
12
269
|
var STORAGE_KEYS = {
|
|
13
270
|
SESSION: "scalemule_session",
|
|
14
271
|
USER_ID: "scalemule_user_id",
|
|
@@ -135,6 +392,7 @@ var PHONE_COUNTRIES = [
|
|
|
135
392
|
{ code: "NZ", name: "New Zealand", dialCode: "+64" }
|
|
136
393
|
];
|
|
137
394
|
[...PHONE_COUNTRIES].sort((a, b) => b.dialCode.length - a.dialCode.length);
|
|
395
|
+
init_upload_resume();
|
|
138
396
|
|
|
139
397
|
// src/types/index.ts
|
|
140
398
|
var ScaleMuleApiError = class extends Error {
|
package/dist/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { ApiError, RealtimeService, StorageService, PhotoService, VideoService, AudioService, TtsService, TtsJobStatus, TtsAudioInfo, FileStatus } from '@scalemule/sdk';
|
|
2
|
+
import { ApiError, RealtimeService, StorageService, PhotoService, VideoService, AudioService, TtsService, SocialService, SocialPolicyService, TtsJobStatus, TtsAudioInfo, FileStatus } from '@scalemule/sdk';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { ReactNode, ReactElement } from 'react';
|
|
5
5
|
import { MoneyClient } from '@scalemule/money';
|
|
@@ -223,6 +223,10 @@ interface ScaleMuleContextValue {
|
|
|
223
223
|
media: unknown;
|
|
224
224
|
/** Base SDK TTS service — exposed for `useTtsJob()` and direct narration requests */
|
|
225
225
|
tts: TtsService;
|
|
226
|
+
/** Base SDK social graph service — follow graph, posts, feed, likes, and activity */
|
|
227
|
+
social: SocialService;
|
|
228
|
+
/** Base SDK social policy service — privacy decisions, requests, blocks, reports */
|
|
229
|
+
socialPolicy: SocialPolicyService;
|
|
226
230
|
/**
|
|
227
231
|
* Default media policy for `useMedia()` calls. Set via
|
|
228
232
|
* `<ScaleMuleProvider mediaPolicy="…">`; per-call overrides win.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
|
-
import { ApiError, RealtimeService, StorageService, PhotoService, VideoService, AudioService, TtsService, TtsJobStatus, TtsAudioInfo, FileStatus } from '@scalemule/sdk';
|
|
2
|
+
import { ApiError, RealtimeService, StorageService, PhotoService, VideoService, AudioService, TtsService, SocialService, SocialPolicyService, TtsJobStatus, TtsAudioInfo, FileStatus } from '@scalemule/sdk';
|
|
3
3
|
import * as React from 'react';
|
|
4
4
|
import { ReactNode, ReactElement } from 'react';
|
|
5
5
|
import { MoneyClient } from '@scalemule/money';
|
|
@@ -223,6 +223,10 @@ interface ScaleMuleContextValue {
|
|
|
223
223
|
media: unknown;
|
|
224
224
|
/** Base SDK TTS service — exposed for `useTtsJob()` and direct narration requests */
|
|
225
225
|
tts: TtsService;
|
|
226
|
+
/** Base SDK social graph service — follow graph, posts, feed, likes, and activity */
|
|
227
|
+
social: SocialService;
|
|
228
|
+
/** Base SDK social policy service — privacy decisions, requests, blocks, reports */
|
|
229
|
+
socialPolicy: SocialPolicyService;
|
|
226
230
|
/**
|
|
227
231
|
* Default media policy for `useMedia()` calls. Set via
|
|
228
232
|
* `<ScaleMuleProvider mediaPolicy="…">`; per-call overrides win.
|