@antzsoft/chat-core 1.1.3 → 1.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/chunk-3QYXS3IW.js +415 -0
- package/dist/chunk-3QYXS3IW.js.map +1 -0
- package/dist/index.cjs +48 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -619
- package/dist/index.d.ts +3 -619
- package/dist/index.js +47 -396
- package/dist/index.js.map +1 -1
- package/dist/internal-CACK6gGJ.d.cts +624 -0
- package/dist/internal-CACK6gGJ.d.ts +624 -0
- package/dist/internal.cjs +209 -0
- package/dist/internal.cjs.map +1 -0
- package/dist/internal.d.cts +1 -0
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +7 -0
- package/dist/internal.js.map +1 -0
- package/docs/integration-guide.html +45 -5
- package/package.json +6 -1
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/internal.ts
|
|
31
|
+
var internal_exports = {};
|
|
32
|
+
__export(internal_exports, {
|
|
33
|
+
uploadBatchWithSlots: () => uploadBatchWithSlots
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(internal_exports);
|
|
36
|
+
|
|
37
|
+
// src/compression/compress.ts
|
|
38
|
+
var GZIP_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
39
|
+
"text/plain",
|
|
40
|
+
"text/csv",
|
|
41
|
+
"text/markdown",
|
|
42
|
+
"text/x-markdown",
|
|
43
|
+
"text/xml",
|
|
44
|
+
"application/xml",
|
|
45
|
+
"text/yaml",
|
|
46
|
+
"text/x-yaml",
|
|
47
|
+
"application/x-yaml",
|
|
48
|
+
"application/rtf",
|
|
49
|
+
"text/rtf",
|
|
50
|
+
"application/json",
|
|
51
|
+
"image/svg+xml"
|
|
52
|
+
]);
|
|
53
|
+
var IMAGE_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
54
|
+
"image/jpeg",
|
|
55
|
+
"image/png",
|
|
56
|
+
"image/gif",
|
|
57
|
+
"image/webp",
|
|
58
|
+
"image/bmp",
|
|
59
|
+
"image/tiff"
|
|
60
|
+
]);
|
|
61
|
+
var SKIP_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
62
|
+
"video/mp4",
|
|
63
|
+
"video/webm",
|
|
64
|
+
"video/quicktime",
|
|
65
|
+
"audio/mpeg",
|
|
66
|
+
"audio/wav",
|
|
67
|
+
"audio/ogg",
|
|
68
|
+
"audio/webm",
|
|
69
|
+
"audio/mp4",
|
|
70
|
+
"application/zip",
|
|
71
|
+
"application/pdf",
|
|
72
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
73
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
74
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
|
75
|
+
]);
|
|
76
|
+
function getCompressionStrategy(mimeType, config) {
|
|
77
|
+
if (SKIP_MIME_TYPES.has(mimeType)) return "skip";
|
|
78
|
+
if (IMAGE_MIME_TYPES.has(mimeType)) return "image";
|
|
79
|
+
if (config.compressDocuments && GZIP_MIME_TYPES.has(mimeType)) return "gzip";
|
|
80
|
+
return "skip";
|
|
81
|
+
}
|
|
82
|
+
async function compressFile(file, platformCompressFn, config) {
|
|
83
|
+
const noop = {
|
|
84
|
+
...file,
|
|
85
|
+
originalSize: file.size,
|
|
86
|
+
compressed: false,
|
|
87
|
+
compressionAlgorithm: "none"
|
|
88
|
+
};
|
|
89
|
+
if (!config.enabled || !platformCompressFn) return noop;
|
|
90
|
+
const strategy = getCompressionStrategy(file.type, config);
|
|
91
|
+
if (strategy === "skip") return noop;
|
|
92
|
+
try {
|
|
93
|
+
return await platformCompressFn(file, config);
|
|
94
|
+
} catch {
|
|
95
|
+
return noop;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/api/client.ts
|
|
100
|
+
var import_axios = __toESM(require("axios"), 1);
|
|
101
|
+
var _instance = null;
|
|
102
|
+
function getApiClient() {
|
|
103
|
+
if (!_instance) throw new Error("[AntzChat] API client not initialized. Call initApiClient first.");
|
|
104
|
+
return _instance;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/api/storage.ts
|
|
108
|
+
var storageApi = {
|
|
109
|
+
async requestPresignedUrl(payload) {
|
|
110
|
+
const { data } = await getApiClient().post("/storage/presigned-url", payload);
|
|
111
|
+
return data;
|
|
112
|
+
},
|
|
113
|
+
async requestPresignedUrlBatch(files) {
|
|
114
|
+
const { data } = await getApiClient().post("/storage/presigned-url/batch", { files });
|
|
115
|
+
return data;
|
|
116
|
+
},
|
|
117
|
+
async confirmUpload(fileId) {
|
|
118
|
+
const { data } = await getApiClient().post(`/storage/confirm/${fileId}`);
|
|
119
|
+
return data;
|
|
120
|
+
},
|
|
121
|
+
async getFile(fileId) {
|
|
122
|
+
const { data } = await getApiClient().get(`/storage/files/${fileId}`);
|
|
123
|
+
return data;
|
|
124
|
+
},
|
|
125
|
+
async getFileUrl(fileId, expiresIn) {
|
|
126
|
+
const { data } = await getApiClient().get(`/storage/files/${fileId}/url`, {
|
|
127
|
+
params: expiresIn ? { expiresIn } : {}
|
|
128
|
+
});
|
|
129
|
+
return data;
|
|
130
|
+
},
|
|
131
|
+
async deleteFile(fileId) {
|
|
132
|
+
await getApiClient().delete(`/storage/files/${fileId}`);
|
|
133
|
+
},
|
|
134
|
+
async getConversationFiles(conversationId, params = {}) {
|
|
135
|
+
const { data } = await getApiClient().get(
|
|
136
|
+
`/storage/conversations/${conversationId}/files`,
|
|
137
|
+
{ params }
|
|
138
|
+
);
|
|
139
|
+
return data;
|
|
140
|
+
},
|
|
141
|
+
async getMyFiles(params = {}) {
|
|
142
|
+
const { data } = await getApiClient().get("/storage/my-files", { params });
|
|
143
|
+
return data;
|
|
144
|
+
}
|
|
145
|
+
};
|
|
146
|
+
async function runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig) {
|
|
147
|
+
const compressedFiles = await Promise.all(
|
|
148
|
+
files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true }))
|
|
149
|
+
);
|
|
150
|
+
const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i] }));
|
|
151
|
+
const requests = slotted.map(({ file: f }) => ({
|
|
152
|
+
filename: f.name,
|
|
153
|
+
mimeType: f.type,
|
|
154
|
+
size: f.size,
|
|
155
|
+
conversationId,
|
|
156
|
+
...f.compressed && {
|
|
157
|
+
metadata: {
|
|
158
|
+
compressed: f.compressed,
|
|
159
|
+
originalSize: f.originalSize,
|
|
160
|
+
compressionAlgorithm: f.compressionAlgorithm
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}));
|
|
164
|
+
const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);
|
|
165
|
+
const failedSlotIds = /* @__PURE__ */ new Set();
|
|
166
|
+
const failed = requestErrors.map((e, i) => {
|
|
167
|
+
const slotId = slotted[urls.length + i]?.slotId ?? slotIds[i];
|
|
168
|
+
failedSlotIds.add(slotId);
|
|
169
|
+
return { filename: e.filename, error: e.error };
|
|
170
|
+
});
|
|
171
|
+
const uploadableSlots = slotted.filter(({ slotId }) => !failedSlotIds.has(slotId));
|
|
172
|
+
const progressMap = {};
|
|
173
|
+
const reportProgress = () => {
|
|
174
|
+
if (!onProgress) return;
|
|
175
|
+
const vals = Object.values(progressMap);
|
|
176
|
+
const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);
|
|
177
|
+
onProgress(Math.round(avg));
|
|
178
|
+
};
|
|
179
|
+
const successful = [];
|
|
180
|
+
const slotToFile = /* @__PURE__ */ new Map();
|
|
181
|
+
await Promise.all(
|
|
182
|
+
urls.map(async (presigned, idx) => {
|
|
183
|
+
const { file, slotId } = uploadableSlots[idx];
|
|
184
|
+
progressMap[idx] = 0;
|
|
185
|
+
try {
|
|
186
|
+
await platformUploadFn(presigned, file, (pct) => {
|
|
187
|
+
progressMap[idx] = Math.round(pct * 0.9);
|
|
188
|
+
reportProgress();
|
|
189
|
+
});
|
|
190
|
+
const fileResponse = await storageApi.confirmUpload(presigned.fileId);
|
|
191
|
+
progressMap[idx] = 100;
|
|
192
|
+
reportProgress();
|
|
193
|
+
successful.push(fileResponse);
|
|
194
|
+
slotToFile.set(slotId, fileResponse);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
failed.push({ filename: file.name, error: err.message });
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
);
|
|
200
|
+
return { result: { successful, failed }, slotToFile };
|
|
201
|
+
}
|
|
202
|
+
async function uploadBatchWithSlots(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig) {
|
|
203
|
+
return runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig);
|
|
204
|
+
}
|
|
205
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
206
|
+
0 && (module.exports = {
|
|
207
|
+
uploadBatchWithSlots
|
|
208
|
+
});
|
|
209
|
+
//# sourceMappingURL=internal.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/internal.ts","../src/compression/compress.ts","../src/api/client.ts","../src/api/storage.ts"],"sourcesContent":["// Internal exports for use by the web and RN SDKs only.\n// Not part of the public API — do not use in application code.\nexport { uploadBatchWithSlots } from './api/storage.js';\n","import type { UploadableFile, CompressedFile, CompressionAlgorithm } from '../types/index.js';\nimport type { PlatformCompressFn, ResolvedCompressionConfig } from '../config/types.js';\n\n// MIME types that benefit from gzip (text-based, not already compressed)\nconst GZIP_MIME_TYPES = new Set([\n 'text/plain', 'text/csv', 'text/markdown', 'text/x-markdown',\n 'text/xml', 'application/xml', 'text/yaml', 'text/x-yaml',\n 'application/x-yaml', 'application/rtf', 'text/rtf',\n 'application/json', 'image/svg+xml',\n]);\n\nconst IMAGE_MIME_TYPES = new Set([\n 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp', 'image/tiff',\n]);\n\n// Already-compressed formats — no gain from recompressing\nconst SKIP_MIME_TYPES = new Set([\n 'video/mp4', 'video/webm', 'video/quicktime',\n 'audio/mpeg', 'audio/wav', 'audio/ogg', 'audio/webm', 'audio/mp4',\n 'application/zip', 'application/pdf',\n 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n]);\n\nexport type CompressionStrategy = 'image' | 'gzip' | 'skip';\n\nexport function getCompressionStrategy(\n mimeType: string,\n config: ResolvedCompressionConfig,\n): CompressionStrategy {\n if (SKIP_MIME_TYPES.has(mimeType)) return 'skip';\n if (IMAGE_MIME_TYPES.has(mimeType)) return 'image';\n if (config.compressDocuments && GZIP_MIME_TYPES.has(mimeType)) return 'gzip';\n return 'skip';\n}\n\n/**\n * Attempt to compress a file using the platform-provided compressor.\n * Returns the original file unchanged (as a CompressedFile with compressed=false)\n * if compression is disabled, no compressor is provided, or the strategy is 'skip'.\n */\nexport async function compressFile(\n file: UploadableFile,\n platformCompressFn: PlatformCompressFn | undefined,\n config: ResolvedCompressionConfig,\n): Promise<CompressedFile> {\n const noop: CompressedFile = {\n ...file,\n originalSize: file.size,\n compressed: false,\n compressionAlgorithm: 'none' as CompressionAlgorithm,\n };\n\n if (!config.enabled || !platformCompressFn) return noop;\n\n const strategy = getCompressionStrategy(file.type, config);\n if (strategy === 'skip') return noop;\n\n try {\n return await platformCompressFn(file, config);\n } catch {\n // Compression failure is non-fatal — fall back to original\n return noop;\n }\n}\n","import axios, {\n AxiosInstance,\n InternalAxiosRequestConfig,\n} from 'axios';\nimport type { ResolvedConfig } from '../config/types.js';\nimport type { AuthTokens } from '../types/index.js';\nimport { encryptPayload, decryptPayload, isTransitEnvelope } from '../crypto/transit.js';\nimport { getSessionKey, getSessionId, isTransitEnabled, waitForTransitReady, configureTransit } from '../crypto/session.js';\n\nexport type TokenStore = {\n getAccessToken: () => string | null | undefined;\n getRefreshToken: () => string | null | undefined;\n setTokens: (tokens: AuthTokens) => void;\n clearTokens: () => void;\n};\n\nlet _tokenStore: TokenStore | null = null;\nlet _config: ResolvedConfig | null = null;\n// Avatar headers are sent only on the first authenticated request after init.\n// The server hashes on receive and deduplicates — subsequent requests don't need them.\nlet _avatarSent = false;\n\nexport function initApiClient(config: ResolvedConfig, tokenStore: TokenStore): AxiosInstance {\n _config = config;\n _tokenStore = tokenStore;\n _avatarSent = false; // reset on re-init (new session / authToken change)\n\n const client = axios.create({\n baseURL: config.apiUrl,\n headers: { 'Content-Type': 'application/json' },\n });\n\n // Configure transit as early as possible — before any requests fire —\n // so waitForTransitReady() in the interceptor knows whether to block or not.\n configureTransit(config.transitEncryption);\n\n // ── Request interceptor ──────────────────────────────────────────────────\n client.interceptors.request.use(async (req: InternalAxiosRequestConfig) => {\n const token = _tokenStore?.getAccessToken();\n if (token) req.headers['Authorization'] = `Bearer ${token}`;\n if (_config?.userId) req.headers['x-user-id'] = _config.userId;\n if (_config?.tenantId) req.headers['X-Tenant-ID'] = _config.tenantId;\n // Send avatar on the first request only — server hashes and deduplicates\n if (token && !_avatarSent && _config?.avatar) {\n if (_config.avatar.base64) req.headers['x-avatar-base64'] = _config.avatar.base64;\n else if (_config.avatar.url) req.headers['x-avatar-url'] = _config.avatar.url;\n _avatarSent = true;\n }\n\n // Wait for the transit session key to be established before sending.\n // HTTP requests that fire before connectSocket completes (e.g. getMe, getConfig)\n // are held here until the ECDH handshake finishes. Resolves immediately when\n // transit is disabled or already established.\n await waitForTransitReady();\n\n // Transit encryption — attach session ID on every request so the server\n // can look up the session key. Encrypt body when present.\n if (isTransitEnabled()) {\n const sessionId = getSessionId();\n const key = getSessionKey();\n if (sessionId && key) {\n req.headers['x-transit-session'] = sessionId;\n if (req.data !== undefined && req.data !== null) {\n const envelope = await encryptPayload(req.data, key);\n req.data = envelope;\n req.headers['x-transit-encrypted'] = '1';\n }\n }\n }\n\n return req;\n });\n\n let isRefreshing = false;\n let refreshQueue: Array<(token: string) => void> = [];\n\n // ── Response interceptor ─────────────────────────────────────────────────\n client.interceptors.response.use(\n async (response) => {\n // Transit decryption — server wraps encrypted payload inside { success, data: <envelope> }.\n // Decrypt the inner envelope, then let the standard unwrap below handle { success, data }.\n if (isTransitEnabled()) {\n const key = getSessionKey();\n if (key) {\n // Case 1: entire response is the envelope (unlikely but handle it)\n if (isTransitEnvelope(response.data)) {\n response.data = await decryptPayload(response.data, key);\n }\n // Case 2: envelope is nested inside { success, data: <envelope> }\n else if (response.data?.data && isTransitEnvelope(response.data.data)) {\n response.data.data = await decryptPayload(response.data.data, key);\n }\n }\n }\n\n // Standard { success, data } unwrap\n if (\n response.data &&\n typeof response.data === 'object' &&\n 'success' in response.data &&\n 'data' in response.data\n ) {\n response.data = response.data.data;\n }\n return response;\n },\n async (error) => {\n // Decrypt error response body — error filter encrypts it too\n if (isTransitEnabled() && error.response?.data) {\n const key = getSessionKey();\n if (key) {\n try {\n if (isTransitEnvelope(error.response.data)) {\n error.response.data = await decryptPayload(error.response.data, key);\n } else if (error.response.data?.data && isTransitEnvelope(error.response.data.data)) {\n error.response.data.data = await decryptPayload(error.response.data.data, key);\n }\n } catch { /* decryption failed — leave as-is */ }\n }\n }\n\n const original = error.config as InternalAxiosRequestConfig & { _retry?: boolean };\n\n if (error.response?.status === 401 && !original._retry) {\n const refreshToken = _tokenStore?.getRefreshToken();\n if (!refreshToken) {\n _tokenStore?.clearTokens();\n return Promise.reject(error);\n }\n\n if (isRefreshing) {\n return new Promise((resolve) => {\n refreshQueue.push((newToken) => {\n original.headers['Authorization'] = `Bearer ${newToken}`;\n resolve(client(original));\n });\n });\n }\n\n original._retry = true;\n isRefreshing = true;\n\n try {\n const { data } = await axios.post<{ data: AuthTokens }>(\n `${_config!.apiUrl}/auth/refresh`,\n { refreshToken },\n );\n const tokens: AuthTokens = (data as any).data ?? data;\n _tokenStore?.setTokens(tokens);\n refreshQueue.forEach((cb) => cb(tokens.accessToken));\n refreshQueue = [];\n original.headers['Authorization'] = `Bearer ${tokens.accessToken}`;\n return client(original);\n } catch {\n _tokenStore?.clearTokens();\n return Promise.reject(error);\n } finally {\n isRefreshing = false;\n }\n }\n\n return Promise.reject(error);\n },\n );\n\n return client;\n}\n\nlet _instance: AxiosInstance | null = null;\n\nexport function setApiClientInstance(instance: AxiosInstance) {\n _instance = instance;\n}\n\nexport function getApiClient(): AxiosInstance {\n if (!_instance) throw new Error('[AntzChat] API client not initialized. Call initApiClient first.');\n return _instance;\n}\n","import type {\n BatchUploadResult,\n FileResponse,\n PaginatedResponse,\n PresignedUrlRequest,\n PresignedUrlResponse,\n FileType,\n UploadableFile,\n} from '../types/index.js';\nimport type { PlatformUploadFn, PlatformCompressFn, ResolvedCompressionConfig } from '../config/types.js';\nimport { compressFile } from '../compression/compress.js';\nimport { getApiClient } from './client.js';\n\nexport const storageApi = {\n async requestPresignedUrl(payload: PresignedUrlRequest): Promise<PresignedUrlResponse> {\n const { data } = await getApiClient().post<PresignedUrlResponse>('/storage/presigned-url', payload);\n return data;\n },\n\n async requestPresignedUrlBatch(files: PresignedUrlRequest[]): Promise<{\n urls: PresignedUrlResponse[];\n errors: Array<{ filename: string; error: string }>;\n }> {\n const { data } = await getApiClient().post('/storage/presigned-url/batch', { files });\n return data;\n },\n\n async confirmUpload(fileId: string): Promise<FileResponse> {\n const { data } = await getApiClient().post<FileResponse>(`/storage/confirm/${fileId}`);\n return data;\n },\n\n async getFile(fileId: string): Promise<FileResponse> {\n const { data } = await getApiClient().get<FileResponse>(`/storage/files/${fileId}`);\n return data;\n },\n\n async getFileUrl(fileId: string, expiresIn?: number): Promise<{ url: string; expiresAt: string }> {\n const { data } = await getApiClient().get(`/storage/files/${fileId}/url`, {\n params: expiresIn ? { expiresIn } : {},\n });\n return data;\n },\n\n async deleteFile(fileId: string): Promise<void> {\n await getApiClient().delete(`/storage/files/${fileId}`);\n },\n\n async getConversationFiles(\n conversationId: string,\n params: { page?: number; limit?: number; type?: FileType } = {},\n ): Promise<PaginatedResponse<FileResponse>> {\n const { data } = await getApiClient().get(\n `/storage/conversations/${conversationId}/files`,\n { params },\n );\n return data;\n },\n\n async getMyFiles(params: { page?: number; limit?: number } = {}): Promise<PaginatedResponse<FileResponse>> {\n const { data } = await getApiClient().get('/storage/my-files', { params });\n return data;\n },\n};\n\n/**\n * High-level batch upload.\n * The actual binary transfer is delegated to platformUploadFn so this\n * function is platform-agnostic (works on web and React Native).\n * If platformCompressFn + compressionConfig are provided, each file is\n * compressed before the presigned URL is requested (so the server receives\n * the correct compressed size and MIME type).\n */\n/**\n * Core upload implementation. Returns the public BatchUploadResult plus a\n * slotId → FileResponse map that useChat hooks use internally to match\n * confirmed uploads back to optimistic UI slots by position rather than\n * filename. The slotToFile map is never part of the public API.\n */\nasync function runUploadBatch(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n slotIds: string[],\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n): Promise<{ result: BatchUploadResult; slotToFile: Map<string, FileResponse> }> {\n // Compress all files first (no-ops for unsupported types or when disabled)\n const compressedFiles = await Promise.all(\n files.map((f) => compressFile(f, platformCompressFn, compressionConfig ?? { enabled: false, imageQuality: 0.85, imageMaxDimension: 1920, compressDocuments: true })),\n );\n\n // Pair each compressed file with its slot ID for position-safe tracking.\n const slotted = compressedFiles.map((f, i) => ({ file: f, slotId: slotIds[i] }));\n\n const requests: PresignedUrlRequest[] = slotted.map(({ file: f }) => ({\n filename: f.name,\n mimeType: f.type,\n size: f.size,\n conversationId,\n ...(f.compressed && {\n metadata: {\n compressed: f.compressed,\n originalSize: f.originalSize,\n compressionAlgorithm: f.compressionAlgorithm,\n },\n }),\n }));\n\n const { urls, errors: requestErrors } = await storageApi.requestPresignedUrlBatch(requests);\n\n // The server returns a *shorter* urls array when some files fail presigned-URL\n // generation — it skips failures rather than leaving gaps. Build the failed set\n // by slot index (not filename) so same-named files are never misidentified.\n const failedSlotIds = new Set<string>();\n const failed: Array<{ filename: string; error: string }> = requestErrors.map((e, i) => {\n const slotId = slotted[urls.length + i]?.slotId ?? slotIds[i];\n failedSlotIds.add(slotId);\n return { filename: e.filename, error: e.error };\n });\n\n const uploadableSlots = slotted.filter(({ slotId }) => !failedSlotIds.has(slotId));\n\n const progressMap: Record<number, number> = {};\n const reportProgress = () => {\n if (!onProgress) return;\n const vals = Object.values(progressMap);\n const avg = vals.reduce((s, v) => s + v, 0) / Math.max(vals.length, 1);\n onProgress(Math.round(avg));\n };\n\n const successful: FileResponse[] = [];\n const slotToFile = new Map<string, FileResponse>();\n\n await Promise.all(\n urls.map(async (presigned, idx) => {\n const { file, slotId } = uploadableSlots[idx];\n progressMap[idx] = 0;\n try {\n await platformUploadFn(presigned, file, (pct) => {\n progressMap[idx] = Math.round(pct * 0.9);\n reportProgress();\n });\n\n const fileResponse = await storageApi.confirmUpload(presigned.fileId);\n progressMap[idx] = 100;\n reportProgress();\n successful.push(fileResponse);\n slotToFile.set(slotId, fileResponse);\n } catch (err) {\n failed.push({ filename: file.name, error: (err as Error).message });\n }\n }),\n );\n\n return { result: { successful, failed }, slotToFile };\n}\n\n/** Public API — returns standard BatchUploadResult, slot tracking is internal. */\nexport async function uploadBatch(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n): Promise<BatchUploadResult> {\n const slotIds = files.map(() => crypto.randomUUID());\n const { result } = await runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig);\n return result;\n}\n\n/**\n * Used only by useChat hooks (web + RN) to get the slotId → FileResponse map\n * for matching confirmed uploads back to optimistic UI slots.\n * Not exported from the package index — internal SDK use only.\n */\nexport async function uploadBatchWithSlots(\n files: UploadableFile[],\n platformUploadFn: PlatformUploadFn,\n slotIds: string[],\n conversationId?: string,\n onProgress?: (pct: number) => void,\n platformCompressFn?: PlatformCompressFn,\n compressionConfig?: ResolvedCompressionConfig,\n): Promise<{ result: BatchUploadResult; slotToFile: Map<string, FileResponse> }> {\n return runUploadBatch(files, platformUploadFn, slotIds, conversationId, onProgress, platformCompressFn, compressionConfig);\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { a3 as uploadBatchWithSlots } from './internal-CACK6gGJ.cjs';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { a3 as uploadBatchWithSlots } from './internal-CACK6gGJ.js';
|
package/dist/internal.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -155,7 +155,7 @@ section.sec>h2:hover{color:#fff}
|
|
|
155
155
|
|
|
156
156
|
<div class="section-label">What's New</div>
|
|
157
157
|
<ul>
|
|
158
|
-
<li><a href="#whats-new">v1.1.
|
|
158
|
+
<li><a href="#whats-new">v1.1.4 Release Notes</a></li>
|
|
159
159
|
</ul>
|
|
160
160
|
|
|
161
161
|
<div class="section-label">Getting Started</div>
|
|
@@ -242,16 +242,56 @@ section.sec>h2:hover{color:#fff}
|
|
|
242
242
|
<h2>What's New</h2>
|
|
243
243
|
<p style="color:var(--muted);font-size:13px;margin-bottom:20px">Version history and release notes. Click a version to expand.</p>
|
|
244
244
|
|
|
245
|
-
<!-- ── v1.1.
|
|
246
|
-
<div class="wn-version open" id="wn-
|
|
247
|
-
<div class="wn-header" onclick="toggleVersion('wn-
|
|
245
|
+
<!-- ── v1.1.4 (current) ── -->
|
|
246
|
+
<div class="wn-version open" id="wn-114">
|
|
247
|
+
<div class="wn-header" onclick="toggleVersion('wn-114')">
|
|
248
248
|
<div class="wn-title">
|
|
249
|
-
<span class="wn-ver">v1.1.
|
|
249
|
+
<span class="wn-ver">v1.1.4</span>
|
|
250
250
|
<span class="wn-badge current">Current</span>
|
|
251
251
|
<span class="wn-date">May 2026</span>
|
|
252
252
|
</div>
|
|
253
253
|
<span class="wn-chevron">▲</span>
|
|
254
254
|
</div>
|
|
255
|
+
<div class="wn-body">
|
|
256
|
+
|
|
257
|
+
<div class="wn-item" id="wn-114-1">
|
|
258
|
+
<div class="wn-item-header" onclick="toggleItem('wn-114-1')">
|
|
259
|
+
<span class="wn-tag fix">Fix</span>
|
|
260
|
+
<span class="wn-item-title">Socket ack timeout eliminated for rapid or large messages</span>
|
|
261
|
+
<span class="wn-chevron-sm">▾</span>
|
|
262
|
+
</div>
|
|
263
|
+
<div class="wn-item-body">
|
|
264
|
+
<p>Sending many messages in quick succession — especially long text messages or in group chats with online recipients — could trigger a <code>Socket ack timeout: send_message</code> error on the client, even though the messages were delivered successfully. The root cause was that the server was performing delivery tracking, room broadcasts, and push notifications synchronously before returning the socket acknowledgement, which could exceed the 5-second ack window.</p>
|
|
265
|
+
<p>The server now returns the ack immediately after the message is written to the database. All subsequent work (delivery status tracking, <code>new_message</code> broadcast to recipients, push notifications) runs in the background. <strong>No behaviour change is visible to users</strong> — delivery ticks, message broadcasts, and push notifications all still fire; they just no longer block the ack.</p>
|
|
266
|
+
<p>No SDK or integration changes required. This fix is a server-side change paired with a client-side queue improvement (see below).</p>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
|
|
270
|
+
<div class="wn-item" id="wn-114-2">
|
|
271
|
+
<div class="wn-item-header" onclick="toggleItem('wn-114-2')">
|
|
272
|
+
<span class="wn-tag fix">Fix</span>
|
|
273
|
+
<span class="wn-item-title">Per-conversation send queue — parallel sends across conversations</span>
|
|
274
|
+
<span class="wn-chevron-sm">▾</span>
|
|
275
|
+
</div>
|
|
276
|
+
<div class="wn-item-body">
|
|
277
|
+
<p>The SDK's internal <code>send_message</code> queue was previously shared across all conversations. This meant that sending messages in Conversation A would block outgoing messages in Conversation B until each ack completed, even though the two conversations are fully independent.</p>
|
|
278
|
+
<p>The queue is now keyed per <code>conversationId</code>. Messages within the same conversation are still serialised (guaranteeing order), but messages across different conversations are sent in parallel. The <code>QUEUE_MAX_SIZE</code> limit of 100 pending messages also now applies per conversation rather than globally.</p>
|
|
279
|
+
<p>No integration changes required. This is an internal SDK improvement in <code>@antzsoft/chat-core</code>.</p>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
|
|
283
|
+
</div>
|
|
284
|
+
</div><!-- /.wn-version -->
|
|
285
|
+
|
|
286
|
+
<!-- ── v1.1.3 ── -->
|
|
287
|
+
<div class="wn-version" id="wn-113">
|
|
288
|
+
<div class="wn-header" onclick="toggleVersion('wn-113')">
|
|
289
|
+
<div class="wn-title">
|
|
290
|
+
<span class="wn-ver">v1.1.3</span>
|
|
291
|
+
<span class="wn-date">May 2026</span>
|
|
292
|
+
</div>
|
|
293
|
+
<span class="wn-chevron">▾</span>
|
|
294
|
+
</div>
|
|
255
295
|
<div class="wn-body">
|
|
256
296
|
|
|
257
297
|
<div class="wn-item" id="wn-113-1">
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@antzsoft/chat-core",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.5",
|
|
4
4
|
"description": "Platform-agnostic core for Antz Chat — API, socket, stores, types. Works in browser, React Native (Expo or bare), and Node.js.",
|
|
5
5
|
"author": "Antz",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,6 +13,11 @@
|
|
|
13
13
|
"types": "./dist/index.d.ts",
|
|
14
14
|
"import": "./dist/index.js",
|
|
15
15
|
"require": "./dist/index.cjs"
|
|
16
|
+
},
|
|
17
|
+
"./internal": {
|
|
18
|
+
"types": "./dist/internal.d.ts",
|
|
19
|
+
"import": "./dist/internal.js",
|
|
20
|
+
"require": "./dist/internal.cjs"
|
|
16
21
|
}
|
|
17
22
|
},
|
|
18
23
|
"files": [
|