@scalemule/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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ScaleMule Inc.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,424 @@
1
+ # @scalemule/sdk
2
+
3
+ Official TypeScript/JavaScript SDK for ScaleMule Backend-as-a-Service.
4
+
5
+ Zero dependencies. Works in browsers, Node.js 18+, and edge runtimes.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @scalemule/sdk
11
+ ```
12
+
13
+ ## Quick Start
14
+
15
+ ```ts
16
+ import { ScaleMule } from '@scalemule/sdk'
17
+
18
+ const sm = new ScaleMule({ apiKey: 'sm_pb_xxx' })
19
+
20
+ // Register
21
+ const { data, error } = await sm.auth.register({
22
+ email: 'user@example.com',
23
+ password: 'SecurePassword123!',
24
+ name: 'Jane Doe',
25
+ })
26
+
27
+ if (error) {
28
+ console.error(error.code, error.message)
29
+ } else {
30
+ console.log('User created:', data.id)
31
+ }
32
+ ```
33
+
34
+ Every method returns `{ data, error }` — never throws on API errors.
35
+
36
+ ## Configuration
37
+
38
+ ```ts
39
+ const sm = new ScaleMule({
40
+ apiKey: 'sm_pb_xxx', // required
41
+ environment: 'prod', // 'dev' | 'prod' (default: 'prod')
42
+ baseUrl: 'https://api.example.com', // overrides environment preset
43
+ timeout: 30000, // request timeout in ms
44
+ retry: {
45
+ maxRetries: 2, // retry on 429/5xx (default: 2)
46
+ backoffMs: 300, // base delay with jitter (default: 300)
47
+ },
48
+ enableRateLimitQueue: true, // auto-queue on 429
49
+ enableOfflineQueue: true, // queue when offline, sync on reconnect
50
+ debug: false, // log requests to console
51
+ })
52
+ ```
53
+
54
+ | Environment | Gateway URL |
55
+ |-------------|-------------|
56
+ | `prod` | `https://api.scalemule.com` |
57
+ | `dev` | `https://api-dev.scalemule.com` |
58
+
59
+ ## Response Contract
60
+
61
+ All methods return `ApiResponse<T>`:
62
+
63
+ ```ts
64
+ type ApiResponse<T> = {
65
+ data: T | null // null when error is present
66
+ error: ApiError | null // null on success
67
+ }
68
+
69
+ type ApiError = {
70
+ code: string // machine-readable (e.g., 'unauthorized')
71
+ message: string // human-readable
72
+ status: number // HTTP status code
73
+ details?: Record<string, unknown>
74
+ }
75
+ ```
76
+
77
+ Paginated methods return `PaginatedResponse<T>`:
78
+
79
+ ```ts
80
+ type PaginatedResponse<T> = {
81
+ data: T[]
82
+ metadata: { total: number; totalPages: number; page: number; perPage: number }
83
+ error: ApiError | null
84
+ }
85
+ ```
86
+
87
+ ## Auth
88
+
89
+ ```ts
90
+ // Register & Login
91
+ const { data } = await sm.auth.register({ email, password, name })
92
+ const { data } = await sm.auth.login({ email, password })
93
+ await sm.auth.logout()
94
+
95
+ // Current user
96
+ const { data: user } = await sm.auth.me()
97
+
98
+ // Session management
99
+ await sm.auth.refreshSession()
100
+ sm.setAccessToken(token)
101
+ sm.clearAccessToken()
102
+
103
+ // Password
104
+ await sm.auth.forgotPassword({ email })
105
+ await sm.auth.resetPassword({ token, newPassword })
106
+ await sm.auth.changePassword({ currentPassword, newPassword })
107
+
108
+ // Email verification
109
+ await sm.auth.verifyEmail({ token })
110
+ await sm.auth.resendVerification()
111
+
112
+ // Phone OTP
113
+ await sm.auth.sendPhoneOtp({ phone, purpose: 'verify_phone' })
114
+ await sm.auth.verifyPhoneOtp({ phone, code })
115
+ await sm.auth.loginWithPhone({ phone, code })
116
+ // SDK auto-sanitizes formatting before send:
117
+ // "(415) 555-1234" -> "+4155551234"
118
+
119
+ // OAuth
120
+ const { data } = await sm.auth.getOAuthUrl('google', { redirectUri })
121
+ const { data } = await sm.auth.handleOAuthCallback({ provider, code, state })
122
+
123
+ // Sessions, Devices, Login History
124
+ const { data } = await sm.auth.sessions.list()
125
+ await sm.auth.sessions.revoke(sessionId)
126
+ const { data } = await sm.auth.devices.list()
127
+ const { data } = await sm.auth.loginHistory.list()
128
+
129
+ // MFA
130
+ const { data } = await sm.auth.mfa.getStatus()
131
+ const { data } = await sm.auth.mfa.setupTotp()
132
+ await sm.auth.mfa.verifySetup({ code })
133
+ await sm.auth.mfa.disable({ password })
134
+ ```
135
+
136
+ ### Phone Country Picker Helpers
137
+
138
+ ```ts
139
+ import { PHONE_COUNTRIES, composePhoneNumber, normalizeAndValidatePhone } from '@scalemule/sdk'
140
+
141
+ const us = PHONE_COUNTRIES.find((country) => country.code === 'US')
142
+ const phone = composePhoneNumber(us?.dialCode ?? '+1', '(415) 555-1234')
143
+ const normalized = normalizeAndValidatePhone(phone)
144
+
145
+ if (!normalized.valid) {
146
+ console.error(normalized.error)
147
+ } else {
148
+ await sm.auth.register({ email, password, phone: normalized.normalized! })
149
+ }
150
+ ```
151
+
152
+ ## Storage
153
+
154
+ ```ts
155
+ // Upload (3-step presigned URL flow, hidden from you)
156
+ const { data, error } = await sm.storage.upload(file, {
157
+ filename: 'photo.jpg',
158
+ isPublic: false,
159
+ onProgress: (pct) => console.log(`${pct}%`),
160
+ signal: abortController.signal,
161
+ })
162
+
163
+ // Signed URLs for viewing/downloading
164
+ const { data } = await sm.storage.getViewUrl(fileId)
165
+ const { data: urls } = await sm.storage.getViewUrls(fileIds) // batch, up to 100
166
+ const { data } = await sm.storage.getDownloadUrl(fileId)
167
+
168
+ // File operations
169
+ const { data: info } = await sm.storage.getInfo(fileId)
170
+ const { data: files, metadata } = await sm.storage.list({ page: 1, perPage: 20 })
171
+ await sm.storage.delete(fileId)
172
+
173
+ // Split upload (server-side: get URL → client uploads to S3 → complete)
174
+ const { data: upload } = await sm.storage.getUploadUrl('photo.jpg', 'image/jpeg')
175
+ // ... client uploads to upload.upload_url ...
176
+ const { data: result } = await sm.storage.completeUpload(upload.file_id, upload.completion_token)
177
+ ```
178
+
179
+ ## Data
180
+
181
+ ```ts
182
+ // CRUD
183
+ const { data: doc } = await sm.data.create('todos', { title: 'Ship SDK', done: false })
184
+ const { data: doc } = await sm.data.get('todos', docId)
185
+ await sm.data.update('todos', docId, { done: true })
186
+ await sm.data.delete('todos', docId)
187
+
188
+ // Query with filters and sorting
189
+ const { data: docs, metadata } = await sm.data.query('todos', {
190
+ filters: [{ operator: 'eq', field: 'done', value: false }],
191
+ sort: [{ field: 'created_at', direction: 'desc' }],
192
+ page: 1,
193
+ perPage: 20,
194
+ })
195
+
196
+ // Collections
197
+ await sm.data.createCollection('todos')
198
+ const { data } = await sm.data.listCollections()
199
+
200
+ // My documents (filtered by current user)
201
+ const { data } = await sm.data.myDocuments('todos')
202
+
203
+ // Aggregations
204
+ const { data } = await sm.data.aggregate('orders', {
205
+ pipeline: [{ $group: { field: 'status', fn: 'count' } }],
206
+ })
207
+ ```
208
+
209
+ **Filter operators**: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `contains`
210
+
211
+ ## Video
212
+
213
+ ```ts
214
+ const { data } = await sm.video.upload(videoFile, {
215
+ onProgress: (pct) => console.log(`${pct}%`),
216
+ })
217
+ const { data } = await sm.video.get(videoId)
218
+ const { data } = await sm.video.getStreamUrl(videoId)
219
+ await sm.video.trackPlayback(videoId, { event: 'play', position: 0 })
220
+ ```
221
+
222
+ ## Realtime
223
+
224
+ ```ts
225
+ // Subscribe (auto-connects WebSocket on first call)
226
+ const unsub = sm.realtime.subscribe('chat:room-1', (msg) => {
227
+ console.log('Message:', msg)
228
+ })
229
+
230
+ // Publish
231
+ await sm.realtime.publish('chat:room-1', { text: 'hello' })
232
+
233
+ // Connection status
234
+ console.log(sm.realtime.status) // 'disconnected' | 'connecting' | 'connected' | 'reconnecting'
235
+
236
+ // Cleanup
237
+ unsub()
238
+ sm.realtime.disconnect()
239
+ ```
240
+
241
+ Features: auto-reconnect with backoff, re-subscribe on reconnect, heartbeat/ping-pong.
242
+
243
+ ## Chat
244
+
245
+ ```ts
246
+ const { data: convo } = await sm.chat.createConversation({ participantIds: [a, b] })
247
+ const { data: msg } = await sm.chat.sendMessage(convo.id, { content: 'Hello!' })
248
+ const { data: msgs } = await sm.chat.getMessages(convo.id, { limit: 50 })
249
+ await sm.chat.sendTyping(convo.id)
250
+ await sm.chat.markRead(convo.id)
251
+ ```
252
+
253
+ ## Social
254
+
255
+ ```ts
256
+ await sm.social.follow(userId)
257
+ await sm.social.unfollow(userId)
258
+ const { data: post } = await sm.social.createPost({ content: 'Hello!', visibility: 'public' })
259
+ const { data: feed } = await sm.social.getFeed({ limit: 20 })
260
+ await sm.social.like('post', postId)
261
+ const { data } = await sm.social.getComments(postId)
262
+ ```
263
+
264
+ ## Teams
265
+
266
+ ```ts
267
+ const { data: team } = await sm.teams.create({ name: 'Engineering' })
268
+ await sm.teams.invite(team.id, { email: 'dev@company.com', role: 'member' })
269
+ await sm.teams.acceptInvitation(token)
270
+ const { data: members } = await sm.teams.listMembers(team.id)
271
+ ```
272
+
273
+ ## Billing
274
+
275
+ ```ts
276
+ const { data: customer } = await sm.billing.createCustomer({ email, name })
277
+ const { data: sub } = await sm.billing.subscribe({ customerId, planId })
278
+ await sm.billing.reportUsage({ metric: 'api_calls', quantity: 1000 })
279
+ const { data: invoices } = await sm.billing.listInvoices()
280
+ ```
281
+
282
+ ## Analytics
283
+
284
+ ```ts
285
+ await sm.analytics.track('button_clicked', { buttonId: 'signup' })
286
+ await sm.analytics.trackPageView({ path: '/pricing' })
287
+ await sm.analytics.trackBatch([
288
+ { event: 'page_view', properties: { path: '/home' } },
289
+ { event: 'page_view', properties: { path: '/pricing' } },
290
+ ])
291
+ ```
292
+
293
+ ## More Services
294
+
295
+ ```ts
296
+ // Queue
297
+ await sm.queue.enqueue({ job_type: 'email.welcome', payload: { userId }, priority: 'high' })
298
+
299
+ // Scheduler
300
+ const { data } = await sm.scheduler.createJob({
301
+ name: 'daily-report', cron: '0 9 * * *',
302
+ type: 'webhook', config: { url: 'https://myapp.com/api/report' },
303
+ })
304
+ await sm.scheduler.pauseJob(jobId)
305
+ await sm.scheduler.runNow(jobId)
306
+
307
+ // Permissions
308
+ await sm.permissions.createRole({ name: 'editor' })
309
+ await sm.permissions.assignPermissions(roleId, ['posts.create', 'posts.edit'])
310
+ const { data: { allowed } } = await sm.permissions.check(userId, 'posts.create')
311
+
312
+ // Communication
313
+ await sm.communication.sendEmail({ to: 'user@example.com', subject: 'Welcome!', body: '<h1>Hi</h1>' })
314
+ await sm.communication.sendEmailTemplate('welcome', { to: email, variables: { name: 'Jane' } })
315
+
316
+ // Search
317
+ await sm.search.index('products', { id: '1', name: 'Widget', price: 9.99 })
318
+ const { data } = await sm.search.query('products', { query: 'widget', limit: 10 })
319
+
320
+ // Cache
321
+ await sm.cache.set('key', { value: 'data' }, { ttl: 3600 })
322
+ const { data } = await sm.cache.get('key')
323
+
324
+ // Graph
325
+ const { data: node } = await sm.graph.createNode({ label: 'person', properties: { name: 'Alice' } })
326
+ const { data: edge } = await sm.graph.createEdge({ fromId: a, toId: b, type: 'knows' })
327
+
328
+ // Content Moderation
329
+ await sm.flagContent.createFlag({ content_type: 'post', content_id: postId, category: 'spam' })
330
+
331
+ // Webhooks
332
+ await sm.webhooks.create({ url: 'https://myapp.com/hooks', events: ['auth.user.created'] })
333
+
334
+ // Leaderboard
335
+ await sm.leaderboard.submitScore(boardId, { userId, score: 1500 })
336
+
337
+ // Listings
338
+ const { data } = await sm.listings.nearby({ lat: 40.7, lng: -74.0, radius: 10 })
339
+
340
+ // Events
341
+ const { data: event } = await sm.events.create({ title: 'Launch Party', startDate: '2026-03-01' })
342
+
343
+ // Functions
344
+ const { data } = await sm.functions.invoke('resize', { imageUrl: '...', width: 200 })
345
+
346
+ // Photo
347
+ const { data } = await sm.photo.upload(imageFile)
348
+
349
+ // Compliance
350
+ await sm.compliance.log({ action: 'user.deleted', resourceType: 'user', resourceId: id })
351
+
352
+ // Orchestrator
353
+ const { data } = await sm.orchestrator.execute(workflowId, { userId })
354
+ ```
355
+
356
+ ## All 30 Services
357
+
358
+ | Service | Property | Description |
359
+ |---------|----------|-------------|
360
+ | Auth | `sm.auth` | Authentication, sessions, MFA, OAuth, phone |
361
+ | Storage | `sm.storage` | File upload (presigned S3), signed URLs |
362
+ | Realtime | `sm.realtime` | WebSocket pub/sub with auto-reconnect |
363
+ | Video | `sm.video` | Video upload, streaming, analytics |
364
+ | Data | `sm.data` | Document CRUD, queries, aggregations |
365
+ | Chat | `sm.chat` | Conversations, messages, typing, read receipts |
366
+ | Social | `sm.social` | Follow, posts, feed, likes, comments |
367
+ | Billing | `sm.billing` | Customers, subscriptions, invoices, usage |
368
+ | Analytics | `sm.analytics` | Event tracking, page views, funnels |
369
+ | Communication | `sm.communication` | Email, SMS, push notifications |
370
+ | Scheduler | `sm.scheduler` | Cron jobs, one-time jobs |
371
+ | Permissions | `sm.permissions` | Roles, permissions, RBAC |
372
+ | Teams | `sm.teams` | Team management, invitations, SSO |
373
+ | Accounts | `sm.accounts` | Client/application management |
374
+ | Identity | `sm.identity` | API key management |
375
+ | Cache | `sm.cache` | Redis key-value cache |
376
+ | Queue | `sm.queue` | Async job processing |
377
+ | Search | `sm.search` | Full-text search |
378
+ | Webhooks | `sm.webhooks` | Webhook management |
379
+ | Graph | `sm.graph` | Graph database (nodes, edges, traversal) |
380
+ | Functions | `sm.functions` | Serverless function execution |
381
+ | Listings | `sm.listings` | Marketplace listings |
382
+ | Events | `sm.events` | Event management, registration |
383
+ | Leaderboard | `sm.leaderboard` | Gamification leaderboards |
384
+ | Photo | `sm.photo` | Image processing |
385
+ | Flag Content | `sm.flagContent` | Content moderation |
386
+ | Compliance | `sm.compliance` | GDPR, audit logs |
387
+ | Orchestrator | `sm.orchestrator` | Workflow automation |
388
+ | Logger | `sm.logger` | Centralized logging |
389
+ | Catalog | `sm.catalog` | Service registry |
390
+
391
+ ## Error Codes
392
+
393
+ | Code | HTTP | When |
394
+ |------|------|------|
395
+ | `unauthorized` | 401 | Missing or invalid auth |
396
+ | `forbidden` | 403 | Valid auth, insufficient permissions |
397
+ | `not_found` | 404 | Resource doesn't exist |
398
+ | `validation_error` | 422 | Bad input (`details` has per-field errors) |
399
+ | `rate_limited` | 429 | Too many requests (`details.retryAfter`) |
400
+ | `conflict` | 409 | Duplicate resource |
401
+ | `file_scanning` | 202 | File uploaded, scan not complete |
402
+ | `file_threat` | 403 | Malware detected |
403
+ | `internal_error` | 500 | Server error |
404
+
405
+ ## TypeScript
406
+
407
+ Full type definitions included. All service methods, request/response types, and error codes are typed.
408
+
409
+ ```ts
410
+ import type {
411
+ ScaleMuleConfig,
412
+ ApiResponse,
413
+ ApiError,
414
+ PaginatedResponse,
415
+ QueryFilter,
416
+ QuerySort,
417
+ PresignedUploadResponse,
418
+ UploadCompleteResponse,
419
+ } from '@scalemule/sdk'
420
+ ```
421
+
422
+ ## License
423
+
424
+ Proprietary - ScaleMule Inc.
@@ -0,0 +1,158 @@
1
+ // src/services/upload-resume.ts
2
+ var DB_NAME = "sm_upload_sessions_v1";
3
+ var STORE_NAME = "sessions";
4
+ var DB_VERSION = 1;
5
+ var MAX_AGE_MS = 24 * 60 * 60 * 1e3;
6
+ var UploadResumeStore = class {
7
+ constructor() {
8
+ this.db = null;
9
+ }
10
+ /** Generate a deterministic resume key from upload identity */
11
+ static async generateResumeKey(appId, userId, filename, size, lastModified) {
12
+ const raw = `${appId}:${userId}:${filename}:${size}:${lastModified ?? 0}`;
13
+ if (typeof crypto !== "undefined" && crypto.subtle) {
14
+ const buffer = new TextEncoder().encode(raw);
15
+ const hash2 = await crypto.subtle.digest("SHA-256", buffer);
16
+ return Array.from(new Uint8Array(hash2)).map((b) => b.toString(16).padStart(2, "0")).join("");
17
+ }
18
+ let hash = 0;
19
+ for (let i = 0; i < raw.length; i++) {
20
+ const chr = raw.charCodeAt(i);
21
+ hash = (hash << 5) - hash + chr;
22
+ hash |= 0;
23
+ }
24
+ return `fallback_${Math.abs(hash).toString(36)}`;
25
+ }
26
+ /** Open the IndexedDB store. No-ops if IndexedDB is unavailable. */
27
+ async open() {
28
+ if (typeof indexedDB === "undefined") return;
29
+ return new Promise((resolve, reject) => {
30
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
31
+ request.onupgradeneeded = () => {
32
+ const db = request.result;
33
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
34
+ const store = db.createObjectStore(STORE_NAME, { keyPath: "key" });
35
+ store.createIndex("updated_at", "updated_at");
36
+ }
37
+ };
38
+ request.onsuccess = () => {
39
+ this.db = request.result;
40
+ resolve();
41
+ };
42
+ request.onerror = () => {
43
+ reject(request.error);
44
+ };
45
+ });
46
+ }
47
+ /** Get a resume session by key. Returns null if not found or stale. */
48
+ async get(key) {
49
+ if (!this.db) return null;
50
+ return new Promise((resolve) => {
51
+ const tx = this.db.transaction(STORE_NAME, "readonly");
52
+ const store = tx.objectStore(STORE_NAME);
53
+ const request = store.get(key);
54
+ request.onsuccess = () => {
55
+ const entry = request.result;
56
+ if (!entry) {
57
+ resolve(null);
58
+ return;
59
+ }
60
+ if (Date.now() - entry.updated_at > MAX_AGE_MS) {
61
+ this.remove(key).catch(() => {
62
+ });
63
+ resolve(null);
64
+ return;
65
+ }
66
+ resolve(entry.session);
67
+ };
68
+ request.onerror = () => resolve(null);
69
+ });
70
+ }
71
+ /** Save a new resume session. */
72
+ async save(key, session) {
73
+ if (!this.db) return;
74
+ return new Promise((resolve, reject) => {
75
+ const tx = this.db.transaction(STORE_NAME, "readwrite");
76
+ const store = tx.objectStore(STORE_NAME);
77
+ const entry = {
78
+ key,
79
+ session: { ...session, created_at: Date.now() },
80
+ updated_at: Date.now()
81
+ };
82
+ const request = store.put(entry);
83
+ request.onsuccess = () => resolve();
84
+ request.onerror = () => reject(request.error);
85
+ });
86
+ }
87
+ /** Update a single completed part in an existing session. */
88
+ async updatePart(key, partNumber, etag) {
89
+ if (!this.db) return;
90
+ return new Promise((resolve) => {
91
+ const tx = this.db.transaction(STORE_NAME, "readwrite");
92
+ const store = tx.objectStore(STORE_NAME);
93
+ const getRequest = store.get(key);
94
+ getRequest.onsuccess = () => {
95
+ const entry = getRequest.result;
96
+ if (!entry) {
97
+ resolve();
98
+ return;
99
+ }
100
+ const existing = entry.session.completed_parts.find((p) => p.part_number === partNumber);
101
+ if (!existing) {
102
+ entry.session.completed_parts.push({ part_number: partNumber, etag });
103
+ }
104
+ entry.updated_at = Date.now();
105
+ const putRequest = store.put(entry);
106
+ putRequest.onsuccess = () => resolve();
107
+ putRequest.onerror = () => resolve();
108
+ };
109
+ getRequest.onerror = () => resolve();
110
+ });
111
+ }
112
+ /** Remove a resume session (e.g., after successful completion). */
113
+ async remove(key) {
114
+ if (!this.db) return;
115
+ return new Promise((resolve) => {
116
+ const tx = this.db.transaction(STORE_NAME, "readwrite");
117
+ const store = tx.objectStore(STORE_NAME);
118
+ const request = store.delete(key);
119
+ request.onsuccess = () => resolve();
120
+ request.onerror = () => resolve();
121
+ });
122
+ }
123
+ /** Purge all stale entries (older than MAX_AGE_MS). */
124
+ async purgeStale() {
125
+ if (!this.db) return 0;
126
+ return new Promise((resolve) => {
127
+ const cutoff = Date.now() - MAX_AGE_MS;
128
+ const tx = this.db.transaction(STORE_NAME, "readwrite");
129
+ const store = tx.objectStore(STORE_NAME);
130
+ const index = store.index("updated_at");
131
+ const range = IDBKeyRange.upperBound(cutoff);
132
+ const request = index.openCursor(range);
133
+ let count = 0;
134
+ request.onsuccess = () => {
135
+ const cursor = request.result;
136
+ if (cursor) {
137
+ cursor.delete();
138
+ count++;
139
+ cursor.continue();
140
+ } else {
141
+ resolve(count);
142
+ }
143
+ };
144
+ request.onerror = () => resolve(0);
145
+ });
146
+ }
147
+ /** Close the database connection. */
148
+ close() {
149
+ if (this.db) {
150
+ this.db.close();
151
+ this.db = null;
152
+ }
153
+ }
154
+ };
155
+
156
+ export {
157
+ UploadResumeStore
158
+ };