@player-ui/async-node-plugin 0.15.3 → 0.15.4--canary.881.37421
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/AsyncNodePlugin.native.js +3284 -2679
- package/dist/AsyncNodePlugin.native.js.map +1 -1
- package/dist/cjs/index.cjs +214 -69
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.legacy-esm.js +207 -60
- package/dist/index.mjs +207 -60
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/src/AsyncNodeError.ts +30 -0
- package/src/__tests__/index.test.ts +277 -4
- package/src/index.ts +189 -68
- package/src/internal-types.ts +30 -0
- package/src/utils/__tests__/getNodeFromError.test.ts +219 -0
- package/src/utils/__tests__/isAsyncPlayerError.test.ts +33 -0
- package/src/utils/getNodeFromError.ts +34 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/isAsyncPlayerError.ts +8 -0
- package/types/AsyncNodeError.d.ts +13 -0
- package/types/index.d.ts +3 -18
- package/types/internal-types.d.ts +29 -0
- package/types/utils/getNodeFromError.d.ts +5 -0
- package/types/utils/index.d.ts +2 -0
- package/types/utils/isAsyncPlayerError.d.ts +4 -0
|
@@ -8,6 +8,11 @@ import {
|
|
|
8
8
|
BeforeTransformFunction,
|
|
9
9
|
Flow,
|
|
10
10
|
NodeType,
|
|
11
|
+
Logger,
|
|
12
|
+
ErrorTypes,
|
|
13
|
+
ErrorSeverity,
|
|
14
|
+
PlayerErrorMetadata,
|
|
15
|
+
ErrorMetadata,
|
|
11
16
|
} from "@player-ui/player";
|
|
12
17
|
import { Player, Parser } from "@player-ui/player";
|
|
13
18
|
import { waitFor } from "@testing-library/react";
|
|
@@ -18,6 +23,19 @@ import {
|
|
|
18
23
|
} from "../index";
|
|
19
24
|
import { CheckPathPlugin } from "@player-ui/check-path-plugin";
|
|
20
25
|
import { Registry } from "@player-ui/partial-match-registry";
|
|
26
|
+
import { ExpressionPlugin } from "@player-ui/expression-plugin";
|
|
27
|
+
import { AsyncNodeError } from "../AsyncNodeError";
|
|
28
|
+
|
|
29
|
+
class ErrorWithProps extends Error implements PlayerErrorMetadata {
|
|
30
|
+
constructor(
|
|
31
|
+
message: string,
|
|
32
|
+
public type: string,
|
|
33
|
+
public severity?: ErrorSeverity,
|
|
34
|
+
public metadata?: ErrorMetadata,
|
|
35
|
+
) {
|
|
36
|
+
super(message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
21
39
|
|
|
22
40
|
const transform: BeforeTransformFunction = createAsyncTransform({
|
|
23
41
|
transformAssetType: "chat-message",
|
|
@@ -79,7 +97,35 @@ const asyncAssetFrf: Flow = {
|
|
|
79
97
|
VIEW_1: {
|
|
80
98
|
state_type: "VIEW",
|
|
81
99
|
ref: "my-view",
|
|
82
|
-
transitions: {
|
|
100
|
+
transitions: {
|
|
101
|
+
"*": "END",
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
END: {
|
|
105
|
+
state_type: "END",
|
|
106
|
+
outcome: "done",
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const nonViewErrorFlow: Flow = {
|
|
113
|
+
id: "test-flow",
|
|
114
|
+
views: [],
|
|
115
|
+
navigation: {
|
|
116
|
+
BEGIN: "FLOW_1",
|
|
117
|
+
FLOW_1: {
|
|
118
|
+
startState: "ACTION_1",
|
|
119
|
+
ACTION_1: {
|
|
120
|
+
state_type: "ACTION",
|
|
121
|
+
exp: ["captureError()"],
|
|
122
|
+
transitions: {
|
|
123
|
+
"*": "END",
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
END: {
|
|
127
|
+
state_type: "END",
|
|
128
|
+
outcome: "done",
|
|
83
129
|
},
|
|
84
130
|
},
|
|
85
131
|
},
|
|
@@ -1075,7 +1121,7 @@ describe("view", () => {
|
|
|
1075
1121
|
|
|
1076
1122
|
await waitFor(() => {
|
|
1077
1123
|
expect(onAsyncNodeErrorCallback).toHaveBeenCalledWith(
|
|
1078
|
-
new Error("Promise Rejected"),
|
|
1124
|
+
expect.objectContaining({ cause: new Error("Promise Rejected") }),
|
|
1079
1125
|
expect.anything(),
|
|
1080
1126
|
);
|
|
1081
1127
|
|
|
@@ -1105,14 +1151,241 @@ describe("view", () => {
|
|
|
1105
1151
|
|
|
1106
1152
|
await waitFor(() => {
|
|
1107
1153
|
expect(onAsyncNodeErrorCallback).toHaveBeenCalledWith(
|
|
1108
|
-
new Error("Promise Rejected"),
|
|
1154
|
+
expect.objectContaining({ cause: new Error("Promise Rejected") }),
|
|
1109
1155
|
expect.anything(),
|
|
1110
1156
|
);
|
|
1111
1157
|
|
|
1112
1158
|
const playerState = player.getState();
|
|
1113
1159
|
expect(playerState.status).toBe("error");
|
|
1114
1160
|
const errorState = playerState as ErrorState;
|
|
1115
|
-
expect(errorState.error.message).toBe(
|
|
1161
|
+
expect(errorState.error.message).toBe(
|
|
1162
|
+
"An error occured during async node resolution. See cause for details.",
|
|
1163
|
+
);
|
|
1164
|
+
expect(errorState.error).toBeInstanceOf(AsyncNodeError);
|
|
1165
|
+
expect((errorState.error as AsyncNodeError).cause?.message).toBe(
|
|
1166
|
+
"Promise Rejected",
|
|
1167
|
+
);
|
|
1168
|
+
});
|
|
1169
|
+
});
|
|
1170
|
+
|
|
1171
|
+
test("should log and absorb errors occuring outside of an in-progress state", async () => {
|
|
1172
|
+
const vitestLogger: Logger = {
|
|
1173
|
+
debug: vi.fn(),
|
|
1174
|
+
error: vi.fn(),
|
|
1175
|
+
info: vi.fn(),
|
|
1176
|
+
trace: vi.fn(),
|
|
1177
|
+
warn: vi.fn(),
|
|
1178
|
+
};
|
|
1179
|
+
const plugin = new AsyncNodePlugin({
|
|
1180
|
+
plugins: [new AsyncNodePluginPlugin()],
|
|
1181
|
+
});
|
|
1182
|
+
|
|
1183
|
+
let throwAsyncError: ((err: Error) => void) | undefined;
|
|
1184
|
+
|
|
1185
|
+
plugin.hooks.onAsyncNode.tap("test", async () => {
|
|
1186
|
+
return new Promise((_, rej) => {
|
|
1187
|
+
throwAsyncError = rej;
|
|
1188
|
+
});
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
const plugins = [plugin, new TestAsyncPlugin()];
|
|
1192
|
+
|
|
1193
|
+
const player = new Player({
|
|
1194
|
+
plugins: plugins,
|
|
1195
|
+
logger: vitestLogger,
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
player.start(asyncAssetFrf);
|
|
1199
|
+
|
|
1200
|
+
await vi.waitFor(() => {
|
|
1201
|
+
expect(throwAsyncError).toBeDefined();
|
|
1202
|
+
});
|
|
1203
|
+
|
|
1204
|
+
(player.getState() as InProgressState).controllers.flow.transition(
|
|
1205
|
+
"done",
|
|
1206
|
+
);
|
|
1207
|
+
await vi.waitFor(() => {
|
|
1208
|
+
const playerState = player.getState();
|
|
1209
|
+
expect(playerState.status).toBe("completed");
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
throwAsyncError!(new Error("Test Error"));
|
|
1213
|
+
|
|
1214
|
+
await vi.waitFor(() => {
|
|
1215
|
+
const state = player.getState();
|
|
1216
|
+
// should not leave completed state
|
|
1217
|
+
expect(state.status).toBe("completed");
|
|
1218
|
+
|
|
1219
|
+
expect(vitestLogger.warn).toHaveBeenCalledWith(
|
|
1220
|
+
expect.any(String), // Message doesn't matter, just check that the logged error is correct
|
|
1221
|
+
new Error("Test Error"),
|
|
1222
|
+
);
|
|
1223
|
+
});
|
|
1224
|
+
});
|
|
1225
|
+
|
|
1226
|
+
test("should do nothing with errors when not in a view state", async () => {
|
|
1227
|
+
const plugin = new AsyncNodePlugin({
|
|
1228
|
+
plugins: [new AsyncNodePluginPlugin()],
|
|
1229
|
+
});
|
|
1230
|
+
// Call capture error in an action state to make sure asyncnodeplugin doesn't try to handle this
|
|
1231
|
+
const expPlugin = new ExpressionPlugin(
|
|
1232
|
+
new Map([
|
|
1233
|
+
[
|
|
1234
|
+
"captureError",
|
|
1235
|
+
() => {
|
|
1236
|
+
(
|
|
1237
|
+
player.getState() as InProgressState
|
|
1238
|
+
).controllers.error.captureError(
|
|
1239
|
+
new ErrorWithProps(
|
|
1240
|
+
"Test Error",
|
|
1241
|
+
ErrorTypes.RENDER,
|
|
1242
|
+
ErrorSeverity.ERROR,
|
|
1243
|
+
{ assetId: "asset" },
|
|
1244
|
+
),
|
|
1245
|
+
);
|
|
1246
|
+
},
|
|
1247
|
+
],
|
|
1248
|
+
]),
|
|
1249
|
+
);
|
|
1250
|
+
const plugins = [plugin, expPlugin, new TestAsyncPlugin()];
|
|
1251
|
+
|
|
1252
|
+
const player = new Player({
|
|
1253
|
+
plugins: plugins,
|
|
1254
|
+
});
|
|
1255
|
+
|
|
1256
|
+
player.start(nonViewErrorFlow).catch(() => {});
|
|
1257
|
+
|
|
1258
|
+
await vi.waitFor(() => {
|
|
1259
|
+
const state = player.getState();
|
|
1260
|
+
expect(state.status).toBe("error");
|
|
1261
|
+
expect(onAsyncNodeErrorCallback).not.toHaveBeenCalled();
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
|
|
1265
|
+
test("should fail to handle errors if the plugin is setup incorrectly", async () => {
|
|
1266
|
+
const vitestLogger: Logger = {
|
|
1267
|
+
debug: vi.fn(),
|
|
1268
|
+
error: vi.fn(),
|
|
1269
|
+
info: vi.fn(),
|
|
1270
|
+
trace: vi.fn(),
|
|
1271
|
+
warn: vi.fn(),
|
|
1272
|
+
};
|
|
1273
|
+
const nodePluginPlugin = new AsyncNodePluginPlugin();
|
|
1274
|
+
|
|
1275
|
+
// Apply AsyncNodePluginPlugin in isolation to observe failures
|
|
1276
|
+
const plugin: PlayerPlugin = {
|
|
1277
|
+
name: "TestPlayerPlugin",
|
|
1278
|
+
apply: (player: Player) => {
|
|
1279
|
+
nodePluginPlugin.applyPlayer(player);
|
|
1280
|
+
player.hooks.view.tap("test", (view) => {
|
|
1281
|
+
nodePluginPlugin.apply(view);
|
|
1282
|
+
});
|
|
1283
|
+
},
|
|
1284
|
+
};
|
|
1285
|
+
const plugins = [plugin, new TestAsyncPlugin()];
|
|
1286
|
+
|
|
1287
|
+
const player = new Player({
|
|
1288
|
+
plugins: plugins,
|
|
1289
|
+
logger: vitestLogger,
|
|
1290
|
+
});
|
|
1291
|
+
player.start(asyncAssetFrf).catch(() => {});
|
|
1292
|
+
|
|
1293
|
+
await vi.waitFor(() => {
|
|
1294
|
+
const state = player.getState();
|
|
1295
|
+
expect(state.status).toBe("in-progress");
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
(player.getState() as InProgressState).controllers.error.captureError(
|
|
1299
|
+
new ErrorWithProps("Test Error", ErrorTypes.VIEW, ErrorSeverity.ERROR, {
|
|
1300
|
+
node: {
|
|
1301
|
+
type: NodeType.Async,
|
|
1302
|
+
value: {},
|
|
1303
|
+
},
|
|
1304
|
+
}),
|
|
1305
|
+
);
|
|
1306
|
+
|
|
1307
|
+
await vi.waitFor(() => {
|
|
1308
|
+
const state = player.getState();
|
|
1309
|
+
expect(state.status).toBe("error");
|
|
1310
|
+
expect(onAsyncNodeErrorCallback).not.toHaveBeenCalled();
|
|
1311
|
+
expect(vitestLogger.warn).toHaveBeenCalledWith(
|
|
1312
|
+
"[AsyncNodePlugin]: No plugin detected. Error handling will fail",
|
|
1313
|
+
);
|
|
1314
|
+
});
|
|
1315
|
+
});
|
|
1316
|
+
|
|
1317
|
+
test("should call onAsyncNodeError hook for any async node involved in generating the current one", async () => {
|
|
1318
|
+
const plugin = new AsyncNodePlugin({
|
|
1319
|
+
plugins: [new AsyncNodePluginPlugin()],
|
|
1320
|
+
});
|
|
1321
|
+
|
|
1322
|
+
const errorHandler = vi.fn((err: Error, node: Node.Async) => {
|
|
1323
|
+
if (node.id === "async-chat-id-2") {
|
|
1324
|
+
return {
|
|
1325
|
+
asset: {
|
|
1326
|
+
type: "text",
|
|
1327
|
+
value: "text",
|
|
1328
|
+
id: "FIXED",
|
|
1329
|
+
},
|
|
1330
|
+
};
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
return undefined;
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
plugin.hooks.onAsyncNodeError.tap("test", errorHandler);
|
|
1337
|
+
|
|
1338
|
+
let id = 0;
|
|
1339
|
+
plugin.hooks.onAsyncNode.tap("test", () => {
|
|
1340
|
+
const messageId = id++;
|
|
1341
|
+
if (messageId > 5) {
|
|
1342
|
+
throw new Error("Test Error");
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
return Promise.resolve({
|
|
1346
|
+
asset: {
|
|
1347
|
+
type: "chat-message",
|
|
1348
|
+
id: `chat-id-${messageId}`,
|
|
1349
|
+
value: {
|
|
1350
|
+
id: `chat-id-text-${messageId}`,
|
|
1351
|
+
type: "text",
|
|
1352
|
+
value: `Test Message ${messageId}`,
|
|
1353
|
+
},
|
|
1354
|
+
},
|
|
1355
|
+
});
|
|
1356
|
+
});
|
|
1357
|
+
|
|
1358
|
+
const player = new Player({
|
|
1359
|
+
plugins: [plugin, new TestAsyncPlugin()],
|
|
1360
|
+
});
|
|
1361
|
+
player.start(asyncAssetFrf).catch(() => {});
|
|
1362
|
+
|
|
1363
|
+
await vi.waitFor(() => {
|
|
1364
|
+
expect(id).toBeGreaterThan(5);
|
|
1365
|
+
expect(errorHandler).toHaveBeenCalledWith(
|
|
1366
|
+
expect.anything(),
|
|
1367
|
+
expect.objectContaining({
|
|
1368
|
+
id: "async-chat-id-5",
|
|
1369
|
+
}),
|
|
1370
|
+
);
|
|
1371
|
+
expect(errorHandler).toHaveBeenCalledWith(
|
|
1372
|
+
expect.anything(),
|
|
1373
|
+
expect.objectContaining({
|
|
1374
|
+
id: "async-chat-id-4",
|
|
1375
|
+
}),
|
|
1376
|
+
);
|
|
1377
|
+
expect(errorHandler).toHaveBeenCalledWith(
|
|
1378
|
+
expect.anything(),
|
|
1379
|
+
expect.objectContaining({
|
|
1380
|
+
id: "async-chat-id-3",
|
|
1381
|
+
}),
|
|
1382
|
+
);
|
|
1383
|
+
expect(errorHandler).toHaveBeenCalledWith(
|
|
1384
|
+
expect.anything(),
|
|
1385
|
+
expect.objectContaining({
|
|
1386
|
+
id: "async-chat-id-2",
|
|
1387
|
+
}),
|
|
1388
|
+
);
|
|
1116
1389
|
});
|
|
1117
1390
|
});
|
|
1118
1391
|
});
|
package/src/index.ts
CHANGED
|
@@ -10,33 +10,17 @@ import type {
|
|
|
10
10
|
ViewPlugin,
|
|
11
11
|
Resolver,
|
|
12
12
|
Resolve,
|
|
13
|
-
ViewController,
|
|
14
13
|
} from "@player-ui/player";
|
|
15
14
|
import { AsyncSeriesBailHook, SyncBailHook } from "tapable-ts";
|
|
16
15
|
import queueMicrotask from "queue-microtask";
|
|
16
|
+
import { AsyncNodeError } from "./AsyncNodeError";
|
|
17
|
+
import { AsyncNodeInfo, AsyncPluginContext } from "./internal-types";
|
|
18
|
+
import { getNodeFromError } from "./utils";
|
|
17
19
|
|
|
18
20
|
export * from "./types";
|
|
19
21
|
export * from "./transform";
|
|
20
22
|
export * from "./createAsyncTransform";
|
|
21
23
|
|
|
22
|
-
/** Object type for storing data related to a single `apply` of the `AsyncNodePluginPlugin`
|
|
23
|
-
* This object should be setup once per ViewInstance to keep any cached info just for that view to avoid conflicts of shared async node ids across different view states.
|
|
24
|
-
*/
|
|
25
|
-
type AsyncPluginContext = {
|
|
26
|
-
/** Map of async node id to resolved content */
|
|
27
|
-
nodeResolveCache: Map<string, Node.Node>;
|
|
28
|
-
/** The view instance this context is attached to. */
|
|
29
|
-
view: ViewInstance;
|
|
30
|
-
/** The view controller this context is attached to. */
|
|
31
|
-
viewController: ViewController;
|
|
32
|
-
/** Map of async node id to promises being used to resolve them */
|
|
33
|
-
inProgressNodes: Set<string>;
|
|
34
|
-
/** Map of async node ids to the original node they represent.
|
|
35
|
-
* In some cases, async nodes are transformed into from other node types so the original reference is needed in order to trigger an update on the view when the async node changes.
|
|
36
|
-
*/
|
|
37
|
-
originalNodeCache: Map<string, Set<Node.Node>>;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
24
|
export interface AsyncNodePluginOptions {
|
|
41
25
|
/** A set of plugins to load */
|
|
42
26
|
plugins?: AsyncNodeViewPlugin[];
|
|
@@ -142,10 +126,10 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
142
126
|
node: Node.Async,
|
|
143
127
|
context: AsyncPluginContext,
|
|
144
128
|
result: any,
|
|
145
|
-
|
|
129
|
+
parseFunction?: (content: any) => Node.Node | null,
|
|
146
130
|
) {
|
|
147
131
|
let parsedNode =
|
|
148
|
-
|
|
132
|
+
parseFunction && result ? parseFunction(result) : undefined;
|
|
149
133
|
|
|
150
134
|
if (parsedNode && node.onValueReceived) {
|
|
151
135
|
parsedNode = node.onValueReceived(parsedNode);
|
|
@@ -168,24 +152,43 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
168
152
|
context: AsyncPluginContext,
|
|
169
153
|
newNode?: Node.Node | null,
|
|
170
154
|
) {
|
|
171
|
-
const {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
155
|
+
const { asyncNodeCache: asyncNodeInfo, viewController } = context;
|
|
156
|
+
const entry = asyncNodeInfo.get(node.id);
|
|
157
|
+
if (!entry) {
|
|
158
|
+
throw new Error("Failed to update async content. Cache entry not found");
|
|
159
|
+
}
|
|
160
|
+
if (entry.resolvedContent !== newNode) {
|
|
161
|
+
entry.resolvedContent = newNode ? newNode : entry.asyncNode;
|
|
162
|
+
viewController.updateViewAST(entry.updateNodes);
|
|
176
163
|
}
|
|
177
164
|
}
|
|
178
165
|
|
|
179
166
|
private hasValidMapping(
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
): boolean {
|
|
183
|
-
const { nodeResolveCache } = context;
|
|
167
|
+
cacheEntry: AsyncNodeInfo,
|
|
168
|
+
): cacheEntry is Required<AsyncNodeInfo> {
|
|
184
169
|
return (
|
|
185
|
-
|
|
170
|
+
cacheEntry.resolvedContent !== undefined &&
|
|
171
|
+
cacheEntry.resolvedContent !== cacheEntry.asyncNode
|
|
186
172
|
);
|
|
187
173
|
}
|
|
188
174
|
|
|
175
|
+
private getOrCreateAsyncNodeCacheEntry(
|
|
176
|
+
node: Node.Async,
|
|
177
|
+
context: AsyncPluginContext,
|
|
178
|
+
): AsyncNodeInfo {
|
|
179
|
+
const { asyncNodeCache: asyncNodeInfo } = context;
|
|
180
|
+
let entry = asyncNodeInfo.get(node.id);
|
|
181
|
+
if (!entry) {
|
|
182
|
+
entry = {
|
|
183
|
+
asyncNode: node,
|
|
184
|
+
updateNodes: new Set(),
|
|
185
|
+
};
|
|
186
|
+
asyncNodeInfo.set(node.id, entry);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return entry;
|
|
190
|
+
}
|
|
191
|
+
|
|
189
192
|
/**
|
|
190
193
|
* Handles the asynchronous API integration for resolving nodes.
|
|
191
194
|
* This method sets up a hook on the resolver's `beforeResolve` event to process async nodes.
|
|
@@ -193,17 +196,32 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
193
196
|
* @param view
|
|
194
197
|
*/
|
|
195
198
|
applyResolver(resolver: Resolver, context: AsyncPluginContext): void {
|
|
199
|
+
const { assetIdCache } = context;
|
|
200
|
+
resolver.hooks.afterNodeUpdate.tap(this.name, (original, _, update) => {
|
|
201
|
+
if (
|
|
202
|
+
update.node.type !== NodeType.Asset &&
|
|
203
|
+
update.node.type !== NodeType.View
|
|
204
|
+
) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
assetIdCache.set(update.value.id, original);
|
|
209
|
+
});
|
|
210
|
+
|
|
196
211
|
resolver.hooks.beforeResolve.tap(this.name, (node, options) => {
|
|
197
212
|
if (!this.isAsync(node)) {
|
|
198
213
|
return node === null ? node : this.resolveAsyncChildren(node, context);
|
|
199
214
|
}
|
|
215
|
+
|
|
216
|
+
const entry = this.getOrCreateAsyncNodeCacheEntry(node, context);
|
|
217
|
+
|
|
200
218
|
if (options.node) {
|
|
201
|
-
|
|
219
|
+
entry.updateNodes = new Set([options.node]);
|
|
220
|
+
context.generatedByMap.set(options.node, node.id);
|
|
202
221
|
}
|
|
203
222
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
return this.resolveAsyncChildren(resolvedNode, context);
|
|
223
|
+
if (entry.resolvedContent !== undefined) {
|
|
224
|
+
return this.resolveAsyncChildren(entry.resolvedContent, context);
|
|
207
225
|
}
|
|
208
226
|
|
|
209
227
|
if (context.inProgressNodes.has(node.id)) {
|
|
@@ -236,20 +254,24 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
236
254
|
let index = 0;
|
|
237
255
|
while (index < node.values.length) {
|
|
238
256
|
const childNode = node.values[index];
|
|
239
|
-
if (
|
|
240
|
-
childNode?.type !== NodeType.Async ||
|
|
241
|
-
!this.hasValidMapping(childNode, context)
|
|
242
|
-
) {
|
|
257
|
+
if (childNode?.type !== NodeType.Async) {
|
|
243
258
|
index++;
|
|
244
259
|
continue;
|
|
245
260
|
}
|
|
261
|
+
const entry = this.getOrCreateAsyncNodeCacheEntry(childNode, context);
|
|
246
262
|
|
|
247
|
-
|
|
263
|
+
if (!this.hasValidMapping(entry)) {
|
|
264
|
+
index++;
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const mappedNode = entry.resolvedContent;
|
|
248
269
|
const nodeSet = new Set<Node.Node>();
|
|
249
270
|
if (mappedNode.type === NodeType.MultiNode && childNode.flatten) {
|
|
250
271
|
mappedNode.values.forEach((v: Node.Node) => {
|
|
251
272
|
v.parent = node;
|
|
252
273
|
nodeSet.add(v);
|
|
274
|
+
context.originalParentMap.set(v, childNode);
|
|
253
275
|
});
|
|
254
276
|
node.values = [
|
|
255
277
|
...node.values.slice(0, index),
|
|
@@ -261,17 +283,23 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
261
283
|
mappedNode.parent = node;
|
|
262
284
|
nodeSet.add(mappedNode);
|
|
263
285
|
}
|
|
264
|
-
|
|
286
|
+
entry.updateNodes = nodeSet;
|
|
287
|
+
for (const n of nodeSet) {
|
|
288
|
+
context.generatedByMap.set(n, childNode.id);
|
|
289
|
+
}
|
|
265
290
|
}
|
|
266
291
|
} else if ("children" in node) {
|
|
267
292
|
node.children?.forEach((c) => {
|
|
268
293
|
// Similar to above, using a while loop lets us handle when async nodes produce more async nodes.
|
|
269
|
-
while (
|
|
270
|
-
c.value
|
|
271
|
-
this.hasValidMapping(
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
294
|
+
while (c.value.type === NodeType.Async) {
|
|
295
|
+
const entry = this.getOrCreateAsyncNodeCacheEntry(c.value, context);
|
|
296
|
+
if (!this.hasValidMapping(entry)) {
|
|
297
|
+
break;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const mappedNode = entry.resolvedContent;
|
|
301
|
+
entry.updateNodes = new Set([mappedNode]);
|
|
302
|
+
context.generatedByMap.set(mappedNode, c.value.id);
|
|
275
303
|
c.value = mappedNode;
|
|
276
304
|
c.value.parent = node;
|
|
277
305
|
}
|
|
@@ -290,35 +318,31 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
290
318
|
const result = await this.basePlugin?.hooks.onAsyncNode.call(
|
|
291
319
|
node,
|
|
292
320
|
(result) => {
|
|
293
|
-
this.parseNodeAndUpdate(node, context, result, options);
|
|
321
|
+
this.parseNodeAndUpdate(node, context, result, options.parseNode);
|
|
294
322
|
},
|
|
295
323
|
);
|
|
296
324
|
|
|
297
325
|
// Stop tracking before the next update is triggered
|
|
298
326
|
context.inProgressNodes.delete(node.id);
|
|
299
|
-
this.parseNodeAndUpdate(node, context, result, options);
|
|
327
|
+
this.parseNodeAndUpdate(node, context, result, options.parseNode);
|
|
300
328
|
} catch (e: unknown) {
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
if (
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
}
|
|
310
|
-
|
|
329
|
+
const cause = e instanceof Error ? e : new Error(String(e));
|
|
330
|
+
const playerState = this.basePlugin?.getPlayerInstance()?.getState();
|
|
331
|
+
|
|
332
|
+
if (playerState?.status !== "in-progress") {
|
|
333
|
+
options.logger?.warn(
|
|
334
|
+
"[AsyncNodePlugin]: An error occured during async node resolution, but the player instance is no londer running. Exception: ",
|
|
335
|
+
cause,
|
|
336
|
+
);
|
|
311
337
|
return;
|
|
312
338
|
}
|
|
313
339
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
error,
|
|
340
|
+
const error = new AsyncNodeError(
|
|
341
|
+
node,
|
|
342
|
+
"An error occured during async node resolution. See cause for details.",
|
|
343
|
+
cause,
|
|
317
344
|
);
|
|
318
|
-
|
|
319
|
-
// Stop tracking before the next update is triggered
|
|
320
|
-
context.inProgressNodes.delete(node.id);
|
|
321
|
-
this.parseNodeAndUpdate(node, context, result, options);
|
|
345
|
+
playerState.controllers.error.captureError(error);
|
|
322
346
|
}
|
|
323
347
|
}
|
|
324
348
|
|
|
@@ -385,15 +409,112 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
385
409
|
}
|
|
386
410
|
|
|
387
411
|
applyPlayer(player: Player): void {
|
|
412
|
+
// TODO: Need a better mechanism for storing the current context.
|
|
413
|
+
let currentContext: AsyncPluginContext | undefined = undefined;
|
|
414
|
+
let parser: Parser | undefined = undefined;
|
|
415
|
+
|
|
416
|
+
player.hooks.errorController.tap("async", (errorController) => {
|
|
417
|
+
errorController.hooks.onError.tap("async", (playerError) => {
|
|
418
|
+
if (currentContext === undefined) {
|
|
419
|
+
return undefined;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Try to handle the error using the onAsyncNodeError hook. Returns true if new content is provided. */
|
|
423
|
+
const tryHandleError = (asyncNode: Node.Async): boolean => {
|
|
424
|
+
if (this.basePlugin === undefined) {
|
|
425
|
+
player.logger.warn(
|
|
426
|
+
`[AsyncNodePlugin]: No plugin detected. Error handling will fail`,
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
let result: any = undefined;
|
|
431
|
+
result = this.basePlugin?.hooks.onAsyncNodeError.call(
|
|
432
|
+
playerError,
|
|
433
|
+
asyncNode,
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
if (result === undefined) {
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
player.logger?.warn(
|
|
441
|
+
"[AsyncNodePlugin]: Async node handling failed and resolved with a fallback. Cause:",
|
|
442
|
+
playerError.message,
|
|
443
|
+
);
|
|
444
|
+
|
|
445
|
+
// Stop tracking before the next update is triggered
|
|
446
|
+
currentContext!.inProgressNodes.delete(asyncNode.id);
|
|
447
|
+
this.parseNodeAndUpdate(
|
|
448
|
+
asyncNode,
|
|
449
|
+
currentContext!,
|
|
450
|
+
result,
|
|
451
|
+
parser?.parseObject.bind(parser),
|
|
452
|
+
);
|
|
453
|
+
|
|
454
|
+
return true;
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const getNextNode = (node: Node.Node): Node.Node | undefined => {
|
|
458
|
+
const parent =
|
|
459
|
+
currentContext?.originalParentMap.get(node) ?? node.parent;
|
|
460
|
+
|
|
461
|
+
if (!parent) {
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// asyncNodeCache has current asyncNode reference more up to date with what's happening in the resolver. Sometimes AsyncNodeError has old references so this helps us move up the tree more accurately
|
|
466
|
+
return this.isAsync(parent)
|
|
467
|
+
? currentContext?.asyncNodeCache.get(parent.id)?.asyncNode
|
|
468
|
+
: parent;
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
let node = getNodeFromError(playerError, currentContext);
|
|
472
|
+
// If the node is an async node try, to handle errors with it first.
|
|
473
|
+
if (node?.type === NodeType.Async && tryHandleError(node)) {
|
|
474
|
+
return true;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Loop through the nodes to see if something is generated by something else. Continue until the error is handled or there are no more nodes to check
|
|
478
|
+
while (node !== undefined) {
|
|
479
|
+
const generatedBy = currentContext.generatedByMap.get(node);
|
|
480
|
+
if (generatedBy) {
|
|
481
|
+
const entry = currentContext.asyncNodeCache.get(generatedBy);
|
|
482
|
+
|
|
483
|
+
if (!entry) {
|
|
484
|
+
node = getNextNode(node);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const { asyncNode } = entry;
|
|
489
|
+
|
|
490
|
+
// Don't return false when the error isn't handled to allow for cases where one async is generated by another. Give different nodes a chance to try to recover from the error.
|
|
491
|
+
if (tryHandleError(asyncNode)) {
|
|
492
|
+
return true;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
node = getNextNode(node);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
return undefined;
|
|
500
|
+
});
|
|
501
|
+
});
|
|
502
|
+
|
|
388
503
|
player.hooks.viewController.tap("async", (viewController) => {
|
|
389
504
|
viewController.hooks.view.tap("async", (view) => {
|
|
505
|
+
view.hooks.parser.tap(this.name, (p) => {
|
|
506
|
+
parser = p;
|
|
507
|
+
});
|
|
390
508
|
const context: AsyncPluginContext = {
|
|
391
|
-
nodeResolveCache: new Map(),
|
|
392
509
|
inProgressNodes: new Set(),
|
|
393
510
|
view,
|
|
394
511
|
viewController,
|
|
395
|
-
|
|
512
|
+
generatedByMap: new Map(),
|
|
513
|
+
assetIdCache: new Map(),
|
|
514
|
+
asyncNodeCache: new Map(),
|
|
515
|
+
originalParentMap: new Map(),
|
|
396
516
|
};
|
|
517
|
+
currentContext = context;
|
|
397
518
|
|
|
398
519
|
view.hooks.resolver.tap("async", (resolver) => {
|
|
399
520
|
this.applyResolver(resolver, context);
|