@converse/skeletor 0.0.8 → 1.0.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.
@@ -0,0 +1,297 @@
1
+ // Copyright 2014 Mozilla
2
+ // Copyright 2015 Thodoris Greasidis
3
+ // Copyright 2018-2025 JC Brand
4
+ //
5
+ // Licensed under the Apache License, Version 2.0 (the "License");
6
+ // you may not use this file except in compliance with the License.
7
+ // You may obtain a copy of the License at
8
+ //
9
+ // http://www.apache.org/licenses/LICENSE-2.0
10
+ //
11
+ // Unless required by applicable law or agreed to in writing, software
12
+ // distributed under the License is distributed on an "AS IS" BASIS,
13
+ // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ // See the License for the specific language governing permissions and
15
+ // limitations under the License.
16
+
17
+ import executeCallback from 'localforage/src/utils/executeCallback';
18
+ import getCallback from 'localforage/src/utils/getCallback';
19
+ import normalizeKey from 'localforage/src/utils/normalizeKey';
20
+ import serializer from 'localforage/src/utils/serializer';
21
+
22
+ const serialize = serializer['serialize'];
23
+ const deserialize = serializer['deserialize'];
24
+
25
+ interface DbInfo {
26
+ keyPrefix: string;
27
+ serializer: {
28
+ serialize: typeof serialize;
29
+ deserialize: typeof deserialize;
30
+ };
31
+ [key: string]: any;
32
+ }
33
+
34
+ interface LocalForageOptions {
35
+ name: string;
36
+ storeName: string;
37
+ [key: string]: any;
38
+ }
39
+
40
+ interface IteratorCallback<T, U> {
41
+ (value: T, key: string, iterationNumber: number): U;
42
+ }
43
+
44
+ function isSessionStorageValid(): boolean {
45
+ try {
46
+ if (sessionStorage && 'setItem' in sessionStorage) {
47
+ return true;
48
+ }
49
+ } catch (e) {
50
+ console.log(e);
51
+ }
52
+ return false;
53
+ }
54
+
55
+ function _getKeyPrefix(options: LocalForageOptions, defaultConfig: LocalForageOptions): string {
56
+ let keyPrefix = options.name + '/';
57
+
58
+ if (options.storeName !== defaultConfig.storeName) {
59
+ keyPrefix += options.storeName + '/';
60
+ }
61
+ return keyPrefix;
62
+ }
63
+
64
+ const dbInfo: DbInfo = {
65
+ keyPrefix: '',
66
+ serializer: {
67
+ serialize: serialize,
68
+ deserialize: deserialize,
69
+ },
70
+ };
71
+
72
+ function _initStorage(this: any, options: LocalForageOptions): void {
73
+ dbInfo.keyPrefix = _getKeyPrefix(options, this._defaultConfig);
74
+ if (options) {
75
+ for (const i in options) {
76
+ dbInfo[i] = options[i];
77
+ }
78
+ }
79
+ }
80
+
81
+ // Remove all keys from the datastore, effectively destroying all data in
82
+ // the app's key/value store!
83
+ function clear(callback?: (err: any) => void): Promise<void> {
84
+ const promise = this.ready().then(function () {
85
+ const keyPrefix = dbInfo.keyPrefix;
86
+
87
+ for (let i = sessionStorage.length - 1; i >= 0; i--) {
88
+ const key = sessionStorage.key(i);
89
+ if (key && key.indexOf(keyPrefix) === 0) {
90
+ sessionStorage.removeItem(key);
91
+ }
92
+ }
93
+ });
94
+
95
+ executeCallback(promise, callback);
96
+ return promise;
97
+ }
98
+
99
+ function getItem<T>(key: string, callback?: (err: any, value: T | null) => void): Promise<T | null> {
100
+ key = normalizeKey(key);
101
+
102
+ const promise = this.ready().then(function (): T | null {
103
+ const result = sessionStorage.getItem(dbInfo.keyPrefix + key);
104
+ // If a result was found, parse it from the serialized
105
+ // string into a JS object. If result isn't truthy, the key
106
+ // is likely undefined and we'll pass it straight to the
107
+ // callback.
108
+ if (result) {
109
+ return dbInfo.serializer.deserialize(result) as T;
110
+ }
111
+ return null;
112
+ });
113
+
114
+ executeCallback(promise, callback);
115
+ return promise;
116
+ }
117
+
118
+ function iterate<T, U>(
119
+ iterator: IteratorCallback<T, U>,
120
+ callback?: (err: any, result: U | undefined) => void
121
+ ): Promise<U | undefined> {
122
+ const self = this;
123
+
124
+ const promise = self.ready().then(function (): U | undefined {
125
+ const keyPrefix = dbInfo.keyPrefix;
126
+ const keyPrefixLength = keyPrefix.length;
127
+ const length = sessionStorage.length;
128
+ let iterationNumber = 1;
129
+
130
+ for (let i = 0; i < length; i++) {
131
+ const key = sessionStorage.key(i);
132
+ if (!key || key.indexOf(keyPrefix) !== 0) {
133
+ continue;
134
+ }
135
+
136
+ // If a result was found, parse it from the serialized
137
+ // string into a JS object. If result isn't truthy, the
138
+ // key is likely undefined and we'll pass it straight
139
+ // to the iterator.
140
+ const item = sessionStorage.getItem(key);
141
+ const value = item ? dbInfo.serializer.deserialize(item) : item;
142
+
143
+ const result = iterator(value as T, key.substring(keyPrefixLength), iterationNumber++);
144
+ if (result !== void 0) {
145
+ return result;
146
+ }
147
+ }
148
+ return;
149
+ });
150
+
151
+ executeCallback(promise, callback);
152
+ return promise;
153
+ }
154
+
155
+ // Same as sessionStorage's key() method, except takes a callback.
156
+ function key(n: number, callback?: (err: any, key: string | null) => void): Promise<string | null> {
157
+ const self = this;
158
+ const promise = self.ready().then(function (): string | null {
159
+ let result: string | null;
160
+ try {
161
+ result = sessionStorage.key(n);
162
+ } catch (error) {
163
+ result = null;
164
+ }
165
+
166
+ // Remove the prefix from the key, if a key is found.
167
+ if (result) {
168
+ result = result.substring(dbInfo.keyPrefix.length);
169
+ }
170
+
171
+ return result;
172
+ });
173
+
174
+ executeCallback(promise, callback);
175
+ return promise;
176
+ }
177
+
178
+ function keys(callback?: (err: any, keys: string[]) => void): Promise<string[]> {
179
+ const self = this;
180
+ const promise = self.ready().then(function (): string[] {
181
+ const length = sessionStorage.length;
182
+ const keys: string[] = [];
183
+
184
+ for (let i = 0; i < length; i++) {
185
+ const itemKey = sessionStorage.key(i);
186
+ if (itemKey && itemKey.indexOf(dbInfo.keyPrefix) === 0) {
187
+ keys.push(itemKey.substring(dbInfo.keyPrefix.length));
188
+ }
189
+ }
190
+ return keys;
191
+ });
192
+
193
+ executeCallback(promise, callback);
194
+ return promise;
195
+ }
196
+
197
+ // Supply the number of keys in the datastore to the callback function.
198
+ function length(callback?: (err: any, numberOfKeys: number) => void): Promise<number> {
199
+ const self = this;
200
+ const promise = self.keys().then(function (keys: string[]) {
201
+ return keys.length;
202
+ });
203
+
204
+ executeCallback(promise, callback);
205
+ return promise;
206
+ }
207
+
208
+ function removeItem(key: string, callback?: (err: any) => void): Promise<void> {
209
+ key = normalizeKey(key);
210
+ const promise = this.ready().then(function () {
211
+ sessionStorage.removeItem(dbInfo.keyPrefix + key);
212
+ });
213
+ executeCallback(promise, callback);
214
+ return promise;
215
+ }
216
+
217
+ async function setItem<T>(key: string, value: T, callback?: (err: any, value: T) => void): Promise<T> {
218
+ key = normalizeKey(key);
219
+ await this.ready();
220
+
221
+ value = value ?? (null as unknown as T);
222
+ const originalValue = value;
223
+
224
+ return new Promise<T>((resolve, reject) => {
225
+ dbInfo.serializer.serialize(value, (serializedValue: string, error: any) => {
226
+ if (error) {
227
+ reject(error);
228
+ executeCallback(Promise.reject(error), callback);
229
+ return;
230
+ }
231
+
232
+ try {
233
+ sessionStorage.setItem(dbInfo.keyPrefix + key, serializedValue);
234
+ resolve(originalValue);
235
+ executeCallback(Promise.resolve(originalValue), callback);
236
+ } catch (e: any) {
237
+ if (e.name === 'QuotaExceededError' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') {
238
+ console.error('Your sessionStorage capacity is used up.');
239
+ }
240
+ reject(e);
241
+ executeCallback(Promise.reject(e), callback);
242
+ }
243
+ });
244
+ });
245
+ }
246
+
247
+ function dropInstance(options: any, callback?: (err: any) => void): Promise<void> {
248
+ callback = getCallback.apply(this, arguments as any);
249
+
250
+ options = (typeof options !== 'function' && options) || {};
251
+ if (!options.name) {
252
+ const currentConfig = this.config();
253
+ options.name = options.name || currentConfig.name;
254
+ options.storeName = options.storeName || currentConfig.storeName;
255
+ }
256
+
257
+ const self = this;
258
+ let promise: Promise<void>;
259
+ if (!options.name) {
260
+ promise = Promise.reject(new Error('Invalid arguments'));
261
+ } else {
262
+ promise = new Promise<string>(function (resolve) {
263
+ if (!options.storeName) {
264
+ resolve(`${options.name}/`);
265
+ } else {
266
+ resolve(_getKeyPrefix(options, self._defaultConfig));
267
+ }
268
+ }).then(function (keyPrefix: string) {
269
+ for (let i = sessionStorage.length - 1; i >= 0; i--) {
270
+ const key = sessionStorage.key(i);
271
+ if (key && key.indexOf(keyPrefix) === 0) {
272
+ sessionStorage.removeItem(key);
273
+ }
274
+ }
275
+ });
276
+ }
277
+
278
+ executeCallback(promise, callback);
279
+ return promise;
280
+ }
281
+
282
+ const sessionStorageWrapper = {
283
+ _driver: 'sessionStorageWrapper',
284
+ _initStorage: _initStorage,
285
+ _support: isSessionStorageValid(),
286
+ iterate,
287
+ getItem,
288
+ setItem,
289
+ removeItem,
290
+ clear,
291
+ length,
292
+ key,
293
+ keys,
294
+ dropInstance,
295
+ };
296
+
297
+ export default sessionStorageWrapper;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * @copyright 2010-2019 Jeremy Ashkenas and DocumentCloud
3
+ * @copyright 2023 JC Brand
4
+ */
5
+ import isEmpty from 'lodash-es/isEmpty';
6
+ import keys from 'lodash-es/keys';
7
+ import uniqueId from 'lodash-es/uniqueId';
8
+ import Listening from './listening';
9
+ import { eventsApi, onApi, offApi, onceMap, tryCatchOn, triggerApi } from './utils/events';
10
+ import {
11
+ ClassConstructor,
12
+ EventCallback,
13
+ EventCallbackMap,
14
+ EventContext,
15
+ EventHandlersMap,
16
+ EventListenerMap,
17
+ ObjectListenedTo,
18
+ } from './types';
19
+
20
+ // A private global variable to share between listeners and listenees.
21
+ let _listening: Listening | undefined;
22
+
23
+ /**
24
+ * @public
25
+ */
26
+ export function EventEmitter<T extends ClassConstructor>(Base: T) {
27
+ return class EventEmitter extends Base implements EventEmitter {
28
+ _events?: EventHandlersMap;
29
+ _listeners?: EventListenerMap;
30
+ _listeningTo?: EventListenerMap;
31
+ _listenId?: string;
32
+
33
+ /**
34
+ * Bind an event to a `callback` function. Passing `"all"` will bind
35
+ * the callback to all events fired.
36
+ */
37
+ on(name: string | EventCallbackMap, callback?: EventCallback | EventContext, context?: EventContext): this {
38
+ this._events = eventsApi(onApi, this._events || {}, name, callback, {
39
+ context: context,
40
+ ctx: this,
41
+ listening: _listening,
42
+ }) as EventHandlersMap;
43
+
44
+ if (_listening) {
45
+ const listeners = this._listeners || (this._listeners = {});
46
+ listeners[_listening.id] = _listening;
47
+ // Allow the listening to use a counter, instead of tracking
48
+ // callbacks for library interop
49
+ _listening.interop = false;
50
+ }
51
+
52
+ return this;
53
+ }
54
+
55
+ /**
56
+ * Inversion-of-control versions of `on`. Tell *this* object to listen to
57
+ * an event in another object... keeping track of what it's listening to
58
+ * for easier unbinding later.
59
+ */
60
+ listenTo(obj: ObjectListenedTo, name: string | EventCallbackMap, callback?: EventCallback): this {
61
+ if (!obj) return this;
62
+
63
+ const id = obj._listenId || (obj._listenId = uniqueId('l'));
64
+ const listeningTo = this._listeningTo || (this._listeningTo = {});
65
+ let listening = (_listening = listeningTo[id]);
66
+
67
+ // This object is not listening to any other events on `obj` yet.
68
+ // Setup the necessary references to track the listening callbacks.
69
+ if (!listening) {
70
+ this._listenId || (this._listenId = uniqueId('l'));
71
+ listening = _listening = listeningTo[id] = new Listening(this, obj);
72
+ }
73
+
74
+ // Bind callbacks on obj.
75
+ const error = tryCatchOn(obj, name, callback, this);
76
+ _listening = undefined;
77
+
78
+ if (error) throw error;
79
+ // If the target obj is not Backbone.Events, track events manually.
80
+ if (listening.interop) listening.start(name, callback, this, _listening);
81
+
82
+ return this;
83
+ }
84
+
85
+ /**
86
+ * Remove one or many callbacks. If `context` is null, removes all
87
+ * callbacks with that function. If `callback` is null, removes all
88
+ * callbacks for the event. If `name` is null, removes all bound
89
+ * callbacks for all events.
90
+ */
91
+ off(
92
+ name?: string | EventCallbackMap | null,
93
+ callback?: EventCallback | EventContext | null,
94
+ context?: EventContext
95
+ ): this {
96
+ if (!this._events) return this;
97
+ this._events = eventsApi(offApi, this._events, name, callback, {
98
+ context: context,
99
+ listeners: this._listeners,
100
+ }) as EventHandlersMap;
101
+
102
+ return this;
103
+ }
104
+
105
+ /**
106
+ * Tell this object to stop listening to either specific events ... or
107
+ * to every object it's currently listening to.
108
+ */
109
+ stopListening(obj?: any, name?: string | EventCallbackMap, callback?: EventCallback): this {
110
+ const listeningTo = this._listeningTo;
111
+ if (!listeningTo) return this;
112
+
113
+ const ids = obj ? [obj._listenId] : keys(listeningTo);
114
+ for (let i = 0; i < ids.length; i++) {
115
+ const listening = listeningTo[ids[i]];
116
+
117
+ // If listening doesn't exist, this object is not currently
118
+ // listening to obj. Break out early.
119
+ if (!listening) break;
120
+
121
+ listening.obj.off(name, callback, this);
122
+ if (listening.interop) listening.stop(name, callback);
123
+ }
124
+ if (isEmpty(listeningTo)) this._listeningTo = undefined;
125
+
126
+ return this;
127
+ }
128
+
129
+ /**
130
+ * Bind an event to only be triggered a single time. After the first time
131
+ * the callback is invoked, its listener will be removed. If multiple events
132
+ * are passed in using the space-separated syntax, the handler will fire
133
+ * once for each event, not once for a combination of all events.
134
+ */
135
+ once(name: string | EventCallbackMap, callback?: EventCallback | EventContext, context?: EventContext): this {
136
+ // Map the event into a `{event: once}` object.
137
+ const events = eventsApi(onceMap, {}, name, callback, this.off.bind(this));
138
+ if (typeof name === 'string' && (context === null || context === undefined)) callback = undefined;
139
+ return this.on(events as EventCallbackMap, callback, context);
140
+ }
141
+
142
+ /**
143
+ * Inversion-of-control versions of `once`.
144
+ */
145
+ listenToOnce(obj: any, name: string | EventCallbackMap, callback?: EventCallback): this {
146
+ // Map the event into a `{event: once}` object.
147
+ const events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj));
148
+ return this.listenTo(obj, events as string | EventCallbackMap);
149
+ }
150
+
151
+ /**
152
+ * Trigger one or many events, firing all bound callbacks. Callbacks are
153
+ * passed the same arguments as `trigger` is, apart from the event name
154
+ * (unless you're listening on `"all"`, which will cause your callback to
155
+ * receive the true name of the event as the first argument).
156
+ */
157
+ trigger(name: string, ...args: any[]): this {
158
+ if (!this._events) return this;
159
+
160
+ eventsApi(triggerApi, this._events, name, undefined, args);
161
+ return this;
162
+ }
163
+ };
164
+ }
165
+
166
+ /**
167
+ * @public
168
+ */
169
+ const EventEmitterObject = EventEmitter(Object);
170
+ export { EventEmitterObject };
171
+
172
+ export default EventEmitter;
package/src/helpers.ts ADDED
@@ -0,0 +1,179 @@
1
+ // (c) 2010-2019 Jeremy Ashkenas and DocumentCloud
2
+ // (c) 2018-2025 JC Brand
3
+ import create from 'lodash-es/create';
4
+ import extend from 'lodash-es/extend';
5
+ import has from 'lodash-es/has';
6
+ import result from 'lodash-es/result';
7
+ import { Model } from './model';
8
+ import { Collection } from './collection';
9
+ import { type SyncOptions, SyncOperation } from './types';
10
+
11
+ /**
12
+ * Custom error for indicating timeouts
13
+ * @namespace _converse
14
+ */
15
+ export class NotImplementedError extends Error {}
16
+
17
+ function S4(): string {
18
+ // Generate four random hex digits.
19
+ return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1);
20
+ }
21
+
22
+ export function guid(): string {
23
+ // Generate a pseudo-GUID by concatenating random hexadecimal.
24
+ return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4();
25
+ }
26
+
27
+ // Helpers
28
+ // -------
29
+
30
+ // Helper function to correctly set up the prototype chain for subclasses.
31
+ // Similar to `goog.inherits`, but uses a hash of prototype properties and
32
+ // class properties to be extended.
33
+ //
34
+ export function inherits<T extends new (...args: any[]) => any>(
35
+ protoProps: Record<string, any> | null,
36
+ staticProps?: Record<string, any>
37
+ ): T {
38
+ // eslint-disable-next-line @typescript-eslint/no-this-alias
39
+ const parent = this;
40
+ let child: T;
41
+
42
+ // The constructor function for the new subclass is either defined by you
43
+ // (the "constructor" property in your `extend` definition), or defaulted
44
+ // by us to simply call the parent constructor.
45
+ if (protoProps && has(protoProps, 'constructor')) {
46
+ child = protoProps.constructor;
47
+ } else {
48
+ child = function (this: any, ...args: any[]) {
49
+ return parent.apply(this, args);
50
+ } as unknown as T;
51
+ }
52
+
53
+ // Add static properties to the constructor function, if supplied.
54
+ extend(child, parent, staticProps);
55
+
56
+ // Set the prototype chain to inherit from `parent`, without calling
57
+ // `parent`'s constructor function and add the prototype properties.
58
+ child.prototype = create(parent.prototype, protoProps);
59
+ child.prototype.constructor = child;
60
+
61
+ // Set a convenience property in case the parent's prototype is needed
62
+ // later.
63
+ (child as any).__super__ = parent.prototype;
64
+
65
+ return child;
66
+ }
67
+
68
+ interface PromiseWrapper {
69
+ isResolved: boolean;
70
+ isPending: boolean;
71
+ isRejected: boolean;
72
+ resolve: ((value?: any) => void) | null;
73
+ reject: ((reason?: any) => void) | null;
74
+ }
75
+
76
+ type ResolveablePromise = Promise<any> & PromiseWrapper;
77
+
78
+ export function getResolveablePromise(): ResolveablePromise {
79
+ const wrapper: PromiseWrapper = {
80
+ isResolved: false,
81
+ isPending: true,
82
+ isRejected: false,
83
+ resolve: null,
84
+ reject: null,
85
+ };
86
+
87
+ const promise = new Promise((resolve, reject) => {
88
+ wrapper.resolve = resolve;
89
+ wrapper.reject = reject;
90
+ }) as ResolveablePromise;
91
+
92
+ Object.assign(promise, wrapper);
93
+ promise.then(
94
+ function (v) {
95
+ promise.isResolved = true;
96
+ promise.isPending = false;
97
+ promise.isRejected = false;
98
+ return v;
99
+ },
100
+ function (e) {
101
+ promise.isResolved = false;
102
+ promise.isPending = false;
103
+ promise.isRejected = true;
104
+ throw e;
105
+ }
106
+ );
107
+ return promise;
108
+ }
109
+
110
+ // Throw an error when a URL is needed, and none is supplied.
111
+ export function urlError(): never {
112
+ throw new Error('A "url" property or function must be specified');
113
+ }
114
+
115
+ // Wrap an optional error callback with a fallback error event.
116
+ export function wrapError(model: Model<any> | Collection<any>, options: any): void {
117
+ const error = options.error;
118
+ options.error = function (resp: any) {
119
+ if (error) error.call(options.context, model, resp, options);
120
+ model.trigger('error', model, resp, options);
121
+ };
122
+ }
123
+
124
+ // Map from CRUD to HTTP for our default `sync` implementation.
125
+ const methodMap: Record<string, string> = {
126
+ create: 'POST',
127
+ update: 'PUT',
128
+ patch: 'PATCH',
129
+ delete: 'DELETE',
130
+ read: 'GET',
131
+ };
132
+
133
+ export function getSyncMethod(model: Model | Collection<any>): typeof sync & { __name__?: string } {
134
+ const store = result(model, 'browserStorage') || result((model as Model).collection, 'browserStorage');
135
+ return store ? (store as any).sync() : sync;
136
+ }
137
+
138
+ /**
139
+ * Override this function to change the manner in which Backbone persists
140
+ * models to the server. You will be passed the type of request, and the
141
+ * model in question. By default makes a `fetch()` API call
142
+ * to the model's `url()`.
143
+ *
144
+ * Some possible customizations could be:
145
+ *
146
+ * - Use `setTimeout` to batch rapid-fire updates into a single request.
147
+ * - Persist models via WebSockets instead of Ajax.
148
+ * - Persist models to browser storage
149
+ */
150
+ /**
151
+ * @public
152
+ */
153
+ export function sync(method: SyncOperation, model: Model | Collection<any>, options: SyncOptions = {}): Promise<any> {
154
+ let data = options.data;
155
+
156
+ // Ensure that we have the appropriate request data.
157
+ if (data == null && model && (method === 'create' || method === 'update' || method === 'patch')) {
158
+ data = options.attrs || model.toJSON();
159
+ }
160
+
161
+ const type = methodMap[method];
162
+ const params: RequestInit & { success?: any; error?: any } = {
163
+ method: type,
164
+ body: data ? JSON.stringify(data) : '',
165
+ headers: {
166
+ 'Content-Type': 'application/json',
167
+ },
168
+ success: options.success,
169
+ error: options.error,
170
+ };
171
+
172
+ const url = options.url || result(model, 'url') || urlError();
173
+ const xhr = fetch(url, params);
174
+ if (options) {
175
+ options.xhr = xhr;
176
+ }
177
+ model.trigger('request', model, xhr, { ...params, xhr });
178
+ return xhr;
179
+ }