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
1 change: 1 addition & 0 deletions etc/brick-kit.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import type { PresetBricksConf } from '@next-core/brick-types';
import { default as React_2 } from 'react';
import { RefForProxy } from '@next-core/brick-types';
import { ResolveConf } from '@next-core/brick-types';
import { ResolveOptions } from '@next-core/brick-types';
import { RouteConf } from '@next-core/brick-types';
import type { RuntimeBootstrapData } from '@next-core/brick-types';
import type { RuntimeMisc } from '@next-core/brick-types';
Expand Down
15 changes: 13 additions & 2 deletions etc/brick-types.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -716,7 +716,7 @@ export interface BuilderSnippetNode extends BuilderBaseNode {

// @public
export interface BuiltinBrickEventHandler {
action: "history.push" | "history.replace" | "history.goBack" | "history.goForward" | "history.reload" | "history.pushQuery" | "history.replaceQuery" | "history.pushAnchor" | "history.block" | "history.unblock" | "segue.push" | "segue.replace" | "alias.push" | "alias.replace" | "localStorage.setItem" | "localStorage.removeItem" | "sessionStorage.setItem" | "sessionStorage.removeItem" | "legacy.go" | "location.reload" | "location.assign" | "window.open" | "event.preventDefault" | "console.log" | "console.error" | "console.warn" | "console.info" | "message.success" | "message.error" | "message.info" | "message.warn" | "handleHttpError" | "context.assign" | "context.replace" | "context.refresh" | "state.update" | "state.refresh" | "tpl.dispatchEvent" | "message.subscribe" | "message.unsubscribe" | "theme.setDarkTheme" | "theme.setLightTheme" | "theme.setTheme" | "mode.setDashboardMode" | "mode.setDefaultMode" | "menu.clearMenuTitleCache" | "menu.clearMenuCache" | "preview.debug" | "analytics.event";
action: "history.push" | "history.replace" | "history.goBack" | "history.goForward" | "history.reload" | "history.pushQuery" | "history.replaceQuery" | "history.pushAnchor" | "history.block" | "history.unblock" | "segue.push" | "segue.replace" | "alias.push" | "alias.replace" | "localStorage.setItem" | "localStorage.removeItem" | "sessionStorage.setItem" | "sessionStorage.removeItem" | "legacy.go" | "location.reload" | "location.assign" | "window.open" | "event.preventDefault" | "console.log" | "console.error" | "console.warn" | "console.info" | "message.success" | "message.error" | "message.info" | "message.warn" | "handleHttpError" | "context.assign" | "context.replace" | "context.refresh" | "context.load" | "state.update" | "state.refresh" | "state.load" | "tpl.dispatchEvent" | "message.subscribe" | "message.unsubscribe" | "theme.setDarkTheme" | "theme.setLightTheme" | "theme.setTheme" | "mode.setDashboardMode" | "mode.setDefaultMode" | "menu.clearMenuTitleCache" | "menu.clearMenuCache" | "preview.debug" | "analytics.event";
args?: unknown[];
callback?: BrickEventHandlerCallback;
if?: string | boolean;
Expand Down Expand Up @@ -1721,6 +1721,13 @@ export interface ResolveMenuConf {
type: "resolve";
}

// Warning: (ae-internal-missing-underscore) The name "ResolveOptions" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal (undocumented)
export interface ResolveOptions {
cache?: "default" | "reload";
}

// Warning: (ae-internal-missing-underscore) The name "RouteAliasConf" should be prefixed with an underscore because the declaration is marked as @internal
//
// @internal (undocumented)
Expand Down Expand Up @@ -2123,7 +2130,11 @@ export interface StoryboardContextItemFreeVariable {
// (undocumented)
eventTarget?: EventTarget;
// (undocumented)
refresh?: () => Promise<unknown>;
load?: (options?: ResolveOptions) => Promise<unknown>;
// (undocumented)
loaded?: boolean;
// (undocumented)
loading?: boolean;
// (undocumented)
type: "free-variable";
// (undocumented)
Expand Down
12 changes: 8 additions & 4 deletions packages/brick-kit/src/core/Resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
HandleReject,
HandleRejectByCatch,
GeneralTransform,
ResolveOptions,
} from "@next-core/brick-types";
import { asyncProcessBrick } from "@next-core/brick-utils";
import { computeRealValue } from "../internal/setProperties";
Expand Down Expand Up @@ -99,21 +100,24 @@ export class Resolver {
resolveConf: ResolveConf,
conf: BrickConf,
brick?: RuntimeBrick,
context?: PluginRuntimeContext
context?: PluginRuntimeContext,
options?: ResolveOptions
): Promise<void>;
async resolveOne(
type: "reference",
resolveConf: ResolveConf,
conf: Record<string, any>,
brick?: RuntimeBrick,
context?: PluginRuntimeContext
context?: PluginRuntimeContext,
options?: ResolveOptions
): Promise<void>;
async resolveOne(
type: "brick" | "reference",
resolveConf: ResolveConf,
conf: BrickConf | Record<string, any>,
brick?: RuntimeBrick,
context?: PluginRuntimeContext
context?: PluginRuntimeContext,
options?: ResolveOptions
): Promise<void> {
const brickConf = conf as BrickConf;
const propsReference = conf as Record<string, any>;
Expand Down Expand Up @@ -238,7 +242,7 @@ export class Resolver {
}

let promise: Promise<any>;
if (this.cache.has(cacheKey)) {
if (options?.cache !== "reload" && this.cache.has(cacheKey)) {
promise = this.cache.get(cacheKey);
} else {
promise = (async () => {
Expand Down
74 changes: 67 additions & 7 deletions packages/brick-kit/src/core/StoryboardContext.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RuntimeBrickElement } from "@next-core/brick-types";
import { ResolveOptions, RuntimeBrickElement } from "@next-core/brick-types";
import { CustomTemplateContext } from "./CustomTemplates/CustomTemplateContext";
import { StoryboardContextWrapper } from "./StoryboardContext";
import * as runtime from "./Runtime";
Expand All @@ -9,16 +9,27 @@ const consoleWarn = jest

let resolveValue = "lazily updated";
const resolveOne = jest.fn(
async (a: unknown, b: unknown, c: Record<string, unknown>) => {
async (
type: unknown,
resolveConf: unknown,
conf: Record<string, unknown>,
brick?: unknown,
context?: unknown,
options?: ResolveOptions
) => {
await Promise.resolve();
c.value = resolveValue;
conf.value = `[cache:${options?.cache ?? "default"}] ${resolveValue}`;
}
);
jest.spyOn(runtime, "_internalApiGetResolver").mockReturnValue({
resolveOne,
} as any);

describe("StoryboardContextWrapper", () => {
afterEach(() => {
jest.clearAllMocks();
});

it("should work", () => {
const tplContext = new CustomTemplateContext({});
const ctx = tplContext.state;
Expand Down Expand Up @@ -87,8 +98,10 @@ describe("StoryboardContextWrapper", () => {
expect(ctx.getValue("asyncValue")).toBe("initial");

await (global as any).flushPromises();
expect(ctx.getValue("asyncValue")).toBe("lazily updated");
expect(ctx.getValue("processedData")).toBe("processed: lazily updated");
expect(ctx.getValue("asyncValue")).toBe("[cache:reload] lazily updated");
expect(ctx.getValue("processedData")).toBe(
"processed: [cache:reload] lazily updated"
);
});

it("should refresh when deps updated", async () => {
Expand Down Expand Up @@ -120,16 +133,63 @@ describe("StoryboardContextWrapper", () => {
brick
);

expect(ctx.getValue("asyncValue")).toBe("initial");
expect(ctx.getValue("asyncValue")).toBe("[cache:default] initial");
expect(ctx.getValue("dep")).toBe("first");

resolveValue = originalResolveValue;
ctx.updateValue("dep", "second", "replace");
expect(ctx.getValue("dep")).toBe("second");
expect(ctx.getValue("asyncValue")).toBe("[cache:default] initial");

await (global as any).flushPromises();
expect(ctx.getValue("asyncValue")).toBe("[cache:default] lazily updated");
});

it("should load", 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",
},
{
name: "processedData",
value: "<% `processed: ${STATE.asyncValue}` %>",
track: true,
},
],
{
tplContextId: tplContext.id,
} as any,
brick
);

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");
expect(ctx.getValue("asyncValue")).toBe("initial");

await (global as any).flushPromises();
// Will not load again if it is already LOADING.
expect(resolveOne).toBeCalledTimes(1);
expect(ctx.getValue("asyncValue")).toBe("[cache:default] lazily updated");
expect(ctx.getValue("processedData")).toBe(
"processed: [cache:default] lazily updated"
);

ctx.updateValue("asyncValue", undefined, "load");
await (global as any).flushPromises();
expect(ctx.getValue("asyncValue")).toBe("lazily updated");
// Will not load again if it is already LOADED.
expect(resolveOne).toBeCalledTimes(1);
});

it("should throw if use resolve with syncDefine", () => {
Expand Down
77 changes: 51 additions & 26 deletions packages/brick-kit/src/core/StoryboardContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
BrickEventHandler,
ContextConf,
PluginRuntimeContext,
ResolveOptions,
StoryboardContextItem,
StoryboardContextItemFreeVariable,
} from "@next-core/brick-types";
Expand Down Expand Up @@ -51,7 +52,7 @@ export class StoryboardContextWrapper {
updateValue(
name: string,
value: unknown,
method: "assign" | "replace" | "refresh"
method: "assign" | "replace" | "refresh" | "load"
): void {
if (!this.data.has(name)) {
if (this.tplContextId) {
Expand All @@ -73,28 +74,48 @@ export class StoryboardContextWrapper {
if (item.type !== "free-variable") {
// eslint-disable-next-line no-console
console.error(
`Conflict storyboard context "${name}", expected "free-variable", received "${item.type}".`
`Unexpected storyboard context "${name}", expected "free-variable", received "${item.type}".`
);
return;
}

if (method === "refresh") {
if (!item.refresh) {
// Todo(steve): support callback for refresh/load.
if (method === "refresh" || method === "load") {
if (!item.load) {
throw new Error(
`You can not refresh the storyboard context "${name}" which has no resolve.`
`You can not ${method} the storyboard context "${name}" which has no resolve.`
);
}
item.refresh().then((val) => {
item.value = val;
item.eventTarget?.dispatchEvent(
new CustomEvent(
this.tplContextId ? "state.change" : "context.change",
{
detail: item.value,
}
)
if ((item.loaded || item.loading) && method === "load") {
// Do not load again.
return;
}
item.loading = true;
// Defaults to ignore cache when refreshing.
item
.load({
cache: method === "load" ? "default" : "reload",
...(value as ResolveOptions),
})
.then(
(val) => {
item.loading = false;
item.loaded = true;
item.value = val;
item.eventTarget?.dispatchEvent(
new CustomEvent(
this.tplContextId ? "state.change" : "context.change",
{
detail: item.value,
}
)
);
},
(err) => {
item.loading = false;
handleHttpError(err);
}
);
}, handleHttpError);
return;
}

Expand Down Expand Up @@ -207,12 +228,12 @@ async function resolveNormalStoryboardContext(
}
const isTemplateState = !!storyboardContextWrapper.tplContextId;
let value = getDefinedTemplateState(isTemplateState, contextConf, brick);
let refresh: () => Promise<unknown> = null;
let load: StoryboardContextItemFreeVariable["load"] = null;
let isLazyResolve = false;
if (value === undefined) {
if (contextConf.resolve) {
if (looseCheckIf(contextConf.resolve, mergedContext)) {
refresh = async () => {
load = async (options) => {
const valueConf: Record<string, unknown> = {};
await _internalApiGetResolver().resolveOne(
"reference",
Expand All @@ -223,19 +244,20 @@ async function resolveNormalStoryboardContext(
},
valueConf,
null,
mergedContext
mergedContext,
options
);
return valueConf.value;
};
isLazyResolve = contextConf.resolve.lazy;
if (!isLazyResolve) {
value = await refresh();
value = await load();
}
} else if (!hasOwnProperty(contextConf, "value")) {
return false;
}
}
if ((!refresh || isLazyResolve) && contextConf.value !== undefined) {
if ((!load || isLazyResolve) && contextConf.value !== undefined) {
// If the context has no resolve, just use its `value`.
// Or if the resolve is ignored or lazy, use its `value` as a fallback.
value = computeRealValue(contextConf.value, mergedContext, true);
Expand All @@ -244,7 +266,7 @@ async function resolveNormalStoryboardContext(
if (contextConf.track) {
// Track its dependencies and auto update when each of them changed.
const deps = (isTemplateState ? trackUsedState : trackUsedContext)(
refresh ? contextConf.resolve : contextConf.value
load ? contextConf.resolve : contextConf.value
);
for (const dep of deps) {
const ctx = storyboardContextWrapper.get().get(dep);
Expand All @@ -253,10 +275,10 @@ async function resolveNormalStoryboardContext(
)?.eventTarget?.addEventListener(
isTemplateState ? "state.change" : "context.change",
() => {
if (refresh) {
if (load) {
storyboardContextWrapper.updateValue(
contextConf.name,
undefined,
{ cache: "default" },
"refresh"
);
} else {
Expand All @@ -278,7 +300,8 @@ async function resolveNormalStoryboardContext(
mergedContext,
storyboardContextWrapper,
brick,
refresh
load,
!isLazyResolve
);
return true;
}
Expand Down Expand Up @@ -333,14 +356,16 @@ function resolveFreeVariableValue(
mergedContext: PluginRuntimeContext,
storyboardContextWrapper: StoryboardContextWrapper,
brick?: RuntimeBrick,
refresh?: () => Promise<unknown>
load?: StoryboardContextItemFreeVariable["load"],
loaded?: boolean
): void {
const newContext: StoryboardContextItem = {
type: "free-variable",
value,
// This is required for tracking context, even if no `onChange` is specified.
eventTarget: new EventTarget(),
refresh,
load,
loaded,
};
if (contextConf.onChange) {
for (const handler of ([] as BrickEventHandler[]).concat(
Expand Down
Loading