UNPKG

@harlem/core

Version:

Powerfully simple global state management for Vue 3

497 lines (493 loc) 12.7 kB
// src/event-emitter.ts function createEventBus() { const listeners = /* @__PURE__ */ new Map(); function on2(event, handler) { const handlers = listeners.get(event) || /* @__PURE__ */ new Set(); handlers.add(handler); listeners.set(event, handlers); return { dispose: () => off2(event, handler) }; } function off2(event, handler) { const handlers = listeners.get(event); if (!handlers) { return; } handlers.delete(handler); if (!handlers.size) { listeners.delete(event); } } function once2(event, handler) { const callback = (payload) => { handler(payload); off2(event, callback); }; return on2(event, callback); } function emit(event, payload) { const handlers = listeners.get(event); if (handlers) { handlers.forEach((handler) => handler(payload)); } } return { on: on2, off: off2, once: once2, emit }; } // src/constants.ts import { objectClone } from "@harlem/utilities"; var SENDER = "core"; var EVENTS = { core: { installed: "core:installed" }, store: { created: "store:created", ready: "store:ready", destroyed: "store:destroyed" }, mutation: { before: "mutation:before", after: "mutation:after", success: "mutation:success", error: "mutation:error" }, action: { before: "action:before", after: "action:after", success: "action:success", error: "action:error" }, ssr: { initServer: "ssr:init:server", initClient: "ssr:init:client" }, devtools: { update: "devtools:update", reset: "devtools:reset" } }; var MUTATIONS = { snapshot: "core:snapshot", reset: "core:reset" }; var PRODUCERS = { read: (value) => value, write: (value) => value, payload: (value) => objectClone(value) }; var INTERNAL = { prefix: "$harlem:", pattern: /^\$harlem:/ }; // src/store.ts import { computed, effectScope, reactive, readonly } from "vue"; import { functionIdentity, objectClone as objectClone2, objectFromPath, objectSet, objectTrace } from "@harlem/utilities"; function localiseHandler(name, handler) { return (payload) => { if (payload && payload.store === name) { handler(payload); } }; } function createInternalStore(name, initialState, eventBus, options) { var _a; const { allowsOverwrite, producers } = { allowsOverwrite: true, ...options, producers: { ...PRODUCERS, ...options == null ? void 0 : options.producers } }; const registrations = {}; const flags = /* @__PURE__ */ new Map(); const scope = effectScope(); const stack = /* @__PURE__ */ new Set(); let isSuppressing = false; const writeState = reactive(initialState); const readState = readonly(writeState); let resetSnapshot; function emit(event, sender, data) { if (!scope.active || isSuppressing) { return; } const payload = { data, sender, store: name }; eventBus.emit(event, payload); } function on2(event, handler) { return eventBus.on(event, localiseHandler(name, handler)); } function once2(event, handler) { return eventBus.once(event, localiseHandler(name, handler)); } function track(callback) { return scope.run(callback); } function hasRegistration(group, name2) { var _a2; return !!((_a2 = registrations[group]) == null ? void 0 : _a2.has(name2)); } function getRegistration(group, name2) { var _a2; return (_a2 = registrations[group]) == null ? void 0 : _a2.get(name2); } function register(group, name2, producer, type = "other") { if (!name2) { throw new Error("Registration name cannot be empty"); } if (!(group in registrations)) { registrations[group] = /* @__PURE__ */ new Map(); } if (!allowsOverwrite && hasRegistration(group, name2)) { throw new Error(`A ${group} named ${name2} has already been registered on this store`); } registrations[group].set(name2, { type, producer }); } function unregister(group, name2) { var _a2; (_a2 = registrations[group]) == null ? void 0 : _a2.delete(name2); } function suppress(callback) { isSuppressing = true; try { return callback(); } finally { isSuppressing = false; } } function getter(name2, getter2) { const output = track(() => computed(() => getter2(readState))); register("getters", name2, () => output.value, "computed"); return output; } function mutate(name2, sender, mutator, payload) { var _a2, _b; if (!scope.active) { throw new Error("The current store has been destroyed. Mutations can no longer take place."); } if (stack.has(name2)) { throw new Error("Circular mutation reference detected. Avoid calling mutations inside other mutations to prevent circular references."); } stack.add(name2); let result; const trigger = (event) => emit(event, sender, { name: name2, payload, result }); trigger(EVENTS.mutation.before); try { const producedState = (_a2 = producers.write(writeState)) != null ? _a2 : writeState; const producedPayload = (_b = producers.payload(payload)) != null ? _b : payload; result = mutator(producedState, producedPayload); trigger(EVENTS.mutation.success); } catch (error) { trigger(EVENTS.mutation.error); throw error; } finally { stack.delete(name2); trigger(EVENTS.mutation.after); } return result; } function mutation(name2, mutator) { const mutation2 = (payload) => { return mutate(name2, SENDER, mutator, payload); }; register("mutations", name2, () => mutation2); return mutation2; } function action(name2, body) { const mutate2 = (mutator) => write(name2, SENDER, mutator); const action2 = async (payload) => { var _a2; let result; const trigger = (event) => emit(event, SENDER, { name: name2, payload, result }); trigger(EVENTS.action.before); try { const producedPayload = (_a2 = producers.payload(payload)) != null ? _a2 : payload; result = await body(producedPayload, mutate2); trigger(EVENTS.action.success); } catch (error) { trigger(EVENTS.action.error); throw error; } finally { trigger(EVENTS.action.after); } return result; }; register("actions", name2, () => action2); return action2; } function snapshot() { const snapshot2 = objectClone2(initialState); const { value, getNodes, resetNodes } = objectTrace(); const apply = (branchAccessor = functionIdentity, mutationName = MUTATIONS.snapshot) => { write(mutationName, SENDER, (state2) => { if (!snapshot2) { return console.warn("Couldn't find snapshot for this operation!"); } resetNodes(); branchAccessor(value); const nodes = getNodes(); const source = objectFromPath(snapshot2, nodes); objectSet(state2, nodes, objectClone2(source)); }); }; return { apply, get state() { return objectClone2(snapshot2); } }; } function reset(branchAccessor = functionIdentity) { resetSnapshot == null ? void 0 : resetSnapshot.apply(branchAccessor, MUTATIONS.reset); } function write(name2, sender, mutator, suppressEvent) { const mutation2 = () => mutate(name2, sender, mutator, void 0); return suppressEvent ? suppress(mutation2) : mutation2(); } function destroy() { scope.stop(); } once2(EVENTS.store.ready, () => resetSnapshot = snapshot()); on2(EVENTS.devtools.reset, () => reset()); const state = (_a = producers.read(readState)) != null ? _a : readState; return { name, allowsOverwrite, flags, producers, registrations, on: on2, once: once2, emit, state, getter, mutation, action, write, snapshot, reset, register, unregister, hasRegistration, getRegistration, track, suppress, destroy }; } // src/index.ts import { matchGetFilter, objectLock, typeIsFunction, typeIsMatchable } from "@harlem/utilities"; function createInstance() { const eventBus = createEventBus(); const stores = /* @__PURE__ */ new Map(); let installed = false; function validateStoreCreation(name) { const store = stores.get(name); if (store && !store.allowsOverwrite) { throw new Error(`A store named ${name} has already been registered.`); } } function emitCreated(store, state) { const created = () => { store.emit(EVENTS.ssr.initClient, SENDER, state); store.emit(EVENTS.store.created, SENDER, state); store.emit(EVENTS.ssr.initServer, SENDER, state); store.emit(EVENTS.store.ready, SENDER, state); store.emit(EVENTS.devtools.update, SENDER, state); }; if (installed) { return created(); } eventBus.once(EVENTS.core.installed, created); } function getExtensionApis(store, extensions) { return extensions.reduce((output, extension) => { let result = {}; try { result = extension(store) || {}; } catch (e) { result = {}; } return { ...output, ...result }; }, {}); } function installPlugin(plugin, app) { if (!typeIsFunction(plugin)) { return; } const lockedStores = objectLock(stores, [ "set", "delete", "clear" ]); try { plugin(app, eventBus, lockedStores); } catch (error) { console.warn("Failed to install Harlem plugin. Skipping."); } } function createStore2(name, state, options) { const { allowsOverwrite, producers: providers, extensions } = { allowsOverwrite: true, extensions: [], ...options }; validateStoreCreation(name); const store = createInternalStore(name, state, eventBus, { allowsOverwrite, producers: providers }); const destroy = () => { stores.delete(name); store.destroy(); store.emit(EVENTS.store.destroyed, SENDER, state); store.emit(EVENTS.devtools.update, SENDER, state); }; const getTrigger = (eventName) => { return (matcher, handler) => { const filter = matchGetFilter( typeIsMatchable(matcher) ? matcher : { include: matcher } ); return store.on(eventName, (event) => { if (event && filter(event.data.name)) { handler(event.data); } }); }; }; const onBeforeMutation = getTrigger(EVENTS.mutation.before); const onAfterMutation = getTrigger(EVENTS.mutation.after); const onMutationSuccess = getTrigger(EVENTS.mutation.success); const onMutationError = getTrigger(EVENTS.mutation.error); const onBeforeAction = getTrigger(EVENTS.action.before); const onAfterAction = getTrigger(EVENTS.action.after); const onActionSuccess = getTrigger(EVENTS.action.success); const onActionError = getTrigger(EVENTS.action.error); const extensionApis = getExtensionApis(store, extensions); stores.set(name, store); emitCreated(store, state); return { destroy, onBeforeMutation, onAfterMutation, onMutationSuccess, onMutationError, onBeforeAction, onAfterAction, onActionSuccess, onActionError, state: store.state, getter: store.getter.bind(store), mutation: store.mutation.bind(store), action: store.action.bind(store), snapshot: store.snapshot.bind(store), reset: store.reset.bind(store), suppress: store.suppress.bind(store), on: store.on.bind(store), once: store.once.bind(store), ...extensionApis }; } function createVuePlugin2(options) { return { install(app) { const { plugins } = { plugins: [], ...options }; if (plugins) { plugins.forEach((plugin) => installPlugin(plugin, app)); } installed = true; eventBus.emit(EVENTS.core.installed); } }; } return { createVuePlugin: createVuePlugin2, createStore: createStore2, on: eventBus.on, once: eventBus.once, off: eventBus.off }; } var { on, off, once, createVuePlugin, createStore } = createInstance(); if (typeof window !== "undefined") { window.$harlem = { createInstance }; } export { EVENTS, INTERNAL, PRODUCERS, createInstance, createStore, createVuePlugin, off, on, once };