@thatopen/services 0.28.2 → 0.29.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/cli.js CHANGED
@@ -6960,6 +6960,14 @@ class ProjectDocuments {
6960
6960
  projectId: this.projectId
6961
6961
  });
6962
6962
  }
6963
+ /**
6964
+ * The document's DRAFTS: each user's working copy, kept so that closing the
6965
+ * browser never loses work. A draft is not a version — it never enters the
6966
+ * history and never shows in the CDE. See {@link DocumentDrafts}.
6967
+ */
6968
+ drafts(folderId) {
6969
+ return new DocumentDrafts(this.client, () => this.#descriptorFile(folderId));
6970
+ }
6963
6971
  /**
6964
6972
  * The legacy `derivedFileId` link, read from the newest version metadata
6965
6973
  * that carries one. Only `fromFile` looks at it, to migrate the link into
@@ -6990,6 +6998,120 @@ class ProjectDocuments {
6990
6998
  return found ? { id: idOf$1(found) } : void 0;
6991
6999
  }
6992
7000
  }
7001
+ const DRAFT_TAG = "draft:";
7002
+ const DRAFT_MAGIC = "TODRAFT1";
7003
+ class DocumentDrafts {
7004
+ constructor(client, holder) {
7005
+ this.client = client;
7006
+ this.holder = holder;
7007
+ }
7008
+ /** Every user's latest draft, without downloading any. Newest first. */
7009
+ async list() {
7010
+ const holder = await this.holder();
7011
+ if (!holder) return [];
7012
+ const latest = /* @__PURE__ */ new Map();
7013
+ for (const entry of await this.#entries(holder.id)) {
7014
+ const known = latest.get(entry.userId);
7015
+ if (!known || entry.savedAt > known.savedAt)
7016
+ latest.set(entry.userId, { userId: entry.userId, savedAt: entry.savedAt });
7017
+ }
7018
+ return [...latest.values()].sort((a, b) => +b.savedAt - +a.savedAt);
7019
+ }
7020
+ /**
7021
+ * One user's draft, downloaded — or, without `userId`, the newest of anyone.
7022
+ * Null when there is none.
7023
+ */
7024
+ async get(options) {
7025
+ const holder = await this.holder();
7026
+ if (!holder) return null;
7027
+ const entries = (await this.#entries(holder.id)).filter((e) => !options?.userId || e.userId === options.userId).sort((a, b) => +b.savedAt - +a.savedAt);
7028
+ const newest = entries[0];
7029
+ if (!newest) return null;
7030
+ const response = await this.client.downloadHiddenFile(newest.hiddenId);
7031
+ const { meta, blob } = await unwrapDraft(await response.blob());
7032
+ return { userId: newest.userId, savedAt: newest.savedAt, meta, blob };
7033
+ }
7034
+ /**
7035
+ * Stores this user's draft, replacing their previous one: only the latest is
7036
+ * kept. Needs the document to have a descriptor (convert loose files first
7037
+ * with `fromFile`).
7038
+ */
7039
+ async put(blob, options) {
7040
+ const holder = await this.holder();
7041
+ if (!holder) {
7042
+ throw new Error("This folder is not a document (no document.json): drafts need one.");
7043
+ }
7044
+ const tag = draftTag(options.userId);
7045
+ const previous = (await this.#entries(holder.id)).filter((e) => e.userId === options.userId);
7046
+ const created = await this.client.createHiddenFile(
7047
+ await wrapDraft(blob, options.meta ?? {}),
7048
+ holder.id,
7049
+ tag
7050
+ );
7051
+ const hiddenId = created.hiddenFileId;
7052
+ if (!hiddenId) throw new Error("The draft upload answered without a hiddenFileId.");
7053
+ await this.#deleteAll(previous.map((e) => e.hiddenId));
7054
+ return { userId: options.userId, savedAt: savedAtOf(hiddenId) };
7055
+ }
7056
+ /** Deletes every draft of this user (on an explicit save, typically). */
7057
+ async delete(options) {
7058
+ const holder = await this.holder();
7059
+ if (!holder) return;
7060
+ const mine = (await this.#entries(holder.id)).filter((e) => e.userId === options.userId);
7061
+ await this.#deleteAll(mine.map((e) => e.hiddenId));
7062
+ }
7063
+ async #entries(holderId) {
7064
+ const hidden = await this.client.getHiddenFilesByParent(holderId);
7065
+ return hidden.filter((h) => typeof h.tag === "string" && h.tag.startsWith(DRAFT_TAG)).map((h) => {
7066
+ const hiddenId = String(h._id);
7067
+ return {
7068
+ hiddenId,
7069
+ userId: h.tag.slice(DRAFT_TAG.length),
7070
+ savedAt: savedAtOf(hiddenId)
7071
+ };
7072
+ });
7073
+ }
7074
+ async #deleteAll(hiddenIds) {
7075
+ for (const id2 of hiddenIds) {
7076
+ try {
7077
+ await this.client.deleteHiddenFile(id2);
7078
+ } catch {
7079
+ }
7080
+ }
7081
+ }
7082
+ }
7083
+ function draftTag(userId) {
7084
+ const tag = `${DRAFT_TAG}${userId}`;
7085
+ if (!/^[A-Za-z0-9][A-Za-z0-9:._-]{0,49}$/.test(tag)) {
7086
+ throw new Error(`A draft cannot be tagged for user "${userId}": ids must be letters, digits or :._- and short.`);
7087
+ }
7088
+ return tag;
7089
+ }
7090
+ function savedAtOf(objectId) {
7091
+ const seconds = parseInt(objectId.slice(0, 8), 16);
7092
+ return new Date(Number.isFinite(seconds) ? seconds * 1e3 : 0);
7093
+ }
7094
+ async function wrapDraft(blob, meta) {
7095
+ const metaBytes = new TextEncoder().encode(JSON.stringify(meta));
7096
+ const length = new Uint8Array(4);
7097
+ new DataView(length.buffer).setUint32(0, metaBytes.length, true);
7098
+ return new Blob([new TextEncoder().encode(DRAFT_MAGIC), length, metaBytes, blob]);
7099
+ }
7100
+ async function unwrapDraft(blob) {
7101
+ const head = new Uint8Array(await blob.slice(0, DRAFT_MAGIC.length + 4).arrayBuffer());
7102
+ if (new TextDecoder().decode(head.slice(0, DRAFT_MAGIC.length)) !== DRAFT_MAGIC) {
7103
+ return { meta: {}, blob };
7104
+ }
7105
+ const length = new DataView(head.buffer).getUint32(DRAFT_MAGIC.length, true);
7106
+ const start = DRAFT_MAGIC.length + 4;
7107
+ const metaText = await blob.slice(start, start + length).text();
7108
+ let meta = {};
7109
+ try {
7110
+ meta = JSON.parse(metaText);
7111
+ } catch {
7112
+ }
7113
+ return { meta, blob: blob.slice(start + length) };
7114
+ }
6993
7115
  function formatOf(fileName, extension) {
6994
7116
  const fromExt = (extension ?? "").replace(/^\./, "").toLowerCase();
6995
7117
  if (fromExt) return fromExt;