🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@kuindji/reactive

Package Overview
Dependencies
Maintainers
1
Versions
28
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@kuindji/reactive - npm Package Compare versions

Comparing version
1.2.0
to
1.3.0
+9
-6
dist/action.js

@@ -148,8 +148,11 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {

lastResponse = null;
// Handle the error if listeners existed at invoke start OR were
// registered while the invocation was in flight. The start-of-invoke
// snapshot is retained (rather than relying solely on the live check)
// so that destroy() tearing the listeners down mid-flight cannot flip
// a previously-handled failure into a rejection.
if (!handlesErrors && !hasErrorListeners()) {
// Re-throw unless the failure is handled. It is handled when a live
// error listener exists now, OR when the action was destroyed
// mid-flight after starting with listeners: destroy() tears the
// listeners down, and that teardown must not flip a previously-handled
// failure into a rejection. The start-of-invoke snapshot is scoped to
// the destroyed case on purpose — an ordinary removeErrorListener()
// while awaiting genuinely leaves the failure unhandled, so invoke()
// must reject rather than silently swallow it.
if (!hasErrorListeners() && !(destroyed && handlesErrors)) {
throw error;

@@ -156,0 +159,0 @@ }

@@ -18,2 +18,16 @@ import type { EventOptions, ListenerOptions } from "../event.js";

/**
* Expand a listener's options into a fully-populated set of the soft fields
* that {@link ListenerOptions} reconciliation updates in place, filling every
* omitted field with its default.
*
* `event.updateListenerOptions` uses partial-merge semantics (only fields
* present in the passed object change), but the React reconciliation layer is
* declarative: the options object fully describes the desired listener state,
* so a field dropped between renders must reset to its default. Passing this
* normalized object makes partial-merge behave as a full reset for exactly the
* soft fields — without disturbing `signal` (identity-managed by the abort
* wiring) or `context` (identity, handled by resubscribe).
*/
export declare function fillListenerUpdateDefaults(options?: ListenerOptions | null): ListenerOptions;
/**
* Domain-specific comparator for {@link EventOptions}. Primitives compare after

@@ -20,0 +34,0 @@ * default semantics; `filter`/`filterContext` compare by reference.

@@ -74,2 +74,28 @@ function normalizeAsync(value) {

/**
* Expand a listener's options into a fully-populated set of the soft fields
* that {@link ListenerOptions} reconciliation updates in place, filling every
* omitted field with its default.
*
* `event.updateListenerOptions` uses partial-merge semantics (only fields
* present in the passed object change), but the React reconciliation layer is
* declarative: the options object fully describes the desired listener state,
* so a field dropped between renders must reset to its default. Passing this
* normalized object makes partial-merge behave as a full reset for exactly the
* soft fields — without disturbing `signal` (identity-managed by the abort
* wiring) or `context` (identity, handled by resubscribe).
*/
export function fillListenerUpdateDefaults(options) {
var _a, _b, _c, _d, _e, _f, _g;
const o = options !== null && options !== void 0 ? options : {};
return {
limit: (_a = o.limit) !== null && _a !== void 0 ? _a : 0,
start: (_b = o.start) !== null && _b !== void 0 ? _b : 1,
tags: (_c = o.tags) !== null && _c !== void 0 ? _c : [],
extraData: (_d = o.extraData) !== null && _d !== void 0 ? _d : null,
alwaysFirst: (_e = o.alwaysFirst) !== null && _e !== void 0 ? _e : false,
alwaysLast: (_f = o.alwaysLast) !== null && _f !== void 0 ? _f : false,
async: (_g = o.async) !== null && _g !== void 0 ? _g : null,
};
}
/**
* Domain-specific comparator for {@link EventOptions}. Primitives compare after

@@ -76,0 +102,0 @@ * default semantics; `filter`/`filterContext` compare by reference.

+1
-1
import type { ActionStatus } from "../action.js";
import type { BaseActionBus } from "../actionBus.js";
import type { KeyOf } from "../lib/types.js";
import type { AsyncActionState } from "./useAsyncAction.js";
import { type AsyncActionState } from "./useAsyncAction.js";
export type { ActionStatus, AsyncActionState };

@@ -6,0 +6,0 @@ /**

import { useCallback, useSyncExternalStore } from "react";
import { toAsyncActionState } from "./useAsyncAction.js";
/**

@@ -21,7 +22,3 @@ * Subscribes to the status of a named action on an ActionBus and returns

const status = useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
return {
loading: status.pending,
error: status.error,
response: status.response,
};
return toAsyncActionState(status);
}

@@ -10,2 +10,8 @@ import type { ActionResponse, ActionStatus } from "../action.js";

/**
* Project an action's {@link ActionStatus} into the {@link AsyncActionState}
* shape consumed by the status hooks (`pending` -> `loading`). Shared so
* `useAsyncAction` and `useActionBusStatus` stay in lockstep.
*/
export declare function toAsyncActionState<Response>(status: ActionStatus<Response>): AsyncActionState<Response>;
/**
* Wraps a function in an action and exposes its in-flight status, so a

@@ -12,0 +18,0 @@ * component can drive `loading`/`disabled` without a hand-rolled

import { useCallback, useLayoutEffect, useMemo, useRef, useSyncExternalStore, } from "react";
import { createAction } from "../action.js";
/**
* Project an action's {@link ActionStatus} into the {@link AsyncActionState}
* shape consumed by the status hooks (`pending` -> `loading`). Shared so
* `useAsyncAction` and `useActionBusStatus` stay in lockstep.
*/
export function toAsyncActionState(status) {
return {
loading: status.pending,
error: status.error,
response: status.response,
};
}
/**
* Wraps a function in an action and exposes its in-flight status, so a

@@ -47,8 +59,4 @@ * component can drive `loading`/`disabled` without a hand-rolled

invoke,
{
loading: status.pending,
error: status.error,
response: status.response,
},
toAsyncActionState(status),
];
}
import { useEffect, useRef } from "react";
import { areListenerOptionsEqual } from "./listenerOptionsEqual.js";
import { areListenerOptionsEqual, fillListenerUpdateDefaults, } from "./listenerOptionsEqual.js";
/**

@@ -40,3 +40,12 @@ * Reconciles a single reactive listener across renders without relying on the

if (!areListenerOptionsEqual(committedRef.current, options)) {
update(context, options);
// Normalize to a full soft-option set so a field dropped since the
// last render resets to its default: updateListenerOptions is
// partial-merge, but the React path is declarative (see
// fillListenerUpdateDefaults). Carry `signal` through only when
// present (matching the old whole-options pass), so its abort wiring
// is rebound/cleared exactly as before; `context` is passed
// separately and not read from the options object.
update(context, Object.assign(Object.assign({}, fillListenerUpdateDefaults(options)), ((options === null || options === void 0 ? void 0 : options.signal) !== undefined
? { signal: options.signal }
: {})));
}

@@ -43,0 +52,0 @@ committedRef.current = options;

@@ -125,2 +125,11 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore, } from "react";

const subscribe = useCallback((onStoreChange) => {
// subscribe can run (during commit) for an already-destroyed store
// — e.g. a provider torn down before this component mounts, or a
// `store` change re-running subscribe. control() reaches the
// destroyed control bus's addListener, which throws "EventBus is
// destroyed" out of render. Skip subscribing and hand back a no-op
// cleanup; getSelection already reads destroyed stores safely.
if (store.isDestroyed()) {
return () => { };
}
const listener = (names) => {

@@ -127,0 +136,0 @@ const currentDeps = depsRef.current;

import { useCallback, useSyncExternalStore } from "react";
export function useStoreState(store, key) {
const subscribe = useCallback((onStoreChange) => {
// subscribe can run (during commit) for a store that was already
// destroyed — e.g. a provider torn down before this component
// mounts, or a `key`/`store` change re-running subscribe. onChange
// reaches the destroyed changes event's addListener, which throws
// "Event is destroyed" out of render. Skip subscribing and hand back
// a no-op cleanup; getSnapshot already reads destroyed stores safely.
if (store.isDestroyed()) {
return () => { };
}
const listener = () => {

@@ -5,0 +14,0 @@ onStoreChange();

@@ -83,9 +83,16 @@ import { createEventBus } from "./eventBus.js";

};
const _set = (name, value, triggerChange = true) => {
const _set = (name, value, triggerChange = true, runBeforeChange = true) => {
var _a, _b, _c, _d, _e;
const prev = data.get(name);
if (prev !== value) {
const beforeChangeResults = control.all(BeforeChangeEventName, name, value);
if (beforeChangeResults.some((result) => result === false)) {
return;
// A computed recompute skips the beforeChange veto (runBeforeChange
// false): the initial computed seed() bypasses beforeChange for the
// same reason, so allowing a veto here would leave the derived value
// stale and no longer equal to fn(deps) — internally inconsistent
// with the seeded value. beforeChange still gates ordinary sets.
if (runBeforeChange) {
const beforeChangeResults = control.all(BeforeChangeEventName, name, value);
if (beforeChangeResults.some((result) => result === false)) {
return;
}
}

@@ -203,2 +210,18 @@ const pipeArgs = [value];

function asyncSet(name, value) {
// Validate computed keys synchronously, mirroring set(). Deferring the
// check to the timer callback would turn a misuse into an uncaught
// exception escaping the timer (crashing Node / surfacing as an uncaught
// browser error) instead of a catchable throw at the call site.
if (typeof name === "string") {
if (computedKeys.has(name)) {
throw new Error(`Cannot set computed property "${name}"`);
}
}
else if (typeof name === "object" && name !== null) {
for (const k of Object.keys(name)) {
if (computedKeys.has(k)) {
throw new Error(`Cannot set computed property "${k}"`);
}
}
}
const timer = setTimeout(() => {

@@ -219,8 +242,10 @@ pendingTimers.delete(timer);

}
// Replay a coalesced change log: one onChange per key, keeping the first
// entry's pre-cascade `prev` and the last entry's settled `value`, dropping
// keys whose net value is unchanged. Mirrors batch()'s replay (including its
// store-change error routing) so a computed touched several times during a
// cascade emits a single, internally-consistent onChange.
const replayCoalescedChanges = (log, hasCallbackError, callbackError) => {
// Coalesce a change log per key and replay it as one onChange each, keeping
// the first entry's pre-cascade `prev` and the last entry's settled `value`
// and dropping keys whose net value is unchanged. Store-change errors route
// to the error event; an unhandled one propagates unless the surrounding
// callback already failed (`hasCallbackError`), in which case it is swallowed
// so the original callback error is the one that ultimately surfaces. Shared
// by batch() and the single-set cascade wrapper so both coalesce identically.
const replayCoalescedLog = (log, hasCallbackError) => {
var _a;

@@ -266,2 +291,7 @@ const coalesced = new Map();

}
};
// Replay a coalesced cascade log and, if the driving callback failed, rethrow
// its error last (after every surviving onChange has fired).
const replayCoalescedChanges = (log, hasCallbackError, callbackError) => {
replayCoalescedLog(log, hasCallbackError);
if (hasCallbackError) {

@@ -468,3 +498,3 @@ throw callbackError;

const batch = (fn) => {
var _a, _b;
var _a;
if (batching) {

@@ -506,44 +536,6 @@ throw new Error("Nested batch() calls are not supported");

// write) must fire onChange once with its final value, not once per
// intermediate value. Keep first-occurrence order, the pre-batch `prev`
// from the first entry, and the final `value` from the last entry; drop
// keys whose net value is unchanged from before the batch.
const coalesced = new Map();
for (const [propName, value, prev] of log) {
const existing = coalesced.get(propName);
if (existing) {
existing.value = value;
}
else {
coalesced.set(propName, { value, prev });
}
}
for (const [propName, { value, prev }] of coalesced) {
if (value === prev) {
continue;
}
const changeArgs = [
value,
prev,
];
try {
changes.trigger(propName, ...changeArgs);
}
catch (error) {
control.trigger(ErrorEventName, {
error: error instanceof Error
? error
: new Error(String(error)),
args: changeArgs,
type: "store-change",
name: propName,
});
if ((_a = control.get(ErrorEventName)) === null || _a === void 0 ? void 0 : _a.hasListener()) {
continue;
}
if (hasCallbackError) {
continue;
}
throw error;
}
}
// intermediate value. Drop keys whose net value is unchanged from before
// the batch. The callback error (if any) is deferred and rethrown at the
// end so the partial-write control change event below still fires.
replayCoalescedLog(log, hasCallbackError);
// Dedupe so the control change event lists each key once, matching the

@@ -565,3 +557,3 @@ // non-batch path (which dedupes via `dedupe([name, ...effectKeys])`).

});
if ((_b = control.get(ErrorEventName)) === null || _b === void 0 ? void 0 : _b.hasListener()) {
if ((_a = control.get(ErrorEventName)) === null || _a === void 0 ? void 0 : _a.hasListener()) {
if (hasCallbackError) {

@@ -680,3 +672,6 @@ throw callbackError;

try {
_set(key, fn(...depValues));
// runBeforeChange=false: a computed key is derived and must
// always hold fn(deps); a beforeChange veto here would strand a
// stale value (see _set and the seed() rationale).
_set(key, fn(...depValues), true, false);
}

@@ -683,0 +678,0 @@ finally {

{
"name": "@kuindji/reactive",
"version": "1.2.0",
"version": "1.3.0",
"author": "Ivan Kuindzhi",

@@ -5,0 +5,0 @@ "type": "module",