summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/shapex.test.ts89
-rw-r--r--src/shapex.ts93
2 files changed, 129 insertions, 53 deletions
diff --git a/src/shapex.test.ts b/src/shapex.test.ts
index aadeea7..d605c3a 100644
--- a/src/shapex.test.ts
+++ b/src/shapex.test.ts
@@ -6,6 +6,7 @@ describe("EventX", () => {
test("subscribes to an event", () => {
const $ = ShapeX({ counter: 1 });
const id = $.subscribe("test-event", (state) => ({ state }));
+
expect(id).toBe(1);
expect($.subscriptionCount("test-event")).toBe(1);
});
@@ -13,16 +14,17 @@ describe("EventX", () => {
test("subscribes to an event once", () => {
const $ = ShapeX({ counter: 1 });
const id = $.subscribeOnce("test-event", (state) => ({ state }));
+
expect(id).toBe(1);
expect($.subscriptionCount("test-event")).toBe(1);
- // Dispatch the event to trigger the one-time subscription
$.dispatch("test-event");
expect($.subscriptionCount("test-event")).toBe(0);
});
test("unsubscribes from an event", () => {
const $ = ShapeX({ counter: 1 });
+
$.subscribe("test-event", (state) => ({ state }));
expect($.subscriptionCount("test-event")).toBe(1);
@@ -58,7 +60,8 @@ describe("EventX", () => {
const $ = ShapeX({ counter: 1 });
const stateChangeSpy = vi.fn((state) => ({ state }));
- $.subscribe("$counter", stateChangeSpy);
+ $.subscribe("$.counter", stateChangeSpy);
+
$.subscribe("increment", (state) => ({
state: { ...state, counter: state.counter + 1 },
}));
@@ -74,6 +77,7 @@ describe("EventX", () => {
const nestedEventSpy = vi.fn((state) => ({ state }));
$.subscribe("nested-event", nestedEventSpy);
+
$.subscribe("parent-event", (state) => ({
state,
dispatch: { eventName: "nested-event" },
@@ -90,7 +94,9 @@ describe("EventX", () => {
const nestedEvent2Spy = vi.fn((state) => ({ state }));
$.subscribe("nested-event-1", nestedEvent1Spy);
+
$.subscribe("nested-event-2", nestedEvent2Spy);
+
$.subscribe("parent-event", (state) => ({
state,
dispatch: [{ eventName: "nested-event-1" }, { eventName: "nested-event-2" }],
@@ -107,6 +113,7 @@ describe("EventX", () => {
const nestedEventSpy = vi.fn((state, arg) => ({ state }));
$.subscribe("nested-event", nestedEventSpy);
+
$.subscribe("parent-event", (state) => ({
state,
dispatch: { eventName: "nested-event", args: ["arg-value"] },
@@ -124,7 +131,8 @@ describe("EventX", () => {
const $ = ShapeX({ counter: 1, nested: { value: "test" } });
const counterChangeSpy = vi.fn((state) => ({ state }));
- $.subscribe("$counter", counterChangeSpy);
+ $.subscribe("$.counter", counterChangeSpy);
+
$.subscribe("change-counter", (state) => ({
state: { ...state, counter: 2 },
}));
@@ -139,28 +147,90 @@ describe("EventX", () => {
const $ = ShapeX({ counter: 1, nested: { value: "test" } });
const nestedValueChangeSpy = vi.fn((state) => ({ state }));
- $.subscribe("$nested.value", nestedValueChangeSpy);
+ $.subscribe("$.nested.value", nestedValueChangeSpy);
+
$.subscribe("change-nested-value", (state) => ({
state: {
...state,
- nested: { ...state.nested, value: "changed" },
+ nested: { ...state.nested, value: "new value" },
+ },
+ }));
+
+ $.subscribe("change-nested-value-again", (state) => ({
+ state: {
+ ...state,
+ nested: { ...state.nested, value: "new value again" },
},
}));
$.dispatch("change-nested-value");
+ $.dispatch("change-nsted-value-again");
expect(nestedValueChangeSpy).toHaveBeenCalledTimes(1);
expect(nestedValueChangeSpy).toHaveBeenCalledWith({
counter: 1,
- nested: { value: "changed" },
+ nested: { value: "new value" },
+ });
+ });
+
+ test("detects addition in state", () => {
+ const $ = ShapeX({} as { view?: string });
+ const additionChangeSpy = vi.fn((state) => ({ state }));
+
+ $.subscribe("$.view", additionChangeSpy);
+
+ $.subscribe("set-view", (state) => {
+ return {
+ state: {
+ ...state,
+ view: "test",
+ },
+ };
+ });
+
+ $.dispatch("set-view");
+
+ expect(additionChangeSpy).toHaveBeenCalledTimes(1);
+ });
+
+ test("detects nested addition in state", () => {
+ const $ = ShapeX({} as { nested?: { value?: string } });
+ const additionChangeSpy = vi.fn((state) => ({ state }));
+ const additionChangeSpy2 = vi.fn((state) => ({ state }));
+
+ $.subscribe("$.nested", additionChangeSpy);
+ $.subscribe("$.nested.value", additionChangeSpy2);
+
+ $.subscribe("set-nested-value", (state) => {
+ return {
+ state: {
+ ...state,
+ nested: { value: "test" },
+ },
+ };
+ });
+
+ $.subscribe("set-nested-value-again", (state) => {
+ return {
+ state: {
+ ...state,
+ nested: { value: "test-again" },
+ },
+ };
});
+
+ $.dispatch("set-nested-value");
+ $.dispatch("set-nested-value-again");
+
+ expect(additionChangeSpy).toHaveBeenCalledTimes(2);
+ expect(additionChangeSpy2).toHaveBeenCalledTimes(2);
});
test("detects deleted properties in state", () => {
const $ = ShapeX({ counter: 1, toDelete: "value" } as { counter: number; toDelete?: string });
const deleteChangeSpy = vi.fn((state) => ({ state }));
- $.subscribe("$toDelete", deleteChangeSpy);
+ $.subscribe("$.toDelete", deleteChangeSpy);
$.subscribe("delete-property", (state) => {
const newState = { counter: state.counter };
return { state: newState };
@@ -175,7 +245,7 @@ describe("EventX", () => {
const $ = ShapeX({ counter: 1 } as { counter: string | number });
const counterChangeSpy = vi.fn((state) => ({ state }));
- $.subscribe("$counter", counterChangeSpy);
+ $.subscribe("$.counter", counterChangeSpy);
$.subscribe("change-counter-type", (state) => ({
state: { ...state, counter: "string now" },
}));
@@ -189,10 +259,12 @@ describe("EventX", () => {
describe("utility methods", () => {
test("returns all subscription names", () => {
const $ = ShapeX({ counter: 1 });
+
$.subscribe("event1", (state) => ({ state }));
$.subscribe("event2", (state) => ({ state }));
const subs = $.subscriptions();
+
expect(subs).toContain("event1");
expect(subs).toContain("event2");
expect(subs.length).toBe(2);
@@ -200,6 +272,7 @@ describe("EventX", () => {
test("returns subscription count for specific event", () => {
const $ = ShapeX({ counter: 1 });
+
$.subscribe("event1", (state) => ({ state }));
$.subscribe("event1", (state) => ({ state }));
$.subscribe("event2", (state) => ({ state }));
diff --git a/src/shapex.ts b/src/shapex.ts
index 80a6b11..2a0a3cc 100644
--- a/src/shapex.ts
+++ b/src/shapex.ts
@@ -20,7 +20,7 @@ type Subscription<T> = {
once: boolean;
};
-type StateChange = "deleted" | "changed-type" | "changed-value";
+type StateChange = "added" | "deleted" | "changed-type" | "changed-value";
type ShapeXInstance<T> = {
subscribe: (listener: string, callback: EventCallback<T>) => number;
@@ -95,54 +95,57 @@ export default function ShapeX<T extends object>(initialState: T): ShapeXInstanc
*
* @param {T extends object} oldState
* @param {T extends object} newState
- * @param {string} path
- * @returns {StateChange[]} The list of changes.
+ * @returns {string[]} The list of changes as array of paths.
*/
- const changedState = <T extends object>(
- oldState: T,
- newState: T,
- path: string = "",
- ): Map<string, StateChange> => {
- let changes: Map<string, StateChange> = new Map();
-
- for (const k in oldState) {
- const currentPath = path ? `${path}.${k}` : k;
-
- // Missing?
- if (!(k in newState)) {
- if (!changes.has(currentPath)) {
- changes.set(currentPath, "deleted");
- }
- }
+ const changedState = <T extends object>(oldState: T, newState: T): string[] => {
+ const paths = <R extends object>(
+ state: R,
+ path: string,
+ ): { path: string; value: unknown }[] => {
+ let _paths = [];
+
+ for (const key in state) {
+ const currentPath = `${path}.${key}`;
+ _paths.push({
+ path: currentPath,
+ value: state[key],
+ });
- // Type changed?
- if (typeof oldState[k] !== typeof newState[k]) {
- if (!changes.has(currentPath)) {
- changes.set(currentPath, "changed-type");
+ if (typeof state[key] === "object" && state[key] !== null) {
+ _paths.push(...paths(state[key], currentPath));
}
}
- // Recursive object check
- if (
- typeof oldState[k] === "object" &&
- typeof newState[k] === "object" &&
- oldState[k] !== null &&
- newState[k] !== null
- ) {
- changedState(oldState[k], newState[k], currentPath).forEach((v, k) => {
- changes.set(k, v);
- });
- }
+ return _paths;
+ };
- // Value changed?
- if (JSON.stringify(oldState[k]) !== JSON.stringify(newState[k])) {
- if (!changes.has(currentPath)) {
- changes.set(currentPath, "changed-value");
- }
- }
- }
+ const differ = <S extends object>(oldState: S, newState: S): string[] => {
+ const oldPaths = paths(oldState, "$");
+ const oldPathKeys = oldPaths.map((x) => x.path);
+ const newPaths = paths(newState, "$");
+ const newPathKeys = newPaths.map((x) => x.path);
- return changes;
+ // All new paths
+ const added = newPathKeys.filter((path) => !oldPathKeys.includes(path));
+
+ // All removed paths
+ const removed = oldPathKeys.filter((path) => !newPathKeys.includes(path));
+
+ // Paths that remained
+ const same = oldPathKeys.filter((path) => newPathKeys.includes(path));
+
+ // Paths that changed
+ const changed = same.filter((path) => {
+ const oldValue = oldPaths.find((x) => x.path === path)?.value;
+ const newValue = newPaths.find((x) => x.path === path)?.value;
+
+ return oldValue !== newValue;
+ });
+
+ return [...new Set([...added, ...removed, ...changed])];
+ };
+
+ return differ(oldState, newState);
};
/**
@@ -170,9 +173,9 @@ export default function ShapeX<T extends object>(initialState: T): ShapeXInstanc
const changes = changedState(_state, response.state);
_state = response.state;
- changes.forEach((_, v) => {
- dispatch(`\$${v}`);
- });
+ for (let i = 0; i < changes.length; i++) {
+ dispatch(changes[i]);
+ }
}
// Dispatches events