@converse/skeletor
Version:
Models and Collections for modern web apps
180 lines (156 loc) • 5.45 kB
text/typescript
// (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;
}