UNPKG

@randajan/vault-kit

Version:

A tiny and universal data vault that behaves like a writable file system. Store, sync, forget. Works on client and server.

519 lines (508 loc) 15.5 kB
var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to2, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to2, key) && key !== except) __defProp(to2, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to2; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/react/index.js var react_exports = {}; __export(react_exports, { default: () => react_default, useVault: () => useVault }); module.exports = __toCommonJS(react_exports); // src/react/useVault.jsx var import_react = __toESM(require("react"), 1); // src/tools.js var is = (type, any) => typeof type === "string" ? typeof any === type : any instanceof type; var to = (type, any, errorName, req = false) => { if (is(type, any)) { return any; } if (!req && (any == null || !errorName)) { return; } throw new TypeError(`Expected ${errorName} to be a '${type}', got '${typeof any}'`); }; var toFn = (any, errorName, req = false) => to("function", any, errorName, req); var toObj = (any, errorName, req = false) => to("object", any, errorName, req); var toBol = (any, errorName, req = false) => to("boolean", any, errorName, req); var toStr = (any, errorName, req = false) => to("string", any, errorName, req); var toNum = (any, errorName, req = false) => to("number", any, errorName, req); var toRng = (any, min, max, errorName, req = false) => { const num = toNum(any, errorName, req); if (num == null) { return; } if (num < min) { throw new Error(`Expected ${errorName} to be greater than '${min}' got '${num}'`); } if (num > max) { throw new Error(`Expected ${errorName} to be lesser than '${max}' got '${num}'`); } return num; }; var timeout = (ms, msg = "Timeout") => new Promise((_, rej) => setTimeout((_2) => rej(new Error(msg)), ms)); var withTimeout = (fn, ms, msg = "Timeout") => { if (!fn || !ms) { return fn; } return (...args) => Promise.race([fn(...args), timeout(ms, msg)]); }; // src/class/Cell.js var Cell = class { constructor(_vault) { this.status = "init"; this._vault = _vault; } notDestroyed() { if (this.status !== "destroyed") { return true; } throw new Error("Was destroyed before"); } isExpired() { return this.expiresAt && this.expiresAt < Date.now(); } onSet(persistent, before, ...a) { const { _vault, status, data, error } = this; const { ttl, handlers } = _vault; if (persistent) { delete this.expiresAt; } else if (ttl > 0) { this.expiresAt = Date.now() + ttl; } return handlers.run({ status, data, error, before }, ...a); } async fetchRemote(status, data, ...a) { try { const { push, pull } = this._vault.remote; const prom = status === "push" ? push(data, ...a) : pull(...a); return this.setReady(status, await prom, ...a); } catch (err) { throw this.setError(err); } } setProm(status, data, ...a) { const { data: d, status: s } = this; this.status = status; this.prom = this.fetchRemote(status, data, ...a); delete this.error; this.onSet(true, { data: d, status: s }, ...a); return this.prom; } setError(err, ...a) { const { data: d, status: s } = this; this.status = "error"; this.error = err; delete this.prom; this.onSet(false, { data: d, status: s }, ...a); return err; } async setReady(mode, data, ...a) { const { _vault, data: d, status: s } = this; const { unfold, trait, purge } = _vault; let res = data; if (unfold && (mode === "local" || mode === "push")) { [data, res] = await unfold(data); } if (this.data != null) { purge(this.data); } this.data = await trait(data, res); this.status = "ready"; delete this.error; delete this.prom; this.onSet(false, { data: d, status: s }, ...a); return res; } pick() { return this.isExpired() ? this.reset("expired") : this; } async get(...a) { this.notDestroyed(); const { _vault, status, data, prom } = this.pick(); const { remote } = _vault; if (status === "ready") { return data; } if (!remote) { return; } if (status === "pull") { return prom; } if (status === "push") { await prom; return this.data; } return this.setProm("pull", void 0, ...a); } async set(data, ...a) { this.notDestroyed(); const { _vault, prom } = this; const { remote, act } = _vault; if (!remote) { return this.setReady("local", await act(data, ...a), ...a); } const push = async (_) => this.setProm("push", await act(data, ...a), ...a); return !prom ? push() : this.prom = this.prom.then(push); } reset(status, ...a) { this.notDestroyed(); const { _vault, data: d, status: s } = this; const { purge } = _vault; try { if (this.data != null) { purge(this.data); } } catch (err) { throw this.setError(err); } this.status = status; delete this.data; delete this.prom; delete this.error; delete this.expiresAt; this.onSet(true, { status: s, data: d }, ...a); return this; } }; // src/class/Cells.js var Cells = class extends Map { constructor(_vault) { super(); this._vault = _vault; } pick(id) { const c = super.get(id); return c?.isExpired() ? this.reset("expired", id) : c; } async get(id, ...a) { const { _vault } = this; let c = this.pick(id); if (!c) { if (!_vault.remote) { return; } super.set(id, c = new Cell(_vault)); } return c.get(id, ...a); } ensure(id) { let c = super.get(id); if (!c) { super.set(id, c = new Cell(this._vault)); } return c; } async setReady(mode, data, id, ...a) { return this.ensure(id).setReady(mode, data, id, ...a); } async set(data, id, ...a) { return this.ensure(id).set(data, id, ...a); } reset(status, id, ...a) { const c = super.get(id); super.delete(id); return c?.reset(status, id, ...a); } }; // src/class/Handlers.js var Handlers = class { constructor(emitter) { this.list = []; const run = (...args) => { for (const fn of [...this.list]) { fn(...args); } }; this.run = !emitter ? run : (...a) => emitter(run, ...a); } on(fn) { fn = toFn(fn, "fn argument", true); const { list } = this; list.push(fn); return () => { const x = list.indexOf(fn); if (x >= 0) { list.splice(x, 1); } }; } once(fn) { let rem; return rem = this.on((...a) => { rem(); return fn(...a); }); } }; // src/class/VaultPrivate.js var formatRemote = (remote, reqPreserveActions = false) => { const m = "options.remote"; remote = toObj(remote, m); if (!remote) { return; } const timeout2 = remote.timeout = toRng(remote.timeout, 0, 2147483647, m + ".timeout") ?? 5e3; remote.init = toFn(remote.init, m + ".init"); remote.push = withTimeout(toFn(remote.push, m + ".push"), timeout2); remote.pull = withTimeout(toFn(remote.pull, m + ".pull", true), timeout2); remote.preserveAction = toBol(remote.preserveAction, m + ".preserveAction", reqPreserveActions); remote.destroy = toFn(remote.destroy, m + ".destroy"); return remote; }; var formatUnfold = (fold, errorName, req = false) => { const f = toFn(fold); if (f) { return f; } let prop = toStr(fold, errorName); if (prop) { return (r) => [r[prop], r]; } if (req) { return (r) => [r, r]; } }; var formatActions = (actions, preserveAction) => { if (!actions) { return (req) => req; } return async (req, ...a) => { const { action, params } = toObj(req, "request", true); if (!action) { throw new Error(`Action is required`); } const act = actions[action]; if (!act) { throw new Error(`Action '${action}' is not defined`); } const data = await act(params, ...a); return preserveAction ? { action, params: data } : data; }; }; var VaultPrivate = class { constructor(opt) { opt = toObj(opt, "options") || {}; const actions = toObj(opt.actions, "options.actions"); const remote = this.remote = formatRemote(opt.remote, actions); const hasMany = this.hasMany = toBol(opt.hasMany, "options.hasMany") || false; this.readonly = remote && !remote.push || toBol(opt.readonly, "options.readonly") || false; const ttl = this.ttl = toRng(opt.ttl, 0, 2147483647 * 2, "options.ttl") ?? 0; this.act = formatActions(actions, remote?.preserveAction); this.unfold = formatUnfold(opt.unfold, "options.unfold"); this.trait = toFn(opt.trait, "options.trait") || ((data) => data); this.purge = toFn(opt.purge, "options.purge") || ((data) => data); this.handlers = new Handlers(toFn(opt.emitter, "options.emitter")); const store = this.store = hasMany ? new Cells(this) : new Cell(this); if (remote?.init) { this.initDestroy = remote.init((...a) => store.setReady("remote", ...a)); } if (remote?.destroy) { this.destroy = (_) => remote.destroy(this.initDestroy); } else { this.destroy = toFn(this.initDestroy, "remote.init return value") || (() => { }); } if (!ttl) { return; } const cleanUp = !hasMany ? (_) => store.pick() : (_) => { for (const id of store.keys()) { store.pick(id); } }; setInterval(cleanUp, ttl / 2); } reset(status, ...a) { const { store } = this; store.reset(status, ...a); } resetAll(status, ...a) { if (!this.hasMany) { this.reset(status, ...a); } else { this.forEach((ctx, id) => this.reset(status, id, ...a)); } } }; // src/class/Vault.js var _privates = /* @__PURE__ */ new WeakMap(); var Vault = class { constructor(options = {}) { const _p = new VaultPrivate(options); const enumerable = true; Object.defineProperty(this, "hasMany", { enumerable, value: _p.hasMany }); Object.defineProperty(this, "hasRemote", { enumerable, value: !!_p.remote }); const act = this.act.bind(this); this.act = this.withActions(act, act); _privates.set(this, _p); } getStatus(...a) { return _privates.get(this).store.pick(...a)?.status || "init"; } getData(...a) { return _privates.get(this).store.pick(...a)?.data; } getError(...a) { return _privates.get(this).store.pick(...a)?.error; } isStatus(statuses, ...a) { const s = this.getStatus(...a); return Array.isArray(statuses) ? statuses.includes(s) : s === statuses; } async get(...a) { return _privates.get(this).store.get(...a); } async set(data, ...a) { const { readonly, store } = _privates.get(this); if (readonly) { throw new Error(`Set is not allowed`); } return store.set(data, ...a); } async act(action, params, ...a) { const { readonly, store } = _privates.get(this); if (readonly) { throw new Error(`Do is not allowed`); } return store.set({ action, params }, ...a); } reset(...a) { _privates.get(this).reset("init", ...a); return this; } resetAll(...a) { _privates.get(this).resetAll("init", ...a); return this; } destroy(...a) { const _vault = _privates.get(this); _vault.resetAll("destroyed", ...a); _vault.destroy(); return this; } on(fn) { return _privates.get(this).handlers.on(fn); } once(fn) { return _privates.get(this).handlers.once(fn); } async has(...a) { const data = await this.get(...a); return data !== void 0; } forEach(exe) { const { store, hasMany } = _privates.get(this); if (!hasMany) { const { status, data } = store.pick(); return exe({ status, data }); } let proms; for (const id of [...store.keys()]) { const { status, data } = store.pick(id); if (status === "init") { continue; } const res = exe({ status, data }, id); if (res instanceof Promise) { (proms ??= []).push(res); } } if (proms) { return Promise.all(proms); } } collect(collector, exe) { const res = this.forEach((ctx, id) => exe(collector, ctx, id)); return res instanceof Promise ? res.then((_) => collector) : collector; } withActions(target, execute) { const d = toFn(execute, "second argument") || this.act; return new Proxy(target, { get(t, prop, receiver) { const val = Reflect.get(t, prop, receiver); if (val !== void 0) { return val; } return (params, ...a) => d(prop, params, ...a); } }); } }; // src/react/useVault.jsx var validateVault = (vault) => { if (!(vault instanceof Vault)) { throw Error("vault must be instance of Vault"); } }; var createPort = (vault, redraw, ...a) => { validateVault(vault); let reply, fallback; const d = async (action, params, ...e) => reply = await vault.act(action, params, ...a, ...e); const enumerable = true; const port = Object.defineProperties({}, { status: { enumerable, get: () => vault.getStatus(...a) }, data: { enumerable, get: () => vault.getData(...a) || fallback }, error: { enumerable, get: () => vault.getError(...a) }, reply: { enumerable, get: () => reply }, confirm: { value: () => { reply = void 0; redraw(Symbol()); } }, set: { value: async (data, ...e) => reply = await vault.set(data, ...a, ...e) }, act: { value: vault.withActions(d, d) }, isStatus: { value: (statuses, ...e) => vault.isStatus(statuses, ...a, ...e) } }); const cleanUp = vault.on((ctx, ...e) => { for (const i in a) { if (e[i] != a[i]) { return; } } const { status, before } = ctx; if (vault.hasRemote) { if (status === "init" || status === "expired") { fallback = before.data; vault.get(...a); } else if (status === "ready" || status === "error" || status === "destroyed") { fallback = void 0; } } redraw(Symbol()); }); vault.get(...a); return [port, cleanUp]; }; var useVault = (vault, ...a) => { const redraw = (0, import_react.useState)(0)[1]; const [port, cleanUp] = (0, import_react.useMemo)((_) => createPort(vault, redraw, ...a), [vault, ...a]); (0, import_react.useEffect)((_) => cleanUp, [cleanUp]); return port; }; // src/react/index.js var react_default = useVault; //# sourceMappingURL=index.js.map