@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/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.remove(record.id);
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.remove(record.id);
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([...ids].map((id) => storage.markAttached(id)));
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 mkdir(directory, { recursive: true });
925
- await writeAtomically(binPath(record.id), bytes);
926
- await writeAtomically(jsonPath(record.id), JSON.stringify(record));
927
- if (!record.attached) (await loadedIndex())?.set(record.id, { ownerId: record.ownerId, size: record.size });
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
- const record = await readRecord(id);
941
- if (record && !record.attached) await writeAtomically(jsonPath(id), JSON.stringify({ ...record, attached: true }));
942
- (await loadedIndex())?.delete(id);
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 record = await readRecord(id).catch(() => void 0);
971
- if (record && !record.attached && Date.parse(record.createdAt) < before.getTime()) {
972
- await removeFiles(id);
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) {