@nurama/sdk 0.0.0-stage → 1.4.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.
Files changed (225) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +5 -0
  3. package/README.md +1080 -2
  4. package/dist/BotClient.d.ts +66 -0
  5. package/dist/BotClient.d.ts.map +1 -0
  6. package/dist/BotClient.js +68 -0
  7. package/dist/BotClient.js.map +1 -0
  8. package/dist/NuramaClient.d.ts +480 -0
  9. package/dist/NuramaClient.d.ts.map +1 -0
  10. package/dist/NuramaClient.js +902 -0
  11. package/dist/NuramaClient.js.map +1 -0
  12. package/dist/browser/nurama-bot-sdk.js +12051 -0
  13. package/dist/browser/nurama-bot-sdk.min.js +1 -0
  14. package/dist/browser/nurama-sdk.js +12003 -0
  15. package/dist/browser/nurama-sdk.min.js +1 -0
  16. package/dist/routes/ai.d.ts +280 -0
  17. package/dist/routes/ai.d.ts.map +1 -0
  18. package/dist/routes/ai.js +173 -0
  19. package/dist/routes/ai.js.map +1 -0
  20. package/dist/routes/asset.d.ts +493 -0
  21. package/dist/routes/asset.d.ts.map +1 -0
  22. package/dist/routes/asset.js +848 -0
  23. package/dist/routes/asset.js.map +1 -0
  24. package/dist/routes/auth.d.ts +218 -0
  25. package/dist/routes/auth.d.ts.map +1 -0
  26. package/dist/routes/auth.js +454 -0
  27. package/dist/routes/auth.js.map +1 -0
  28. package/dist/routes/blogPosts.d.ts +17 -0
  29. package/dist/routes/blogPosts.d.ts.map +1 -0
  30. package/dist/routes/blogPosts.js +29 -0
  31. package/dist/routes/blogPosts.js.map +1 -0
  32. package/dist/routes/board.d.ts +187 -0
  33. package/dist/routes/board.d.ts.map +1 -0
  34. package/dist/routes/board.js +270 -0
  35. package/dist/routes/board.js.map +1 -0
  36. package/dist/routes/bot.d.ts +202 -0
  37. package/dist/routes/bot.d.ts.map +1 -0
  38. package/dist/routes/bot.js +229 -0
  39. package/dist/routes/bot.js.map +1 -0
  40. package/dist/routes/chat.d.ts +842 -0
  41. package/dist/routes/chat.d.ts.map +1 -0
  42. package/dist/routes/chat.js +863 -0
  43. package/dist/routes/chat.js.map +1 -0
  44. package/dist/routes/chatAi.d.ts +51 -0
  45. package/dist/routes/chatAi.d.ts.map +1 -0
  46. package/dist/routes/chatAi.js +109 -0
  47. package/dist/routes/chatAi.js.map +1 -0
  48. package/dist/routes/config.d.ts +11 -0
  49. package/dist/routes/config.d.ts.map +1 -0
  50. package/dist/routes/config.js +24 -0
  51. package/dist/routes/config.js.map +1 -0
  52. package/dist/routes/convo.d.ts +169 -0
  53. package/dist/routes/convo.d.ts.map +1 -0
  54. package/dist/routes/convo.js +284 -0
  55. package/dist/routes/convo.js.map +1 -0
  56. package/dist/routes/credits.d.ts +82 -0
  57. package/dist/routes/credits.d.ts.map +1 -0
  58. package/dist/routes/credits.js +49 -0
  59. package/dist/routes/credits.js.map +1 -0
  60. package/dist/routes/device.d.ts +74 -0
  61. package/dist/routes/device.d.ts.map +1 -0
  62. package/dist/routes/device.js +122 -0
  63. package/dist/routes/device.js.map +1 -0
  64. package/dist/routes/folder.d.ts +75 -0
  65. package/dist/routes/folder.d.ts.map +1 -0
  66. package/dist/routes/folder.js +99 -0
  67. package/dist/routes/folder.js.map +1 -0
  68. package/dist/routes/invite.d.ts +61 -0
  69. package/dist/routes/invite.d.ts.map +1 -0
  70. package/dist/routes/invite.js +86 -0
  71. package/dist/routes/invite.js.map +1 -0
  72. package/dist/routes/joinLink.d.ts +88 -0
  73. package/dist/routes/joinLink.d.ts.map +1 -0
  74. package/dist/routes/joinLink.js +205 -0
  75. package/dist/routes/joinLink.js.map +1 -0
  76. package/dist/routes/membership.d.ts +116 -0
  77. package/dist/routes/membership.d.ts.map +1 -0
  78. package/dist/routes/membership.js +183 -0
  79. package/dist/routes/membership.js.map +1 -0
  80. package/dist/routes/notification.d.ts +103 -0
  81. package/dist/routes/notification.d.ts.map +1 -0
  82. package/dist/routes/notification.js +89 -0
  83. package/dist/routes/notification.js.map +1 -0
  84. package/dist/routes/oauthGrant.d.ts +45 -0
  85. package/dist/routes/oauthGrant.d.ts.map +1 -0
  86. package/dist/routes/oauthGrant.js +32 -0
  87. package/dist/routes/oauthGrant.js.map +1 -0
  88. package/dist/routes/payment.d.ts +56 -0
  89. package/dist/routes/payment.d.ts.map +1 -0
  90. package/dist/routes/payment.js +78 -0
  91. package/dist/routes/payment.js.map +1 -0
  92. package/dist/routes/product.d.ts +43 -0
  93. package/dist/routes/product.d.ts.map +1 -0
  94. package/dist/routes/product.js +53 -0
  95. package/dist/routes/product.js.map +1 -0
  96. package/dist/routes/project.d.ts +821 -0
  97. package/dist/routes/project.d.ts.map +1 -0
  98. package/dist/routes/project.js +1153 -0
  99. package/dist/routes/project.js.map +1 -0
  100. package/dist/routes/public.d.ts +269 -0
  101. package/dist/routes/public.d.ts.map +1 -0
  102. package/dist/routes/public.js +412 -0
  103. package/dist/routes/public.js.map +1 -0
  104. package/dist/routes/scratch.d.ts +70 -0
  105. package/dist/routes/scratch.d.ts.map +1 -0
  106. package/dist/routes/scratch.js +67 -0
  107. package/dist/routes/scratch.js.map +1 -0
  108. package/dist/routes/settings.d.ts +102 -0
  109. package/dist/routes/settings.d.ts.map +1 -0
  110. package/dist/routes/settings.js +94 -0
  111. package/dist/routes/settings.js.map +1 -0
  112. package/dist/routes/shortlink.d.ts +79 -0
  113. package/dist/routes/shortlink.d.ts.map +1 -0
  114. package/dist/routes/shortlink.js +25 -0
  115. package/dist/routes/shortlink.js.map +1 -0
  116. package/dist/routes/socket.d.ts +108 -0
  117. package/dist/routes/socket.d.ts.map +1 -0
  118. package/dist/routes/socket.js +573 -0
  119. package/dist/routes/socket.js.map +1 -0
  120. package/dist/routes/storage.d.ts +44 -0
  121. package/dist/routes/storage.d.ts.map +1 -0
  122. package/dist/routes/storage.js +49 -0
  123. package/dist/routes/storage.js.map +1 -0
  124. package/dist/routes/subscription.d.ts +184 -0
  125. package/dist/routes/subscription.d.ts.map +1 -0
  126. package/dist/routes/subscription.js +219 -0
  127. package/dist/routes/subscription.js.map +1 -0
  128. package/dist/routes/supportChat.d.ts +40 -0
  129. package/dist/routes/supportChat.d.ts.map +1 -0
  130. package/dist/routes/supportChat.js +53 -0
  131. package/dist/routes/supportChat.js.map +1 -0
  132. package/dist/routes/supportTicket.d.ts +89 -0
  133. package/dist/routes/supportTicket.d.ts.map +1 -0
  134. package/dist/routes/supportTicket.js +54 -0
  135. package/dist/routes/supportTicket.js.map +1 -0
  136. package/dist/routes/tag.d.ts +72 -0
  137. package/dist/routes/tag.d.ts.map +1 -0
  138. package/dist/routes/tag.js +81 -0
  139. package/dist/routes/tag.js.map +1 -0
  140. package/dist/routes/task.d.ts +252 -0
  141. package/dist/routes/task.d.ts.map +1 -0
  142. package/dist/routes/task.js +284 -0
  143. package/dist/routes/task.js.map +1 -0
  144. package/dist/routes/taskRelation.d.ts +80 -0
  145. package/dist/routes/taskRelation.d.ts.map +1 -0
  146. package/dist/routes/taskRelation.js +71 -0
  147. package/dist/routes/taskRelation.js.map +1 -0
  148. package/dist/routes/token.d.ts +97 -0
  149. package/dist/routes/token.d.ts.map +1 -0
  150. package/dist/routes/token.js +73 -0
  151. package/dist/routes/token.js.map +1 -0
  152. package/dist/routes/user.d.ts +112 -0
  153. package/dist/routes/user.d.ts.map +1 -0
  154. package/dist/routes/user.js +151 -0
  155. package/dist/routes/user.js.map +1 -0
  156. package/dist/routes/version.d.ts +42 -0
  157. package/dist/routes/version.d.ts.map +1 -0
  158. package/dist/routes/version.js +38 -0
  159. package/dist/routes/version.js.map +1 -0
  160. package/dist/routes/webhook.d.ts +170 -0
  161. package/dist/routes/webhook.d.ts.map +1 -0
  162. package/dist/routes/webhook.js +173 -0
  163. package/dist/routes/webhook.js.map +1 -0
  164. package/dist/routes/workspace.d.ts +120 -0
  165. package/dist/routes/workspace.d.ts.map +1 -0
  166. package/dist/routes/workspace.js +199 -0
  167. package/dist/routes/workspace.js.map +1 -0
  168. package/dist/utils/uploadSessionManager.d.ts +133 -0
  169. package/dist/utils/uploadSessionManager.d.ts.map +1 -0
  170. package/dist/utils/uploadSessionManager.js +321 -0
  171. package/dist/utils/uploadSessionManager.js.map +1 -0
  172. package/dist/utils/urlParams.d.ts +35 -0
  173. package/dist/utils/urlParams.d.ts.map +1 -0
  174. package/dist/utils/urlParams.js +146 -0
  175. package/dist/utils/urlParams.js.map +1 -0
  176. package/dist/version.d.ts +15 -0
  177. package/dist/version.d.ts.map +1 -0
  178. package/dist/version.js +12 -0
  179. package/dist/version.js.map +1 -0
  180. package/package.json +87 -3
  181. package/src/BotClient.ts +113 -0
  182. package/src/NuramaClient.ts +1253 -0
  183. package/src/bot-browser-entry.js +15 -0
  184. package/src/browser-entry.js +20 -0
  185. package/src/routes/ai.ts +378 -0
  186. package/src/routes/asset.ts +1104 -0
  187. package/src/routes/auth.ts +587 -0
  188. package/src/routes/blogPosts.ts +29 -0
  189. package/src/routes/board.ts +403 -0
  190. package/src/routes/bot.ts +356 -0
  191. package/src/routes/chat.ts +1292 -0
  192. package/src/routes/chatAi.ts +125 -0
  193. package/src/routes/config.ts +31 -0
  194. package/src/routes/convo.ts +321 -0
  195. package/src/routes/credits.ts +112 -0
  196. package/src/routes/device.ts +133 -0
  197. package/src/routes/folder.ts +154 -0
  198. package/src/routes/invite.ts +133 -0
  199. package/src/routes/joinLink.ts +233 -0
  200. package/src/routes/membership.ts +237 -0
  201. package/src/routes/notification.ts +166 -0
  202. package/src/routes/oauthGrant.ts +64 -0
  203. package/src/routes/payment.ts +104 -0
  204. package/src/routes/product.ts +67 -0
  205. package/src/routes/project.ts +1528 -0
  206. package/src/routes/public.ts +496 -0
  207. package/src/routes/scratch.ts +94 -0
  208. package/src/routes/settings.ts +152 -0
  209. package/src/routes/shortlink.ts +90 -0
  210. package/src/routes/socket.ts +757 -0
  211. package/src/routes/storage.ts +83 -0
  212. package/src/routes/subscription.ts +307 -0
  213. package/src/routes/supportChat.ts +62 -0
  214. package/src/routes/supportTicket.ts +114 -0
  215. package/src/routes/tag.ts +131 -0
  216. package/src/routes/task.ts +431 -0
  217. package/src/routes/taskRelation.ts +125 -0
  218. package/src/routes/token.ts +152 -0
  219. package/src/routes/user.ts +214 -0
  220. package/src/routes/version.ts +62 -0
  221. package/src/routes/webhook.ts +295 -0
  222. package/src/routes/workspace.ts +223 -0
  223. package/src/utils/uploadSessionManager.ts +407 -0
  224. package/src/utils/urlParams.ts +181 -0
  225. 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();