all repos — snow-editor @ 24ce9e9ef60be3233adb8eaa03f5a936c1ef4acd

small and cozy markdown, and orgmode editor

src/lib/api.js (view raw)

 1
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
 100
 101
 102
 103
 104
 105
 106
 107
 108
 109
 110
 111
 112
 113
 114
 115
 116
 117
 118
 119
 120
 121
 122
 123
 124
 125
 126
import { STR } from './strings.js';

const API_BASE = import.meta.env.VITE_API_BASE ?? '';

function getPublicOrigin() {
  const configured = import.meta.env.VITE_PUBLIC_ORIGIN?.trim();
  if (configured) {
    return configured.replace(/\/$/, '');
  }
  return window.location.origin;
}

export class ApiError extends Error {
  constructor(status, code, message, payload = {}) {
    super(message);
    this.name = 'ApiError';
    this.status = status;
    this.code = code;
    this.payload = payload;
  }
}

const ERROR_MESSAGES = {
  NOT_FOUND: STR.NOT_FOUND,
  EXPIRED: STR.EXPIRED,
  DOCUMENT_LOCKED: STR.DOCUMENT_LOCKED,
  LOCK_REQUIRED: STR.LOCK_REQUIRED,
  CONTENT_TOO_LARGE: STR.CONTENT_TOO_LARGE,
  RATE_LIMIT: STR.RATE_LIMIT,
  NETWORK: STR.NETWORK,
};

export function friendlyErrorMessage(error) {
  if (error instanceof ApiError) {
    return error.message || ERROR_MESSAGES[error.code] || STR.GENERIC_ERROR;
  }
  return ERROR_MESSAGES.NETWORK;
}

async function parseResponse(res) {
  let data = null;
  const text = await res.text();
  if (text) {
    try {
      data = JSON.parse(text);
    } catch {
      data = { message: text };
    }
  }

  if (!res.ok) {
    const code = data?.error ?? 'UNKNOWN';
    const message =
      data?.message ?? ERROR_MESSAGES[code] ?? STR.UNEXPECTED_ERROR;
    throw new ApiError(res.status, code, message, data ?? {});
  }

  return data;
}

async function request(path, options = {}) {
  let res;
  try {
    res = await fetch(`${API_BASE}${path}`, {
      ...options,
      headers: {
        'Content-Type': 'application/json',
        ...(options.headers ?? {}),
      },
    });
  } catch {
    throw new ApiError(0, 'NETWORK', ERROR_MESSAGES.NETWORK);
  }
  return parseResponse(res);
}

export function createDocument(body) {
  return request('/api/documents', {
    method: 'POST',
    body: JSON.stringify(body),
  });
}

export function fetchViewDocument(token) {
  return request(`/api/documents/view/${encodeURIComponent(token)}`);
}

export function fetchEditDocument(token) {
  return request(`/api/documents/edit/${encodeURIComponent(token)}`);
}

export function acquireEditLock(token, clientId) {
  return request(`/api/documents/edit/${encodeURIComponent(token)}/lock`, {
    method: 'POST',
    body: JSON.stringify({ clientId }),
  });
}

export function refreshEditLock(token, clientId, lockToken) {
  return request(
    `/api/documents/edit/${encodeURIComponent(token)}/lock/refresh`,
    {
      method: 'POST',
      body: JSON.stringify({ clientId, lockToken }),
    },
  );
}

export function releaseEditLock(token, clientId, lockToken) {
  return request(`/api/documents/edit/${encodeURIComponent(token)}/lock`, {
    method: 'DELETE',
    body: JSON.stringify({ clientId, lockToken }),
  });
}

export function updateDocument(token, body) {
  return request(`/api/documents/edit/${encodeURIComponent(token)}`, {
    method: 'PUT',
    body: JSON.stringify(body),
  });
}

export function toAbsoluteUrl(path) {
  if (path.startsWith('http')) return path;
  return `${getPublicOrigin()}${path}`;
}