@nurama/sdk 0.0.0-stage → 1.4.0
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 +202 -0
- package/NOTICE +5 -0
- package/README.md +1080 -2
- package/dist/BotClient.d.ts +66 -0
- package/dist/BotClient.d.ts.map +1 -0
- package/dist/BotClient.js +68 -0
- package/dist/BotClient.js.map +1 -0
- package/dist/NuramaClient.d.ts +448 -0
- package/dist/NuramaClient.d.ts.map +1 -0
- package/dist/NuramaClient.js +864 -0
- package/dist/NuramaClient.js.map +1 -0
- package/dist/browser/nurama-bot-sdk.js +11780 -0
- package/dist/browser/nurama-bot-sdk.min.js +1 -0
- package/dist/browser/nurama-sdk.js +11732 -0
- package/dist/browser/nurama-sdk.min.js +1 -0
- package/dist/routes/ai.d.ts +280 -0
- package/dist/routes/ai.d.ts.map +1 -0
- package/dist/routes/ai.js +173 -0
- package/dist/routes/ai.js.map +1 -0
- package/dist/routes/asset.d.ts +493 -0
- package/dist/routes/asset.d.ts.map +1 -0
- package/dist/routes/asset.js +848 -0
- package/dist/routes/asset.js.map +1 -0
- package/dist/routes/auth.d.ts +218 -0
- package/dist/routes/auth.d.ts.map +1 -0
- package/dist/routes/auth.js +454 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/blogPosts.d.ts +17 -0
- package/dist/routes/blogPosts.d.ts.map +1 -0
- package/dist/routes/blogPosts.js +29 -0
- package/dist/routes/blogPosts.js.map +1 -0
- package/dist/routes/board.d.ts +187 -0
- package/dist/routes/board.d.ts.map +1 -0
- package/dist/routes/board.js +270 -0
- package/dist/routes/board.js.map +1 -0
- package/dist/routes/bot.d.ts +147 -0
- package/dist/routes/bot.d.ts.map +1 -0
- package/dist/routes/bot.js +157 -0
- package/dist/routes/bot.js.map +1 -0
- package/dist/routes/chat.d.ts +842 -0
- package/dist/routes/chat.d.ts.map +1 -0
- package/dist/routes/chat.js +863 -0
- package/dist/routes/chat.js.map +1 -0
- package/dist/routes/chatAi.d.ts +51 -0
- package/dist/routes/chatAi.d.ts.map +1 -0
- package/dist/routes/chatAi.js +109 -0
- package/dist/routes/chatAi.js.map +1 -0
- package/dist/routes/config.d.ts +11 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +24 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/convo.d.ts +169 -0
- package/dist/routes/convo.d.ts.map +1 -0
- package/dist/routes/convo.js +284 -0
- package/dist/routes/convo.js.map +1 -0
- package/dist/routes/credits.d.ts +82 -0
- package/dist/routes/credits.d.ts.map +1 -0
- package/dist/routes/credits.js +49 -0
- package/dist/routes/credits.js.map +1 -0
- package/dist/routes/device.d.ts +74 -0
- package/dist/routes/device.d.ts.map +1 -0
- package/dist/routes/device.js +122 -0
- package/dist/routes/device.js.map +1 -0
- package/dist/routes/folder.d.ts +75 -0
- package/dist/routes/folder.d.ts.map +1 -0
- package/dist/routes/folder.js +99 -0
- package/dist/routes/folder.js.map +1 -0
- package/dist/routes/invite.d.ts +61 -0
- package/dist/routes/invite.d.ts.map +1 -0
- package/dist/routes/invite.js +86 -0
- package/dist/routes/invite.js.map +1 -0
- package/dist/routes/joinLink.d.ts +38 -0
- package/dist/routes/joinLink.d.ts.map +1 -0
- package/dist/routes/joinLink.js +81 -0
- package/dist/routes/joinLink.js.map +1 -0
- package/dist/routes/membership.d.ts +116 -0
- package/dist/routes/membership.d.ts.map +1 -0
- package/dist/routes/membership.js +183 -0
- package/dist/routes/membership.js.map +1 -0
- package/dist/routes/notification.d.ts +103 -0
- package/dist/routes/notification.d.ts.map +1 -0
- package/dist/routes/notification.js +89 -0
- package/dist/routes/notification.js.map +1 -0
- package/dist/routes/payment.d.ts +56 -0
- package/dist/routes/payment.d.ts.map +1 -0
- package/dist/routes/payment.js +78 -0
- package/dist/routes/payment.js.map +1 -0
- package/dist/routes/product.d.ts +43 -0
- package/dist/routes/product.d.ts.map +1 -0
- package/dist/routes/product.js +53 -0
- package/dist/routes/product.js.map +1 -0
- package/dist/routes/project.d.ts +821 -0
- package/dist/routes/project.d.ts.map +1 -0
- package/dist/routes/project.js +1153 -0
- package/dist/routes/project.js.map +1 -0
- package/dist/routes/public.d.ts +269 -0
- package/dist/routes/public.d.ts.map +1 -0
- package/dist/routes/public.js +412 -0
- package/dist/routes/public.js.map +1 -0
- package/dist/routes/scratch.d.ts +70 -0
- package/dist/routes/scratch.d.ts.map +1 -0
- package/dist/routes/scratch.js +67 -0
- package/dist/routes/scratch.js.map +1 -0
- package/dist/routes/settings.d.ts +102 -0
- package/dist/routes/settings.d.ts.map +1 -0
- package/dist/routes/settings.js +94 -0
- package/dist/routes/settings.js.map +1 -0
- package/dist/routes/shortlink.d.ts +79 -0
- package/dist/routes/shortlink.d.ts.map +1 -0
- package/dist/routes/shortlink.js +25 -0
- package/dist/routes/shortlink.js.map +1 -0
- package/dist/routes/socket.d.ts +108 -0
- package/dist/routes/socket.d.ts.map +1 -0
- package/dist/routes/socket.js +555 -0
- package/dist/routes/socket.js.map +1 -0
- package/dist/routes/storage.d.ts +44 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +49 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/subscription.d.ts +184 -0
- package/dist/routes/subscription.d.ts.map +1 -0
- package/dist/routes/subscription.js +219 -0
- package/dist/routes/subscription.js.map +1 -0
- package/dist/routes/supportChat.d.ts +40 -0
- package/dist/routes/supportChat.d.ts.map +1 -0
- package/dist/routes/supportChat.js +53 -0
- package/dist/routes/supportChat.js.map +1 -0
- package/dist/routes/supportTicket.d.ts +89 -0
- package/dist/routes/supportTicket.d.ts.map +1 -0
- package/dist/routes/supportTicket.js +54 -0
- package/dist/routes/supportTicket.js.map +1 -0
- package/dist/routes/tag.d.ts +72 -0
- package/dist/routes/tag.d.ts.map +1 -0
- package/dist/routes/tag.js +81 -0
- package/dist/routes/tag.js.map +1 -0
- package/dist/routes/task.d.ts +252 -0
- package/dist/routes/task.d.ts.map +1 -0
- package/dist/routes/task.js +284 -0
- package/dist/routes/task.js.map +1 -0
- package/dist/routes/taskRelation.d.ts +80 -0
- package/dist/routes/taskRelation.d.ts.map +1 -0
- package/dist/routes/taskRelation.js +71 -0
- package/dist/routes/taskRelation.js.map +1 -0
- package/dist/routes/token.d.ts +75 -0
- package/dist/routes/token.d.ts.map +1 -0
- package/dist/routes/token.js +51 -0
- package/dist/routes/token.js.map +1 -0
- package/dist/routes/user.d.ts +112 -0
- package/dist/routes/user.d.ts.map +1 -0
- package/dist/routes/user.js +151 -0
- package/dist/routes/user.js.map +1 -0
- package/dist/routes/version.d.ts +42 -0
- package/dist/routes/version.d.ts.map +1 -0
- package/dist/routes/version.js +38 -0
- package/dist/routes/version.js.map +1 -0
- package/dist/routes/webhook.d.ts +170 -0
- package/dist/routes/webhook.d.ts.map +1 -0
- package/dist/routes/webhook.js +173 -0
- package/dist/routes/webhook.js.map +1 -0
- package/dist/routes/workspace.d.ts +120 -0
- package/dist/routes/workspace.d.ts.map +1 -0
- package/dist/routes/workspace.js +199 -0
- package/dist/routes/workspace.js.map +1 -0
- package/dist/utils/uploadSessionManager.d.ts +133 -0
- package/dist/utils/uploadSessionManager.d.ts.map +1 -0
- package/dist/utils/uploadSessionManager.js +321 -0
- package/dist/utils/uploadSessionManager.js.map +1 -0
- package/dist/utils/urlParams.d.ts +35 -0
- package/dist/utils/urlParams.d.ts.map +1 -0
- package/dist/utils/urlParams.js +146 -0
- package/dist/utils/urlParams.js.map +1 -0
- package/dist/version.d.ts +15 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +12 -0
- package/dist/version.js.map +1 -0
- package/package.json +87 -3
- package/src/BotClient.ts +113 -0
- package/src/NuramaClient.ts +1193 -0
- package/src/bot-browser-entry.js +15 -0
- package/src/browser-entry.js +20 -0
- package/src/routes/ai.ts +378 -0
- package/src/routes/asset.ts +1104 -0
- package/src/routes/auth.ts +587 -0
- package/src/routes/blogPosts.ts +29 -0
- package/src/routes/board.ts +403 -0
- package/src/routes/bot.ts +257 -0
- package/src/routes/chat.ts +1292 -0
- package/src/routes/chatAi.ts +125 -0
- package/src/routes/config.ts +31 -0
- package/src/routes/convo.ts +321 -0
- package/src/routes/credits.ts +112 -0
- package/src/routes/device.ts +133 -0
- package/src/routes/folder.ts +154 -0
- package/src/routes/invite.ts +133 -0
- package/src/routes/joinLink.ts +100 -0
- package/src/routes/membership.ts +237 -0
- package/src/routes/notification.ts +166 -0
- package/src/routes/payment.ts +104 -0
- package/src/routes/product.ts +67 -0
- package/src/routes/project.ts +1528 -0
- package/src/routes/public.ts +496 -0
- package/src/routes/scratch.ts +94 -0
- package/src/routes/settings.ts +152 -0
- package/src/routes/shortlink.ts +90 -0
- package/src/routes/socket.ts +739 -0
- package/src/routes/storage.ts +83 -0
- package/src/routes/subscription.ts +307 -0
- package/src/routes/supportChat.ts +62 -0
- package/src/routes/supportTicket.ts +114 -0
- package/src/routes/tag.ts +131 -0
- package/src/routes/task.ts +431 -0
- package/src/routes/taskRelation.ts +125 -0
- package/src/routes/token.ts +113 -0
- package/src/routes/user.ts +214 -0
- package/src/routes/version.ts +62 -0
- package/src/routes/webhook.ts +295 -0
- package/src/routes/workspace.ts +223 -0
- package/src/utils/uploadSessionManager.ts +407 -0
- package/src/utils/urlParams.ts +181 -0
- package/src/version.ts +22 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import NuramaClient from '../NuramaClient.js';
|
|
2
|
+
|
|
3
|
+
// Define specific types for workspace requests/responses if desired
|
|
4
|
+
export interface CreateWorkspaceData {
|
|
5
|
+
name: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UpdateWorkspaceData {
|
|
10
|
+
name?: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
updateSlug?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface FileData {
|
|
16
|
+
name: string;
|
|
17
|
+
checksum: string;
|
|
18
|
+
sizeInMB: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface SortParams {
|
|
22
|
+
[key: string]: 1 | -1;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Defines workspace-related methods for the NuramaClient.
|
|
27
|
+
* @param {NuramaClient} client - The NuramaClient instance.
|
|
28
|
+
* @returns {object} An object containing the workspace methods.
|
|
29
|
+
*/
|
|
30
|
+
export default function createWorkspaceMethods(client: NuramaClient) {
|
|
31
|
+
// Specify return type more accurately if possible, using WorkspaceMethods interface
|
|
32
|
+
return {
|
|
33
|
+
/**
|
|
34
|
+
* Creates a new workspace.
|
|
35
|
+
* Requires authentication.
|
|
36
|
+
* @param data - Workspace data.
|
|
37
|
+
* @returns {Promise<object>} The created workspace object.
|
|
38
|
+
*/
|
|
39
|
+
async createWorkspace(data: CreateWorkspaceData): Promise<any> {
|
|
40
|
+
return client._request({
|
|
41
|
+
endpoint: '/v1/workspaces',
|
|
42
|
+
method: 'POST',
|
|
43
|
+
body: data,
|
|
44
|
+
sendJWT: true,
|
|
45
|
+
});
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Retrieves a specific workspace by its ID.
|
|
50
|
+
* Requires authentication.
|
|
51
|
+
* @param {string} workspaceId - The ID of the workspace to retrieve.
|
|
52
|
+
* @returns {Promise<object>} The workspace object.
|
|
53
|
+
*/
|
|
54
|
+
async getWorkspace(workspaceId: string): Promise<any> {
|
|
55
|
+
if (!workspaceId) throw new Error('workspaceId is required.');
|
|
56
|
+
return client._request({
|
|
57
|
+
endpoint: `/v1/workspaces/${workspaceId}`,
|
|
58
|
+
method: 'GET',
|
|
59
|
+
sendJWT: true,
|
|
60
|
+
});
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Lists all workspaces the authenticated user has access to.
|
|
65
|
+
* Requires authentication.
|
|
66
|
+
* @param {object} [sortParams] - Optional sorting. Keys are field names (`id`, `name`, `createdAt`, `updatedAt`);
|
|
67
|
+
* values are 1 for ascending or -1 for descending. Sent as `?sort[name]=1&sort[createdAt]=-1`.
|
|
68
|
+
* @returns {Promise<Array<object>>} Workspaces with the caller's `roles` and the workspace `capabilities`.
|
|
69
|
+
*/
|
|
70
|
+
async listWorkspaces(sortParams: SortParams = {}): Promise<any[]> {
|
|
71
|
+
return client._request({
|
|
72
|
+
endpoint: '/v1/workspaces',
|
|
73
|
+
method: 'GET',
|
|
74
|
+
params: Object.keys(sortParams).length > 0 ? { sort: sortParams } : undefined,
|
|
75
|
+
sendJWT: true,
|
|
76
|
+
});
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Request signed URL data to upload a workspace logo.
|
|
81
|
+
* Caller then multipart-uploads the file using the returned `signedUrlData`
|
|
82
|
+
* and calls `client.asset.completeUpload(...)` to finalize.
|
|
83
|
+
*
|
|
84
|
+
* @param {string} fileData.checksum - The MD5 checksum.
|
|
85
|
+
* @param {number} fileData.sizeInMB - The file size in MB.
|
|
86
|
+
* @returns {Promise<object>} Response containing signed URL data and workspace info.
|
|
87
|
+
*/
|
|
88
|
+
async createLogo(workspaceId: string, fileData: FileData): Promise<any> {
|
|
89
|
+
if (!workspaceId) throw new Error('workspaceId is required.');
|
|
90
|
+
return client._request({
|
|
91
|
+
endpoint: `/v1/workspaces/${workspaceId}/logo`,
|
|
92
|
+
method: 'POST',
|
|
93
|
+
body: fileData,
|
|
94
|
+
sendJWT: true,
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Request signed URL data to replace the workspace logo. Same upload
|
|
100
|
+
* shape as `createLogo`; existing logo is replaced once `completeUpload`
|
|
101
|
+
* lands.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} fileData.checksum - The MD5 checksum.
|
|
104
|
+
* @param {number} fileData.sizeInMB - The file size in MB.
|
|
105
|
+
* @returns {Promise<object>} Response containing signed URL data and workspace info.
|
|
106
|
+
*/
|
|
107
|
+
async updateLogo(workspaceId: string, fileData: FileData): Promise<any> {
|
|
108
|
+
if (!workspaceId) throw new Error('workspaceId is required.');
|
|
109
|
+
return client._request({
|
|
110
|
+
endpoint: `/v1/workspaces/${workspaceId}/logo`,
|
|
111
|
+
method: 'PUT',
|
|
112
|
+
body: fileData,
|
|
113
|
+
sendJWT: true,
|
|
114
|
+
});
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Request signed URL data to upload a workspace icon (the small square
|
|
119
|
+
* shown in sidebars and tabs — distinct from the logo).
|
|
120
|
+
*
|
|
121
|
+
* @param {string} fileData.checksum - The MD5 checksum.
|
|
122
|
+
* @param {number} fileData.sizeInMB - The file size in MB.
|
|
123
|
+
* @returns {Promise<object>} Response containing signed URL data and workspace info.
|
|
124
|
+
*/
|
|
125
|
+
async createIcon(workspaceId: string, fileData: FileData): Promise<any> {
|
|
126
|
+
if (!workspaceId) throw new Error('workspaceId is required.');
|
|
127
|
+
return client._request({
|
|
128
|
+
endpoint: `/v1/workspaces/${workspaceId}/icon`,
|
|
129
|
+
method: 'POST',
|
|
130
|
+
body: fileData,
|
|
131
|
+
sendJWT: true,
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Request signed URL data to replace the workspace icon. Same upload
|
|
137
|
+
* shape as `createIcon`.
|
|
138
|
+
*
|
|
139
|
+
* @param {string} fileData.checksum - The MD5 checksum.
|
|
140
|
+
* @param {number} fileData.sizeInMB - The file size in MB.
|
|
141
|
+
* @returns {Promise<object>} Response containing signed URL data and workspace info.
|
|
142
|
+
*/
|
|
143
|
+
async updateIcon(workspaceId: string, fileData: FileData): Promise<any> {
|
|
144
|
+
if (!workspaceId) throw new Error('workspaceId is required.');
|
|
145
|
+
return client._request({
|
|
146
|
+
endpoint: `/v1/workspaces/${workspaceId}/icon`,
|
|
147
|
+
method: 'PUT',
|
|
148
|
+
body: fileData,
|
|
149
|
+
sendJWT: true,
|
|
150
|
+
});
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* List the projects inside a workspace that the calling user has
|
|
155
|
+
* access to.
|
|
156
|
+
*
|
|
157
|
+
* @param {string} workspaceId - The ID of the workspace.
|
|
158
|
+
* @returns {Promise<Array<object>>} An array of project objects.
|
|
159
|
+
*/
|
|
160
|
+
async listProjects(workspaceId: string): Promise<any[]> {
|
|
161
|
+
if (!workspaceId) throw new Error('workspaceId is required.');
|
|
162
|
+
return client._request({
|
|
163
|
+
endpoint: `/v1/workspaces/${workspaceId}/projects`,
|
|
164
|
+
method: 'GET',
|
|
165
|
+
sendJWT: true,
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Update workspace metadata (name, description, etc.).
|
|
171
|
+
*
|
|
172
|
+
* @param {boolean} [updateData.updateSlug] - Whether to update the slug based on the new name.
|
|
173
|
+
* @returns {Promise<object>} The updated workspace object.
|
|
174
|
+
*/
|
|
175
|
+
async updateWorkspace(workspaceId: string, updateData: UpdateWorkspaceData): Promise<any> {
|
|
176
|
+
if (!workspaceId) throw new Error('workspaceId is required.');
|
|
177
|
+
return client._request({
|
|
178
|
+
endpoint: `/v1/workspaces/${workspaceId}`,
|
|
179
|
+
method: 'PUT',
|
|
180
|
+
body: updateData,
|
|
181
|
+
sendJWT: true,
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Soft-delete a workspace. All nested resources (projects, chats,
|
|
187
|
+
* assets) become inaccessible; bytes are reclaimed by the cleanup
|
|
188
|
+
* cron.
|
|
189
|
+
*
|
|
190
|
+
* @param {string} workspaceId - The ID of the workspace to delete.
|
|
191
|
+
* @returns {Promise<object>} Object confirming the status change.
|
|
192
|
+
*/
|
|
193
|
+
async deleteWorkspace(workspaceId: string): Promise<any> {
|
|
194
|
+
if (!workspaceId) throw new Error('workspaceId is required.');
|
|
195
|
+
return client._request({
|
|
196
|
+
endpoint: `/v1/workspaces/${workspaceId}`,
|
|
197
|
+
method: 'DELETE',
|
|
198
|
+
sendJWT: true,
|
|
199
|
+
});
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Toggle a single workspace boolean setting (e.g. an `enableX` feature
|
|
204
|
+
* flag).
|
|
205
|
+
*
|
|
206
|
+
* @param {string} workspaceId - The ID of the workspace.
|
|
207
|
+
* @param {string} settingName - The name of the setting.
|
|
208
|
+
* @param {boolean} value - The new value for the setting.
|
|
209
|
+
* @returns {Promise<object>} The updated workspace object with new settings.
|
|
210
|
+
*/
|
|
211
|
+
async updateSetting(workspaceId: string, settingName: string, value: boolean): Promise<any> {
|
|
212
|
+
if (!workspaceId) throw new Error('workspaceId is required.');
|
|
213
|
+
if (!settingName) throw new Error('settingName is required.');
|
|
214
|
+
return client._request({
|
|
215
|
+
endpoint: `/v1/workspaces/${workspaceId}/settings/${settingName}`,
|
|
216
|
+
method: 'PUT',
|
|
217
|
+
body: { value },
|
|
218
|
+
sendJWT: true,
|
|
219
|
+
});
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
};
|
|
223
|
+
}
|
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Upload Session Manager for Nurama SDK
|
|
3
|
+
*
|
|
4
|
+
* Provides upload progress persistence and cross-tab synchronization
|
|
5
|
+
* - Persists upload progress to localStorage with project isolation
|
|
6
|
+
* - Syncs upload state across multiple browser tabs
|
|
7
|
+
* - Automatic cleanup of completed/failed uploads
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export interface UploadSessionData {
|
|
11
|
+
sessionId: string;
|
|
12
|
+
projectId: string;
|
|
13
|
+
uploadId: string; // S3 multipart upload ID
|
|
14
|
+
key: string; // S3 object key
|
|
15
|
+
assetId?: string; // Backend asset ID (once available)
|
|
16
|
+
|
|
17
|
+
// Progress tracking
|
|
18
|
+
progress: number; // Overall progress (0-100)
|
|
19
|
+
partNumber: number; // Current part being uploaded
|
|
20
|
+
totalParts: number; // Total number of parts
|
|
21
|
+
completedParts: number[]; // Array of completed part numbers
|
|
22
|
+
|
|
23
|
+
// Metadata
|
|
24
|
+
fileName: string;
|
|
25
|
+
fileSize: number;
|
|
26
|
+
timestamp: number; // Last update timestamp
|
|
27
|
+
status: 'uploading' | 'completing' | 'completed' | 'failed' | 'cancelled';
|
|
28
|
+
|
|
29
|
+
// Optional error information
|
|
30
|
+
error?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface CrossTabMessage {
|
|
34
|
+
type: 'upload_progress_update' | 'upload_status_change' | 'upload_cleanup';
|
|
35
|
+
projectId: string;
|
|
36
|
+
sessionId: string;
|
|
37
|
+
data?: Partial<UploadSessionData>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export class UploadSessionManager {
|
|
41
|
+
private static instance: UploadSessionManager;
|
|
42
|
+
private messageListeners: Map<string, (message: CrossTabMessage) => void> = new Map();
|
|
43
|
+
private readonly STORAGE_PREFIX = 'nurama_upload_session_';
|
|
44
|
+
private readonly MESSAGE_CHANNEL = 'nurama_upload_sync';
|
|
45
|
+
private broadcastChannel?: BroadcastChannel;
|
|
46
|
+
|
|
47
|
+
constructor() {
|
|
48
|
+
this.initializeCrossTabSync();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public static getInstance(): UploadSessionManager {
|
|
52
|
+
if (!UploadSessionManager.instance) {
|
|
53
|
+
UploadSessionManager.instance = new UploadSessionManager();
|
|
54
|
+
}
|
|
55
|
+
return UploadSessionManager.instance;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Initialize cross-tab synchronization using BroadcastChannel
|
|
60
|
+
*/
|
|
61
|
+
private initializeCrossTabSync(): void {
|
|
62
|
+
if (typeof window !== 'undefined' && 'BroadcastChannel' in window) {
|
|
63
|
+
try {
|
|
64
|
+
this.broadcastChannel = new BroadcastChannel(this.MESSAGE_CHANNEL);
|
|
65
|
+
this.broadcastChannel.addEventListener('message', this.handleCrossTabMessage.bind(this));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.warn('[UploadSessionManager] BroadcastChannel not available:', error);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Handle cross-tab messages
|
|
74
|
+
*/
|
|
75
|
+
private handleCrossTabMessage(event: MessageEvent<CrossTabMessage>): void {
|
|
76
|
+
const message = event.data;
|
|
77
|
+
if (message && message.type && message.projectId && message.sessionId) {
|
|
78
|
+
// Notify registered listeners
|
|
79
|
+
this.messageListeners.forEach(listener => {
|
|
80
|
+
try {
|
|
81
|
+
listener(message);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error('[UploadSessionManager] Error in message listener:', error);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Generate storage key for upload session
|
|
91
|
+
*/
|
|
92
|
+
private getStorageKey(projectId: string, sessionId: string): string {
|
|
93
|
+
return `${this.STORAGE_PREFIX}${projectId}_${sessionId}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a new upload session
|
|
98
|
+
*/
|
|
99
|
+
public createSession(
|
|
100
|
+
projectId: string,
|
|
101
|
+
sessionId: string,
|
|
102
|
+
uploadData: {
|
|
103
|
+
uploadId: string;
|
|
104
|
+
key: string;
|
|
105
|
+
fileName: string;
|
|
106
|
+
fileSize: number;
|
|
107
|
+
totalParts: number;
|
|
108
|
+
assetId?: string;
|
|
109
|
+
}
|
|
110
|
+
): UploadSessionData {
|
|
111
|
+
const session: UploadSessionData = {
|
|
112
|
+
sessionId,
|
|
113
|
+
projectId,
|
|
114
|
+
uploadId: uploadData.uploadId,
|
|
115
|
+
key: uploadData.key,
|
|
116
|
+
assetId: uploadData.assetId,
|
|
117
|
+
progress: 0,
|
|
118
|
+
partNumber: 1,
|
|
119
|
+
totalParts: uploadData.totalParts,
|
|
120
|
+
completedParts: [],
|
|
121
|
+
fileName: uploadData.fileName,
|
|
122
|
+
fileSize: uploadData.fileSize,
|
|
123
|
+
timestamp: Date.now(),
|
|
124
|
+
status: 'uploading'
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
this.saveSession(session);
|
|
128
|
+
this.broadcastMessage('upload_status_change', projectId, sessionId, session);
|
|
129
|
+
|
|
130
|
+
return session;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Update upload progress for a session
|
|
135
|
+
*/
|
|
136
|
+
public updateProgress(
|
|
137
|
+
projectId: string,
|
|
138
|
+
sessionId: string,
|
|
139
|
+
progress: {
|
|
140
|
+
progress: number;
|
|
141
|
+
partNumber: number;
|
|
142
|
+
completedParts?: number[];
|
|
143
|
+
}
|
|
144
|
+
): void {
|
|
145
|
+
const session = this.getSession(projectId, sessionId);
|
|
146
|
+
if (!session) {
|
|
147
|
+
console.warn(`[UploadSessionManager] Session not found: ${sessionId}`);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const updatedSession: UploadSessionData = {
|
|
152
|
+
...session,
|
|
153
|
+
progress: progress.progress,
|
|
154
|
+
partNumber: progress.partNumber,
|
|
155
|
+
completedParts: progress.completedParts || session.completedParts,
|
|
156
|
+
timestamp: Date.now()
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
this.saveSession(updatedSession);
|
|
160
|
+
this.broadcastMessage('upload_progress_update', projectId, sessionId, {
|
|
161
|
+
progress: progress.progress,
|
|
162
|
+
partNumber: progress.partNumber,
|
|
163
|
+
completedParts: progress.completedParts,
|
|
164
|
+
timestamp: updatedSession.timestamp
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Update upload status
|
|
170
|
+
*/
|
|
171
|
+
public updateStatus(
|
|
172
|
+
projectId: string,
|
|
173
|
+
sessionId: string,
|
|
174
|
+
status: UploadSessionData['status'],
|
|
175
|
+
error?: string
|
|
176
|
+
): void {
|
|
177
|
+
const session = this.getSession(projectId, sessionId);
|
|
178
|
+
if (!session) {
|
|
179
|
+
console.warn(`[UploadSessionManager] Session not found: ${sessionId}`);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const updatedSession: UploadSessionData = {
|
|
184
|
+
...session,
|
|
185
|
+
status,
|
|
186
|
+
error,
|
|
187
|
+
timestamp: Date.now()
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
this.saveSession(updatedSession);
|
|
191
|
+
this.broadcastMessage('upload_status_change', projectId, sessionId, {
|
|
192
|
+
status,
|
|
193
|
+
error,
|
|
194
|
+
timestamp: updatedSession.timestamp
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Get upload session data
|
|
200
|
+
*/
|
|
201
|
+
public getSession(projectId: string, sessionId: string): UploadSessionData | null {
|
|
202
|
+
if (typeof window === 'undefined') return null;
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const key = this.getStorageKey(projectId, sessionId);
|
|
206
|
+
const data = localStorage.getItem(key);
|
|
207
|
+
if (data) {
|
|
208
|
+
const session = JSON.parse(data) as UploadSessionData;
|
|
209
|
+
// Validate session data
|
|
210
|
+
if (session.projectId === projectId && session.sessionId === sessionId) {
|
|
211
|
+
return session;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.warn(`[UploadSessionManager] Failed to get session ${sessionId}:`, error);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get all upload sessions for a project
|
|
223
|
+
*/
|
|
224
|
+
public getProjectSessions(projectId: string): UploadSessionData[] {
|
|
225
|
+
if (typeof window === 'undefined') return [];
|
|
226
|
+
|
|
227
|
+
const sessions: UploadSessionData[] = [];
|
|
228
|
+
const prefix = `${this.STORAGE_PREFIX}${projectId}_`;
|
|
229
|
+
|
|
230
|
+
try {
|
|
231
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
232
|
+
const key = localStorage.key(i);
|
|
233
|
+
if (key && key.startsWith(prefix)) {
|
|
234
|
+
const data = localStorage.getItem(key);
|
|
235
|
+
if (data) {
|
|
236
|
+
try {
|
|
237
|
+
const session = JSON.parse(data) as UploadSessionData;
|
|
238
|
+
if (session.projectId === projectId) {
|
|
239
|
+
sessions.push(session);
|
|
240
|
+
}
|
|
241
|
+
} catch (error) {
|
|
242
|
+
console.warn(`[UploadSessionManager] Invalid session data in ${key}:`, error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
} catch (error) {
|
|
248
|
+
console.warn('[UploadSessionManager] Failed to get project sessions:', error);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Sort by timestamp (most recent first)
|
|
252
|
+
return sessions.sort((a, b) => b.timestamp - a.timestamp);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Get every upload session across ALL projects (scans the whole localStorage
|
|
257
|
+
* prefix, not a single project). Used by app-level guards that need a global
|
|
258
|
+
* "is anything uploading right now" answer without knowing the project id.
|
|
259
|
+
*/
|
|
260
|
+
public getAllSessions(): UploadSessionData[] {
|
|
261
|
+
if (typeof window === 'undefined') return [];
|
|
262
|
+
|
|
263
|
+
const sessions: UploadSessionData[] = [];
|
|
264
|
+
try {
|
|
265
|
+
for (let i = 0; i < localStorage.length; i++) {
|
|
266
|
+
const key = localStorage.key(i);
|
|
267
|
+
if (key && key.startsWith(this.STORAGE_PREFIX)) {
|
|
268
|
+
const data = localStorage.getItem(key);
|
|
269
|
+
if (data) {
|
|
270
|
+
try {
|
|
271
|
+
sessions.push(JSON.parse(data) as UploadSessionData);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
console.warn(`[UploadSessionManager] Invalid session data in ${key}:`, error);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
} catch (error) {
|
|
279
|
+
console.warn('[UploadSessionManager] Failed to enumerate sessions:', error);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return sessions.sort((a, b) => b.timestamp - a.timestamp);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* True when at least one upload is genuinely in flight anywhere in the app.
|
|
287
|
+
*
|
|
288
|
+
* "In flight" = status `uploading`/`completing` AND updated within `staleMs`.
|
|
289
|
+
* The freshness window matters: a tab that crashed mid-upload leaves an
|
|
290
|
+
* `uploading` row in localStorage forever (it never reaches a terminal
|
|
291
|
+
* status). Progress ticks bump `timestamp` on every part, so a real upload
|
|
292
|
+
* stays fresh; a dead one goes stale and stops counting — otherwise a stale
|
|
293
|
+
* row would block app refreshes/navigation indefinitely.
|
|
294
|
+
*
|
|
295
|
+
* @param staleMs How long since the last progress update still counts as
|
|
296
|
+
* active. Default 2 minutes — comfortably longer than a single part PUT on
|
|
297
|
+
* a slow connection, short enough that a crashed upload clears quickly.
|
|
298
|
+
*/
|
|
299
|
+
public hasActiveUploads(staleMs: number = 2 * 60 * 1000): boolean {
|
|
300
|
+
const cutoff = Date.now() - staleMs;
|
|
301
|
+
return this.getAllSessions().some(
|
|
302
|
+
(s) =>
|
|
303
|
+
(s.status === 'uploading' || s.status === 'completing') &&
|
|
304
|
+
s.timestamp >= cutoff
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* Save session to localStorage
|
|
310
|
+
*/
|
|
311
|
+
private saveSession(session: UploadSessionData): void {
|
|
312
|
+
if (typeof window === 'undefined') return;
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
const key = this.getStorageKey(session.projectId, session.sessionId);
|
|
316
|
+
localStorage.setItem(key, JSON.stringify(session));
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.warn(`[UploadSessionManager] Failed to save session ${session.sessionId}:`, error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Remove upload session
|
|
324
|
+
*/
|
|
325
|
+
public removeSession(projectId: string, sessionId: string): void {
|
|
326
|
+
if (typeof window === 'undefined') return;
|
|
327
|
+
|
|
328
|
+
try {
|
|
329
|
+
const key = this.getStorageKey(projectId, sessionId);
|
|
330
|
+
localStorage.removeItem(key);
|
|
331
|
+
this.broadcastMessage('upload_cleanup', projectId, sessionId);
|
|
332
|
+
} catch (error) {
|
|
333
|
+
console.warn(`[UploadSessionManager] Failed to remove session ${sessionId}:`, error);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Clean up old or completed sessions
|
|
339
|
+
*/
|
|
340
|
+
public cleanupSessions(projectId: string, olderThanMs: number = 24 * 60 * 60 * 1000): void {
|
|
341
|
+
const sessions = this.getProjectSessions(projectId);
|
|
342
|
+
const cutoff = Date.now() - olderThanMs;
|
|
343
|
+
|
|
344
|
+
sessions.forEach(session => {
|
|
345
|
+
// Remove old sessions or completed/failed/cancelled sessions older than 1 hour
|
|
346
|
+
const shouldRemove = session.timestamp < cutoff ||
|
|
347
|
+
(['completed', 'failed', 'cancelled'].includes(session.status) &&
|
|
348
|
+
session.timestamp < (Date.now() - 60 * 60 * 1000));
|
|
349
|
+
|
|
350
|
+
if (shouldRemove) {
|
|
351
|
+
this.removeSession(projectId, session.sessionId);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Register a listener for cross-tab messages
|
|
358
|
+
*/
|
|
359
|
+
public onMessage(listenerId: string, callback: (message: CrossTabMessage) => void): void {
|
|
360
|
+
this.messageListeners.set(listenerId, callback);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Unregister a message listener
|
|
365
|
+
*/
|
|
366
|
+
public offMessage(listenerId: string): void {
|
|
367
|
+
this.messageListeners.delete(listenerId);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* Broadcast a message to other tabs
|
|
372
|
+
*/
|
|
373
|
+
private broadcastMessage(
|
|
374
|
+
type: CrossTabMessage['type'],
|
|
375
|
+
projectId: string,
|
|
376
|
+
sessionId: string,
|
|
377
|
+
data?: Partial<UploadSessionData>
|
|
378
|
+
): void {
|
|
379
|
+
if (this.broadcastChannel) {
|
|
380
|
+
try {
|
|
381
|
+
const message: CrossTabMessage = {
|
|
382
|
+
type,
|
|
383
|
+
projectId,
|
|
384
|
+
sessionId,
|
|
385
|
+
data
|
|
386
|
+
};
|
|
387
|
+
this.broadcastChannel.postMessage(message);
|
|
388
|
+
} catch (error) {
|
|
389
|
+
console.warn('[UploadSessionManager] Failed to broadcast message:', error);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Cleanup resources
|
|
396
|
+
*/
|
|
397
|
+
public destroy(): void {
|
|
398
|
+
if (this.broadcastChannel) {
|
|
399
|
+
this.broadcastChannel.close();
|
|
400
|
+
this.broadcastChannel = undefined;
|
|
401
|
+
}
|
|
402
|
+
this.messageListeners.clear();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// Export a singleton instance for convenience
|
|
407
|
+
export const uploadSessionManager = UploadSessionManager.getInstance();
|