Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion etc/brick-types.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2134,7 +2134,7 @@ export interface StoryboardContextItemFreeVariable {
// (undocumented)
loaded?: boolean;
// (undocumented)
loading?: boolean;
loading?: Promise<unknown>;
// (undocumented)
type: "free-variable";
// (undocumented)
Expand Down
134 changes: 123 additions & 11 deletions packages/brick-kit/src/core/StoryboardContext.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@ import { CustomTemplateContext } from "./CustomTemplates/CustomTemplateContext";
import { StoryboardContextWrapper } from "./StoryboardContext";
import * as runtime from "./Runtime";

const consoleWarn = jest
.spyOn(console, "warn")
.mockImplementation(() => void 0);
const consoleWarn = jest.spyOn(console, "warn").mockImplementation();
const consoleInfo = jest.spyOn(console, "info").mockImplementation();

let resolveValue = "lazily updated";
let resolveValue: string;
let rejectReason: Error;
const resolveOne = jest.fn(
async (
type: unknown,
Expand All @@ -17,6 +17,9 @@ const resolveOne = jest.fn(
context?: unknown,
options?: ResolveOptions
) => {
if (rejectReason) {
throw rejectReason;
}
await Promise.resolve();
conf.value = `[cache:${options?.cache ?? "default"}] ${resolveValue}`;
}
Expand All @@ -26,7 +29,9 @@ jest.spyOn(runtime, "_internalApiGetResolver").mockReturnValue({
} as any);

describe("StoryboardContextWrapper", () => {
afterEach(() => {
beforeEach(() => {
resolveValue = "lazily updated";
rejectReason = undefined;
jest.clearAllMocks();
});

Expand Down Expand Up @@ -94,22 +99,87 @@ describe("StoryboardContextWrapper", () => {

expect(ctx.getValue("asyncValue")).toBe("initial");
expect(ctx.getValue("processedData")).toBe("processed: initial");
ctx.updateValue("asyncValue", undefined, "refresh");
ctx.updateValue("asyncValue", undefined, "refresh", {
success: {
action: "console.info",
args: ["success", "<% EVENT.detail %>"],
},
error: {
action: "console.info",
args: ["error", "<% EVENT.detail.message %>"],
},
finally: {
action: "console.info",
args: ["finally", "<% EVENT.detail %>"],
},
});
expect(ctx.getValue("asyncValue")).toBe("initial");
expect(consoleInfo).not.toBeCalled();

await (global as any).flushPromises();
expect(ctx.getValue("asyncValue")).toBe("[cache:reload] lazily updated");
expect(ctx.getValue("processedData")).toBe(
"processed: [cache:reload] lazily updated"
);
expect(consoleInfo).toBeCalledTimes(2);
expect(consoleInfo).toHaveBeenNthCalledWith(1, "success", {
value: "[cache:reload] lazily updated",
});
expect(consoleInfo).toHaveBeenNthCalledWith(2, "finally", null);
});

it("should handle error when load failed", async () => {
const brick = { properties: {} };
const tplContext = new CustomTemplateContext(brick);
const ctx = tplContext.state;
await ctx.define(
[
{
name: "asyncValue",
resolve: {
useProvider: "my-provider",
lazy: true,
},
value: "initial",
},
],
{
tplContextId: tplContext.id,
} as any,
brick
);

rejectReason = new Error("oops");
ctx.updateValue("asyncValue", undefined, "load", {
success: {
action: "console.info",
args: ["success", "<% EVENT.detail %>"],
},
error: {
action: "console.info",
args: ["error", "<% EVENT.detail.message %>"],
},
finally: {
action: "console.info",
args: ["finally", "<% EVENT.detail %>"],
},
});

expect(ctx.getValue("asyncValue")).toBe("initial");
expect(consoleInfo).not.toBeCalled();

await (global as any).flushPromises();
expect(ctx.getValue("asyncValue")).toBe("initial");
expect(consoleInfo).toBeCalledTimes(2);
expect(consoleInfo).toHaveBeenNthCalledWith(1, "error", "oops");
expect(consoleInfo).toHaveBeenNthCalledWith(2, "finally", null);
});

it("should refresh when deps updated", async () => {
const brick = { properties: {} };
const tplContext = new CustomTemplateContext(brick);
const ctx = tplContext.state;

const originalResolveValue = resolveValue;
resolveValue = "initial";

await ctx.define(
Expand All @@ -136,7 +206,7 @@ describe("StoryboardContextWrapper", () => {
expect(ctx.getValue("asyncValue")).toBe("[cache:default] initial");
expect(ctx.getValue("dep")).toBe("first");

resolveValue = originalResolveValue;
resolveValue = "lazily updated";
ctx.updateValue("dep", "second", "replace");
expect(ctx.getValue("dep")).toBe("second");
expect(ctx.getValue("asyncValue")).toBe("[cache:default] initial");
Expand Down Expand Up @@ -174,9 +244,28 @@ describe("StoryboardContextWrapper", () => {
expect(ctx.getValue("asyncValue")).toBe("initial");
expect(ctx.getValue("processedData")).toBe("processed: initial");
// Trigger load twice.
ctx.updateValue("asyncValue", undefined, "load");
ctx.updateValue("asyncValue", undefined, "load");
ctx.updateValue("asyncValue", undefined, "load", {
success: {
action: "console.info",
args: ["[1] success", "<% EVENT.detail %>"],
},
finally: {
action: "console.info",
args: ["[1] finally", "<% EVENT.detail %>"],
},
});
ctx.updateValue("asyncValue", undefined, "load", {
success: {
action: "console.info",
args: ["[2] success", "<% EVENT.detail %>"],
},
finally: {
action: "console.info",
args: ["[2] finally", "<% EVENT.detail %>"],
},
});
expect(ctx.getValue("asyncValue")).toBe("initial");
expect(consoleInfo).not.toBeCalled();

await (global as any).flushPromises();
// Will not load again if it is already LOADING.
Expand All @@ -185,11 +274,34 @@ describe("StoryboardContextWrapper", () => {
expect(ctx.getValue("processedData")).toBe(
"processed: [cache:default] lazily updated"
);
expect(consoleInfo).toBeCalledTimes(4);
expect(consoleInfo).toHaveBeenNthCalledWith(1, "[1] success", {
value: "[cache:default] lazily updated",
});
expect(consoleInfo).toHaveBeenNthCalledWith(2, "[1] finally", null);
expect(consoleInfo).toHaveBeenNthCalledWith(3, "[2] success", {
value: "[cache:default] lazily updated",
});
expect(consoleInfo).toHaveBeenNthCalledWith(4, "[2] finally", null);

ctx.updateValue("asyncValue", undefined, "load");
ctx.updateValue("asyncValue", undefined, "load", {
success: {
action: "console.info",
args: ["[3] success", "<% EVENT.detail %>"],
},
finally: {
action: "console.info",
args: ["[3] finally", "<% EVENT.detail %>"],
},
});
await (global as any).flushPromises();
// Will not load again if it is already LOADED.
expect(resolveOne).toBeCalledTimes(1);
expect(consoleInfo).toBeCalledTimes(6);
expect(consoleInfo).toHaveBeenNthCalledWith(5, "[3] success", {
value: "[cache:default] lazily updated",
});
expect(consoleInfo).toHaveBeenNthCalledWith(6, "[3] finally", null);
});

it("should throw if use resolve with syncDefine", () => {
Expand Down
65 changes: 49 additions & 16 deletions packages/brick-kit/src/core/StoryboardContext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import EventTarget from "@ungap/event-target";
import {
BrickEventHandler,
BrickEventHandlerCallback,
ContextConf,
PluginRuntimeContext,
ResolveOptions,
Expand All @@ -16,9 +17,12 @@ import {
trackUsedState,
} from "@next-core/brick-utils";
import { looseCheckIf } from "../checkIf";
import { listenerFactory } from "../internal/bindListeners";
import {
eventCallbackFactory,
listenerFactory,
} from "../internal/bindListeners";
import { computeRealValue } from "../internal/setProperties";
import { RuntimeBrick } from "./exports";
import { RuntimeBrick, _internalApiGetCurrentContext } from "./exports";
import { _internalApiGetResolver } from "./Runtime";
import { handleHttpError } from "../handleHttpError";

Expand Down Expand Up @@ -52,7 +56,8 @@ export class StoryboardContextWrapper {
updateValue(
name: string,
value: unknown,
method: "assign" | "replace" | "refresh" | "load"
method: "assign" | "replace" | "refresh" | "load",
callback?: BrickEventHandlerCallback
): void {
if (!this.data.has(name)) {
if (this.tplContextId) {
Expand All @@ -79,27 +84,31 @@ export class StoryboardContextWrapper {
return;
}

// Todo(steve): support callback for refresh/load.
if (method === "refresh" || method === "load") {
if (!item.load) {
throw new Error(
`You can not ${method} the storyboard context "${name}" which has no resolve.`
);
}
if ((item.loaded || item.loading) && method === "load") {
// Do not load again.
return;

let promise: Promise<unknown>;
if (method === "load") {
// Try to reuse previous request when calling `load`.
if (item.loaded) {
promise = Promise.resolve(item.value);
} else if (item.loading) {
promise = item.loading;
}
}
item.loading = true;
// Defaults to ignore cache when refreshing.
item
.load({

if (!promise) {
promise = item.loading = item.load({
cache: method === "load" ? "default" : "reload",
...(value as ResolveOptions),
})
.then(
});
// Do not use the chained promise, since the callbacks need the original promise.
promise.then(
(val) => {
item.loading = false;
item.loaded = true;
item.value = val;
item.eventTarget?.dispatchEvent(
Expand All @@ -112,10 +121,34 @@ export class StoryboardContextWrapper {
);
},
(err) => {
item.loading = false;
handleHttpError(err);
// Let users to override error handling.
if (!callback?.error) {
handleHttpError(err);
}
}
);
}

if (callback) {
const callbackFactory = eventCallbackFactory(
callback,
() =>
this.getResolveOptions(_internalApiGetCurrentContext())
.mergedContext
);

promise.then(
(val) => {
callbackFactory("success")({ value: val });
callbackFactory("finally")();
},
(err) => {
callbackFactory("error")(err);
callbackFactory("finally")();
}
);
}

return;
}

Expand Down
8 changes: 6 additions & 2 deletions packages/brick-kit/src/internal/bindListeners.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,7 +267,9 @@ describe("bindListeners", () => {
type: "free-variable",
value: "initial",
load(options) {
return Promise.resolve(`[cache:${options.cache}] lazily updated`);
return Promise.resolve(
`[cache:${options.cache ?? "default"}] lazily updated`
);
},
},
],
Expand Down Expand Up @@ -970,7 +972,9 @@ describe("bindListeners", () => {
type: "free-variable",
value: "initial",
load(options) {
return Promise.resolve(`[cache:${options.cache}] lazily updated`);
return Promise.resolve(
`[cache:${options.cache ?? "default"}] lazily updated`
);
},
});

Expand Down
Loading