Skip to content

Commit

Permalink
add r18Use and fix runp not ending when it is consumed from cache (#142)
Browse files Browse the repository at this point in the history
* enhande the read API
* change application[resource][api].use to suspend and throw on error
* app pattern enhancements and types fixes
* add application.useAsyncState to replace old ".use"
* add more tests for hydration, contexts, deps and more
* more tests and files for everything
* Move StateHook back to react-async-states
* Set version to 1.3
  • Loading branch information
incepter authored Apr 2, 2023
1 parent 7081133 commit 7477d34
Show file tree
Hide file tree
Showing 44 changed files with 1,060 additions and 452 deletions.
1 change: 1 addition & 0 deletions packages/core/jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = {
"__tests__",
"index-prod.js",
"configuration-warn",
"devtools",
"type*.ts"
],
testMatch: [
Expand Down
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"author": "incepter",
"sideEffects": false,
"name": "async-states",
"version": "1.2.0",
"version": "1.3.0",
"main": "dist/umd/index",
"types": "dist/es/index",
"module": "dist/es/src/index",
Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/AsyncState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ export class AsyncState<T, E, R, A extends unknown[]> implements StateInterface<
return;
}
this.willUpdate = true;
if (this.state?.status === pending || (
if (this.state.status === pending || (
isFunction(this.currentAbort) && !this.isEmitting
)) {
this.abort();
Expand Down Expand Up @@ -1166,8 +1166,9 @@ function attemptCache<T, E, R, A extends unknown[]>(
if (cachedState) {
if (didNotExpire(cachedState)) {
if (cachedState.state !== instance.state) {
instance.replaceState(cachedState.state, true, runProps);
instance.replaceState(cachedState.state);
}
invokeChangeCallbacks(cachedState.state, runProps);
if (__DEV__) devtools.emitRunConsumedFromCache(instance, payload, args);
return true;
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import {requestContext} from "../../pool";

jest.mock("../../utils", () => {
return {
...jest.requireActual("../../utils"),
isServer: true,
}
})
describe('createContext in the server', () => {
it('should throw when no context is found in the server', () => {
let originalConsoleError = console.error;
console.error = jest.fn()
expect(() => requestContext({}))
.toThrow("You should always provide an execution context in the server")
expect(console.error).toHaveBeenCalledTimes(1)
console.error = originalConsoleError
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {Sources} from "../../AsyncState";
import {createContext, terminateContext} from "../../pool";
import {createContext, getContext, terminateContext} from "../../pool";
import {expect} from "@jest/globals";

describe('Create instances in different contexts', () => {
let consoleErrorSpy;
Expand All @@ -26,6 +27,30 @@ describe('Create instances in different contexts', () => {
let source2 = Sources.for("key2", null, {context: ctx})
expect(source1).toBe(source2)
});
it('should return the same context when creating with the same object', () => {
let ctx = {}
let context1 = createContext(ctx)
let context2 = createContext(ctx)
let context3 = createContext({})

expect(context1).toBe(context2)
expect(context1).not.toBe(context3)
});
it('should throw when a non object is passed (a part from null)', () => {
expect(() => createContext("string"))
.toThrow("createContext requires an object")
expect(() => createContext(Symbol("ok")))
.toThrow("createContext requires an object")
expect(() => createContext(15))
.toThrow("createContext requires an object")
expect(() => createContext(undefined))
.toThrow("createContext requires an object")

// should not throw when null is passed (default context)
expect(() => createContext(null))
.not
.toThrow("createContext requires an object")
});

it('should use a different instance from different pools', () => {
let ctx1 = {}
Expand Down Expand Up @@ -53,5 +78,17 @@ describe('Create instances in different contexts', () => {
terminateContext(ctx)
expect(() => Sources.for("key6", null, {context: ctx}))
.toThrow("No execution context for context [object Object]")

// this should do nothing: terminate a non existing context
terminateContext(undefined)
});


it('getContext should return the actual context', () => {
let ctx = {}
let context = createContext(ctx)
let defaultContext = getContext(null)
expect(getContext(ctx)).toBe(context)
expect(defaultContext).not.toBe(context)
});
});
31 changes: 28 additions & 3 deletions packages/core/src/__tests__/async-state/AsyncState.pools.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,39 +60,64 @@ describe('Create instances in different pools', () => {
expect(src2.getPayload()).toEqual({hello: true, ok2: true})
});

it('should listen to instances being added to pool', async () => {
it('should listen to instances being added to pool and then un-listen', async () => {
let testPool = requestContext(null).getOrCreatePool("test-pool")

let handler = jest.fn()
testPool.listen(handler)
let handler2 = jest.fn()
let unlisten = testPool.listen(handler)
let unlisten2 = testPool.listen(handler2)

let src = new AsyncState("test-key3", null, {pool: "test-pool"})
await flushPromises();

expect(handler).toHaveBeenCalledTimes(1)
expect(handler).toHaveBeenCalledWith(src, "test-key3")
expect(handler2).toHaveBeenCalledTimes(1)
expect(handler2).toHaveBeenCalledWith(src, "test-key3")

handler.mockClear()
handler2.mockClear()
unlisten!()

let src2 = new AsyncState("test-key31", null, {pool: "test-pool"})
await flushPromises()

expect(handler).toHaveBeenCalledTimes(0)
expect(handler2).toHaveBeenCalledTimes(1)
expect(handler2).toHaveBeenCalledWith(src2, "test-key31")
unlisten2!()
});
it('should watch over an instance in the pool', async () => {
it('should watch over an instance in the pool and then unwatch', async () => {
let testPool = requestContext(null).getOrCreatePool("test-pool")

let handler = jest.fn()
let handler2 = jest.fn()
testPool.watch("test-key4", handler)
let unwatch2 = testPool.watch("test-key4", handler2)

let src = new AsyncState("test-key4", null, {pool: "test-pool"})
await flushPromises();
expect(handler).toHaveBeenCalledTimes(1)
expect(handler).toHaveBeenCalledWith(src, "test-key4")
expect(handler2).toHaveBeenCalledTimes(1)
expect(handler2).toHaveBeenCalledWith(src, "test-key4")

unwatch2!()

handler.mockClear()
handler2.mockClear()
let src2 = new AsyncState("test-key5", null, {pool: "test-pool"})
await flushPromises();
expect(handler).not.toHaveBeenCalled()
expect(handler2).not.toHaveBeenCalled()

handler.mockClear()
let src3 = new AsyncState("test-key4", null, {pool: "test-pool"})
testPool.set("test-key4", src3)
await flushPromises();
expect(handler).toHaveBeenCalledTimes(1)
expect(handler).toHaveBeenCalledWith(src3, "test-key4")
expect(handler2).not.toHaveBeenCalled()
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import {maybeWindow} from "../../utils";
import {AsyncState, createSource, Sources} from "../../AsyncState";
import {mockDateNow} from "../utils/setup";
import {Status} from "../../enums";
import {expect} from "@jest/globals";

mockDateNow();
describe('AsyncState instance creation', () => {
function bootHydration(data) {
eval(data)
}

it('should boot instance state from hydrated content', () => {
if (!maybeWindow) {
throw new Error("No globalThis")
}
bootHydration(
'window.__ASYNC_STATES_HYDRATION_DATA__ = Object.assign(window.__ASYNC_STATES_HYDRATION_DATA__ || {}, {"ASYNC-STATES-default-POOL__INSTANCE__state-1":{"state":{"status":"success","data":42,"props":{"args":[42],"payload":{}},"timestamp":1487076708000},"payload":{}}})'
)
let instance = new AsyncState("state-1", null)
expect(instance.getVersion()).toBe(0)
expect(instance.getState().data).toBe(42)
expect(instance.getState().status).toBe("success")
expect(instance.lastSuccess).toBe(instance.getState())
bootHydration(
'window.__ASYNC_STATES_HYDRATION_DATA__ = Object.assign(window.__ASYNC_STATES_HYDRATION_DATA__ || {}, {"ASYNC-STATES-default-POOL__INSTANCE__state-2":{"state":{"status":"error","data":42,"props":{"args":[42],"payload":{}},"timestamp":1487076708000},"payload":{}}})'
)
let instance2 = new AsyncState("state-2", null, {initialValue: 15})
expect(instance2.getState().data).toBe(42)
expect(instance2.getState().status).toBe("error")
expect(instance2.lastSuccess.data).toBe(15)
expect(instance2.lastSuccess.status).toBe("initial")
});
it('should initialize state from cache and invalidate it', () => {
let persistSpy = jest.fn()
let instance = new AsyncState("state-3",
props => props.args[0],
{
initialValue(cache) {
return cache?.stateHash.state.data
},
cacheConfig: {
enabled: true,
load() {
return {
stateHash: {
addedAt: Date.now(),
deadline: Date.now() + 1000,
state: {
timestamp: Date.now(),
status: Status.success,
data: 55,
props: {args: [55]}
}
},
stateHash2: {
addedAt: Date.now(),
deadline: Date.now() + 1000,
state: {
timestamp: Date.now(),
status: Status.success,
data: 66,
props: {args: [66]}
}
},
}
},
persist() {
persistSpy.apply(null, arguments)
}
}
})

expect(instance.getState().status).toBe("initial")
expect(instance.getState().data).toBe(55)

let prevCache1 = instance.cache!.stateHash

instance.replaceCache("stateHash", {...prevCache1})
expect(instance.cache!.stateHash).not.toBe(prevCache1)

persistSpy.mockClear()
expect(instance.cache!.stateHash).not.toBe(undefined)
expect(instance.cache!.stateHash2).not.toBe(undefined)
instance.invalidateCache("stateHash")
expect(persistSpy).toHaveBeenCalledTimes(1)
expect(instance.cache!.stateHash).toBe(undefined)
expect(instance.cache!.stateHash2).not.toBe(undefined)

persistSpy.mockClear()
instance.invalidateCache()
expect(persistSpy).toHaveBeenCalledTimes(1)
expect(instance.cache!.stateHash).toBe(undefined)
expect(instance.cache!.stateHash2).toBe(undefined)
});

it('should update given configuration', () => {
let instance = new AsyncState("state-4", null, {initialValue: 15})
expect(instance.getConfig().initialValue).toBe(15)
instance.patchConfig({initialValue: 16})
expect(instance.getConfig().initialValue).toBe(16)
});
it('should answer correctly for hasLane and delete lane', () => {
let instance = new AsyncState("state-5", null)
expect(instance.removeLane()).toBe(false)
expect(instance.hasLane("notfound")).toBe(false)
instance.getLane("toBeForgotten")
expect(instance.hasLane("toBeForgotten")).toBe(true)

expect(instance.removeLane("toBeForgotten")).toBe(true)
expect(instance.hasLane("toBeForgotten")).toBe(false)
});
it('should return main instance on getLane', () => {
let instance = new AsyncState("state-6", null)
expect(instance.getLane()).toBe(instance)
});
it('should throw when attempting to force a non existing status', () => {
let instance = new AsyncState("state-7", null, {initialValue: 1})
// @ts-ignore
expect(() => instance.setState(15, "unknown")).toThrow("Unknown status ('unknown')")
});
it('should return all lanes', () => {
let instance = new AsyncState("state-8", null)
expect(instance._source.getAllLanes()).toEqual([])
let secondSrc = instance.getLane("toForget")._source
expect(instance._source.getAllLanes()).toEqual([secondSrc])
});
it('should get source by all ways', () => {
let src = createSource({ key: "state-9"})
expect(src.key).toEqual("state-9")
expect(Sources.of("state-9")).toBe(src)

let prevConsoleError = console.error;
console.error = () => {} // shut warning
// @ts-ignore
expect(Sources("state-9")).toBe(src)
expect(Sources.for("state-9")).toBe(src)
console.error = prevConsoleError
});
});
Loading

0 comments on commit 7477d34

Please sign in to comment.