UNPKG

@converse/skeletor

Version:

Models and Collections for modern web apps

180 lines (156 loc) 5.45 kB
// (c) 2010-2019 Jeremy Ashkenas and DocumentCloud // (c) 2018-2025 JC Brand import create from 'lodash-es/create'; import extend from 'lodash-es/extend'; import has from 'lodash-es/has'; import result from 'lodash-es/result'; import { Model } from './model'; import { Collection } from './collection'; import { type SyncOptions, SyncOperation } from './types'; /** * Custom error for indicating timeouts * @namespace _converse */ export class NotImplementedError extends Error {} function S4(): string { // Generate four random hex digits. return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); } export function guid(): string { // Generate a pseudo-GUID by concatenating random hexadecimal. return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4(); } // Helpers // ------- // Helper function to correctly set up the prototype chain for subclasses. // Similar to `goog.inherits`, but uses a hash of prototype properties and // class properties to be extended. // export function inherits<T extends new (...args: any[]) => any>( protoProps: Record<string, any> | null, staticProps?: Record<string, any> ): T { // eslint-disable-next-line @typescript-eslint/no-this-alias const parent = this; let child: T; // The constructor function for the new subclass is either defined by you // (the "constructor" property in your `extend` definition), or defaulted // by us to simply call the parent constructor. if (protoProps && has(protoProps, 'constructor')) { child = protoProps.constructor; } else { child = function (this: any, ...args: any[]) { return parent.apply(this, args); } as unknown as T; } // Add static properties to the constructor function, if supplied. extend(child, parent, staticProps); // Set the prototype chain to inherit from `parent`, without calling // `parent`'s constructor function and add the prototype properties. child.prototype = create(parent.prototype, protoProps); child.prototype.constructor = child; // Set a convenience property in case the parent's prototype is needed // later. (child as any).__super__ = parent.prototype; return child; } interface PromiseWrapper { isResolved: boolean; isPending: boolean; isRejected: boolean; resolve: ((value?: any) => void) | null; reject: ((reason?: any) => void) | null; } type ResolveablePromise = Promise<any> & PromiseWrapper; export function getResolveablePromise(): ResolveablePromise { const wrapper: PromiseWrapper = { isResolved: false, isPending: true, isRejected: false, resolve: null, reject: null, }; const promise = new Promise((resolve, reject) => { wrapper.resolve = resolve; wrapper.reject = reject; }) as ResolveablePromise; Object.assign(promise, wrapper); promise.then( function (v) { promise.isResolved = true; promise.isPending = false; promise.isRejected = false; return v; }, function (e) { promise.isResolved = false; promise.isPending = false; promise.isRejected = true; throw e; } ); return promise; } // Throw an error when a URL is needed, and none is supplied. export function urlError(): never { throw new Error('A "url" property or function must be specified'); } // Wrap an optional error callback with a fallback error event. export function wrapError(model: Model<any> | Collection<any>, options: any): void { const error = options.error; options.error = function (resp: any) { if (error) error.call(options.context, model, resp, options); model.trigger('error', model, resp, options); }; } // Map from CRUD to HTTP for our default `sync` implementation. const methodMap: Record<string, string> = { create: 'POST', update: 'PUT', patch: 'PATCH', delete: 'DELETE', read: 'GET', }; export function getSyncMethod(model: Model | Collection<any>): typeof sync & { __name__?: string } { const store = result(model, 'browserStorage') || result((model as Model).collection, 'browserStorage'); return store ? (store as any).sync() : sync; } /** * Override this function to change the manner in which Backbone persists * models to the server. You will be passed the type of request, and the * model in question. By default makes a `fetch()` API call * to the model's `url()`. * * Some possible customizations could be: * * - Use `setTimeout` to batch rapid-fire updates into a single request. * - Persist models via WebSockets instead of Ajax. * - Persist models to browser storage */ /** * @public */ export function sync(method: SyncOperation, model: Model | Collection<any>, options: SyncOptions = {}): Promise<any> { let data = options.data; // Ensure that we have the appropriate request data. if (data == null && model && (method === 'create' || method === 'update' || method === 'patch')) { data = options.attrs || model.toJSON(); } const type = methodMap[method]; const params: RequestInit & { success?: any; error?: any } = { method: type, body: data ? JSON.stringify(data) : '', headers: { 'Content-Type': 'application/json', }, success: options.success, error: options.error, }; const url = options.url || result(model, 'url') || urlError(); const xhr = fetch(url, params); if (options) { options.xhr = xhr; } model.trigger('request', model, xhr, { ...params, xhr }); return xhr; }