@ykurilov/agent-chat-attachments 0.80.0 → 0.81.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/CHANGELOG.md +25 -0
- package/README.md +3 -0
- package/dist/attachment-storage-contract.js +62 -0
- package/dist/attachment-storage-contract.js.map +1 -1
- package/dist/index.js +66 -19
- package/dist/index.js.map +1 -1
- package/dist/types/attachment-storage.d.ts +10 -1
- package/dist/types/disk-attachment-storage.d.ts +2 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -719,7 +719,8 @@ function createAttachmentRoutes(options) {
|
|
|
719
719
|
if (!record) return failure("not_found");
|
|
720
720
|
if (method === "GET") return download(record);
|
|
721
721
|
if (record.attached) return failure("attached");
|
|
722
|
-
await storage.
|
|
722
|
+
const removed = await storage.removeUnattached(record.id);
|
|
723
|
+
if (!removed && await storage.get(record.id)) return failure("attached");
|
|
723
724
|
return new Response(null, { status: 204 });
|
|
724
725
|
}
|
|
725
726
|
};
|
|
@@ -739,6 +740,9 @@ var ABANDONED_AFTER_MS = 24 * 60 * 6e4;
|
|
|
739
740
|
var CLEANUP_EVERY_MS = 15 * 6e4;
|
|
740
741
|
function createAttachments(options) {
|
|
741
742
|
const { storage } = options;
|
|
743
|
+
if (typeof storage.removeUnattached !== "function") {
|
|
744
|
+
throw new TypeError("AttachmentStorage: \u043D\u0443\u0436\u0435\u043D \u043C\u0435\u0442\u043E\u0434 removeUnattached(id) \u2014 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u043D\u0435\u043F\u0440\u0438\u043A\u0440\u0435\u043F\u043B\u0451\u043D\u043D\u043E\u0433\u043E \u0444\u0430\u0439\u043B\u0430, \u0430\u0442\u043E\u043C\u0430\u0440\u043D\u043E \u0441 markAttached");
|
|
745
|
+
}
|
|
742
746
|
const limits = resolveAttachmentLimits(options.limits);
|
|
743
747
|
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
744
748
|
const imagePlans = createImagePlans(limits.imageCacheBytes);
|
|
@@ -771,7 +775,7 @@ function createAttachments(options) {
|
|
|
771
775
|
const record = isAttachmentId(id) ? await storage.get(id) : void 0;
|
|
772
776
|
if (!record || record.chatId !== input.chatId || record.ownerId !== input.userId) throw notFound();
|
|
773
777
|
if (!record.attached && Date.parse(record.createdAt) < now().getTime() - ABANDONED_AFTER_MS) {
|
|
774
|
-
await storage.
|
|
778
|
+
await storage.removeUnattached(record.id);
|
|
775
779
|
throw new AttachmentRejectedError3("attachment_expired", "\u0424\u0430\u0439\u043B \u0445\u0440\u0430\u043D\u0438\u043B\u0441\u044F \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0434\u043E\u043B\u0433\u043E \u0438 \u0443\u0434\u0430\u043B\u0451\u043D: \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u0435 \u0435\u0433\u043E \u0441\u043D\u043E\u0432\u0430");
|
|
776
780
|
}
|
|
777
781
|
if (acceptedMediaTypes && !acceptedMediaTypes.includes(record.sourceMediaType ?? record.mediaType)) {
|
|
@@ -786,8 +790,18 @@ function createAttachments(options) {
|
|
|
786
790
|
return records.map((record) => ({ type: "file", url: `${PREFIX2}${record.id}`, mediaType: record.mediaType, filename: record.name }));
|
|
787
791
|
}
|
|
788
792
|
async function confirmUserParts(parts) {
|
|
789
|
-
const ids = new Set(parts.flatMap((part) => part.url.startsWith(PREFIX2) ? [part.url.slice(PREFIX2.length)] : []));
|
|
790
|
-
await Promise.all(
|
|
793
|
+
const ids = [...new Set(parts.flatMap((part) => part.url.startsWith(PREFIX2) ? [part.url.slice(PREFIX2.length)] : []))];
|
|
794
|
+
const records = await Promise.all(ids.map(async (id) => {
|
|
795
|
+
await storage.markAttached(id);
|
|
796
|
+
return storage.get(id);
|
|
797
|
+
})).catch((error) => {
|
|
798
|
+
const unavailable2 = new AttachmentRejectedError3("attachment_unavailable", "\u0424\u0430\u0439\u043B \u0441\u0435\u0439\u0447\u0430\u0441 \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u0438\u043A\u0440\u0435\u043F\u0438\u0442\u044C: \u043E\u0442\u043F\u0440\u0430\u0432\u044C\u0442\u0435 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0435\u0449\u0451 \u0440\u0430\u0437");
|
|
799
|
+
unavailable2.cause = error;
|
|
800
|
+
throw unavailable2;
|
|
801
|
+
});
|
|
802
|
+
if (records.some((record) => record?.attached !== true)) {
|
|
803
|
+
throw new AttachmentRejectedError3("attachment_not_found", "\u0424\u0430\u0439\u043B \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u0435 \u0435\u0433\u043E \u0435\u0449\u0451 \u0440\u0430\u0437");
|
|
804
|
+
}
|
|
791
805
|
}
|
|
792
806
|
return {
|
|
793
807
|
storage,
|
|
@@ -850,6 +864,16 @@ function createDiskAttachmentStorage(options) {
|
|
|
850
864
|
const binPath = (id) => join(directory, `${id}.bin`);
|
|
851
865
|
const jsonPath = (id) => join(directory, `${id}.json`);
|
|
852
866
|
let unsentIndex;
|
|
867
|
+
const fileQueues = /* @__PURE__ */ new Map();
|
|
868
|
+
function oneAtATime(id, work) {
|
|
869
|
+
const done = (fileQueues.get(id) ?? Promise.resolve()).then(work);
|
|
870
|
+
const settled = done.catch(() => void 0);
|
|
871
|
+
fileQueues.set(id, settled);
|
|
872
|
+
void settled.then(() => {
|
|
873
|
+
if (fileQueues.get(id) === settled) fileQueues.delete(id);
|
|
874
|
+
});
|
|
875
|
+
return done;
|
|
876
|
+
}
|
|
853
877
|
async function writeAtomically(path, data) {
|
|
854
878
|
const temporary = `${path}.${crypto.randomUUID()}.tmp`;
|
|
855
879
|
await writeFile(temporary, data);
|
|
@@ -894,6 +918,14 @@ function createDiskAttachmentStorage(options) {
|
|
|
894
918
|
await Promise.all([rm(jsonPath(id), { force: true }), rm(binPath(id), { force: true })]);
|
|
895
919
|
(await loadedIndex())?.delete(id);
|
|
896
920
|
}
|
|
921
|
+
function removeIfUnattached(id, stale = () => true) {
|
|
922
|
+
return oneAtATime(id, async () => {
|
|
923
|
+
const record = await readRecord(id);
|
|
924
|
+
if (!record || record.attached || !stale(record)) return false;
|
|
925
|
+
await removeFiles(id);
|
|
926
|
+
return true;
|
|
927
|
+
});
|
|
928
|
+
}
|
|
897
929
|
async function modifiedBefore(name, before) {
|
|
898
930
|
try {
|
|
899
931
|
return (await stat(join(directory, name))).mtimeMs < before.getTime();
|
|
@@ -921,10 +953,12 @@ function createDiskAttachmentStorage(options) {
|
|
|
921
953
|
return {
|
|
922
954
|
async save(record, bytes) {
|
|
923
955
|
assertAttachmentId(record.id);
|
|
924
|
-
await
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
956
|
+
await oneAtATime(record.id, async () => {
|
|
957
|
+
await mkdir(directory, { recursive: true });
|
|
958
|
+
await writeAtomically(binPath(record.id), bytes);
|
|
959
|
+
await writeAtomically(jsonPath(record.id), JSON.stringify(record));
|
|
960
|
+
if (!record.attached) (await loadedIndex())?.set(record.id, { ownerId: record.ownerId, size: record.size });
|
|
961
|
+
});
|
|
928
962
|
},
|
|
929
963
|
get: readRecord,
|
|
930
964
|
async readBytes(id) {
|
|
@@ -937,16 +971,22 @@ function createDiskAttachmentStorage(options) {
|
|
|
937
971
|
}
|
|
938
972
|
},
|
|
939
973
|
async markAttached(id) {
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
974
|
+
if (!isAttachmentId(id)) return;
|
|
975
|
+
await oneAtATime(id, async () => {
|
|
976
|
+
const record = await readRecord(id);
|
|
977
|
+
if (record && !record.attached) await writeAtomically(jsonPath(id), JSON.stringify({ ...record, attached: true }));
|
|
978
|
+
(await loadedIndex())?.delete(id);
|
|
979
|
+
});
|
|
943
980
|
},
|
|
944
981
|
async remove(id) {
|
|
945
|
-
if (isAttachmentId(id)) await removeFiles(id);
|
|
982
|
+
if (isAttachmentId(id)) await oneAtATime(id, () => removeFiles(id));
|
|
983
|
+
},
|
|
984
|
+
async removeUnattached(id) {
|
|
985
|
+
return isAttachmentId(id) && removeIfUnattached(id);
|
|
946
986
|
},
|
|
947
987
|
async removeAllOfChat(chatId) {
|
|
948
988
|
const mine = (await listRecords()).filter((record) => record.chatId === chatId);
|
|
949
|
-
for (const record of mine) await removeFiles(record.id);
|
|
989
|
+
for (const record of mine) await oneAtATime(record.id, () => removeFiles(record.id));
|
|
950
990
|
return mine.length;
|
|
951
991
|
},
|
|
952
992
|
async unattachedTotals(ownerId) {
|
|
@@ -967,13 +1007,16 @@ function createDiskAttachmentStorage(options) {
|
|
|
967
1007
|
for (const name of names) {
|
|
968
1008
|
if (name.endsWith(".json")) {
|
|
969
1009
|
const id = name.slice(0, -".json".length);
|
|
970
|
-
const
|
|
971
|
-
if (
|
|
972
|
-
|
|
973
|
-
removed++;
|
|
974
|
-
}
|
|
975
|
-
} else if (name.endsWith(".tmp") || name.endsWith(".bin") && !present.has(`${name.slice(0, -".bin".length)}.json`)) {
|
|
1010
|
+
const stale = (record) => Date.parse(record.createdAt) < before.getTime();
|
|
1011
|
+
if (await removeIfUnattached(id, stale).catch(() => false)) removed++;
|
|
1012
|
+
} else if (name.endsWith(".tmp")) {
|
|
976
1013
|
if (await modifiedBefore(name, before)) await rm(join(directory, name), { force: true });
|
|
1014
|
+
} else if (name.endsWith(".bin") && !present.has(`${name.slice(0, -".bin".length)}.json`)) {
|
|
1015
|
+
const id = name.slice(0, -".bin".length);
|
|
1016
|
+
await oneAtATime(id, async () => {
|
|
1017
|
+
const recordAppeared = await readRecord(id).catch(() => void 0) !== void 0;
|
|
1018
|
+
if (!recordAppeared && await modifiedBefore(name, before)) await rm(join(directory, name), { force: true });
|
|
1019
|
+
});
|
|
977
1020
|
}
|
|
978
1021
|
}
|
|
979
1022
|
return removed;
|
|
@@ -1003,6 +1046,10 @@ function createMemoryAttachmentStorage() {
|
|
|
1003
1046
|
async remove(id) {
|
|
1004
1047
|
if (isAttachmentId(id)) records.delete(id);
|
|
1005
1048
|
},
|
|
1049
|
+
async removeUnattached(id) {
|
|
1050
|
+
const entry = isAttachmentId(id) ? records.get(id) : void 0;
|
|
1051
|
+
return entry !== void 0 && !entry.record.attached && records.delete(id);
|
|
1052
|
+
},
|
|
1006
1053
|
async removeAllOfChat(chatId) {
|
|
1007
1054
|
let removed = 0;
|
|
1008
1055
|
for (const [id, { record }] of records) {
|