@player-ui/async-node-plugin 0.15.4--canary.881.37421 → 0.15.4--canary.884.37483
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 +2676 -3281
- package/dist/AsyncNodePlugin.native.js.map +1 -1
- package/dist/cjs/index.cjs +69 -214
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/index.legacy-esm.js +60 -207
- package/dist/index.mjs +60 -207
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -4
- package/src/__tests__/index.test.ts +4 -277
- package/src/index.ts +68 -189
- package/src/utils/index.ts +0 -2
- package/types/index.d.ts +18 -3
- package/types/utils/index.d.ts +0 -2
- package/src/AsyncNodeError.ts +0 -30
- package/src/internal-types.ts +0 -30
- package/src/utils/__tests__/getNodeFromError.test.ts +0 -219
- package/src/utils/__tests__/isAsyncPlayerError.test.ts +0 -33
- package/src/utils/getNodeFromError.ts +0 -34
- package/src/utils/isAsyncPlayerError.ts +0 -8
- package/types/AsyncNodeError.d.ts +0 -13
- package/types/internal-types.d.ts +0 -29
- package/types/utils/getNodeFromError.d.ts +0 -5
- package/types/utils/isAsyncPlayerError.d.ts +0 -4
|
@@ -8,11 +8,6 @@ import {
|
|
|
8
8
|
BeforeTransformFunction,
|
|
9
9
|
Flow,
|
|
10
10
|
NodeType,
|
|
11
|
-
Logger,
|
|
12
|
-
ErrorTypes,
|
|
13
|
-
ErrorSeverity,
|
|
14
|
-
PlayerErrorMetadata,
|
|
15
|
-
ErrorMetadata,
|
|
16
11
|
} from "@player-ui/player";
|
|
17
12
|
import { Player, Parser } from "@player-ui/player";
|
|
18
13
|
import { waitFor } from "@testing-library/react";
|
|
@@ -23,19 +18,6 @@ import {
|
|
|
23
18
|
} from "../index";
|
|
24
19
|
import { CheckPathPlugin } from "@player-ui/check-path-plugin";
|
|
25
20
|
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
|
-
}
|
|
39
21
|
|
|
40
22
|
const transform: BeforeTransformFunction = createAsyncTransform({
|
|
41
23
|
transformAssetType: "chat-message",
|
|
@@ -97,35 +79,7 @@ const asyncAssetFrf: Flow = {
|
|
|
97
79
|
VIEW_1: {
|
|
98
80
|
state_type: "VIEW",
|
|
99
81
|
ref: "my-view",
|
|
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",
|
|
82
|
+
transitions: {},
|
|
129
83
|
},
|
|
130
84
|
},
|
|
131
85
|
},
|
|
@@ -1121,7 +1075,7 @@ describe("view", () => {
|
|
|
1121
1075
|
|
|
1122
1076
|
await waitFor(() => {
|
|
1123
1077
|
expect(onAsyncNodeErrorCallback).toHaveBeenCalledWith(
|
|
1124
|
-
|
|
1078
|
+
new Error("Promise Rejected"),
|
|
1125
1079
|
expect.anything(),
|
|
1126
1080
|
);
|
|
1127
1081
|
|
|
@@ -1151,241 +1105,14 @@ describe("view", () => {
|
|
|
1151
1105
|
|
|
1152
1106
|
await waitFor(() => {
|
|
1153
1107
|
expect(onAsyncNodeErrorCallback).toHaveBeenCalledWith(
|
|
1154
|
-
|
|
1108
|
+
new Error("Promise Rejected"),
|
|
1155
1109
|
expect.anything(),
|
|
1156
1110
|
);
|
|
1157
1111
|
|
|
1158
1112
|
const playerState = player.getState();
|
|
1159
1113
|
expect(playerState.status).toBe("error");
|
|
1160
1114
|
const errorState = playerState as ErrorState;
|
|
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
|
-
);
|
|
1115
|
+
expect(errorState.error.message).toBe("Promise Rejected");
|
|
1389
1116
|
});
|
|
1390
1117
|
});
|
|
1391
1118
|
});
|
package/src/index.ts
CHANGED
|
@@ -10,17 +10,33 @@ import type {
|
|
|
10
10
|
ViewPlugin,
|
|
11
11
|
Resolver,
|
|
12
12
|
Resolve,
|
|
13
|
+
ViewController,
|
|
13
14
|
} from "@player-ui/player";
|
|
14
15
|
import { AsyncSeriesBailHook, SyncBailHook } from "tapable-ts";
|
|
15
16
|
import queueMicrotask from "queue-microtask";
|
|
16
|
-
import { AsyncNodeError } from "./AsyncNodeError";
|
|
17
|
-
import { AsyncNodeInfo, AsyncPluginContext } from "./internal-types";
|
|
18
|
-
import { getNodeFromError } from "./utils";
|
|
19
17
|
|
|
20
18
|
export * from "./types";
|
|
21
19
|
export * from "./transform";
|
|
22
20
|
export * from "./createAsyncTransform";
|
|
23
21
|
|
|
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
|
+
|
|
24
40
|
export interface AsyncNodePluginOptions {
|
|
25
41
|
/** A set of plugins to load */
|
|
26
42
|
plugins?: AsyncNodeViewPlugin[];
|
|
@@ -126,10 +142,10 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
126
142
|
node: Node.Async,
|
|
127
143
|
context: AsyncPluginContext,
|
|
128
144
|
result: any,
|
|
129
|
-
|
|
145
|
+
options: Resolve.NodeResolveOptions,
|
|
130
146
|
) {
|
|
131
147
|
let parsedNode =
|
|
132
|
-
|
|
148
|
+
options.parseNode && result ? options.parseNode(result) : undefined;
|
|
133
149
|
|
|
134
150
|
if (parsedNode && node.onValueReceived) {
|
|
135
151
|
parsedNode = node.onValueReceived(parsedNode);
|
|
@@ -152,41 +168,22 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
152
168
|
context: AsyncPluginContext,
|
|
153
169
|
newNode?: Node.Node | null,
|
|
154
170
|
) {
|
|
155
|
-
const {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (entry.resolvedContent !== newNode) {
|
|
161
|
-
entry.resolvedContent = newNode ? newNode : entry.asyncNode;
|
|
162
|
-
viewController.updateViewAST(entry.updateNodes);
|
|
171
|
+
const { nodeResolveCache, viewController, originalNodeCache } = context;
|
|
172
|
+
if (nodeResolveCache.get(node.id) !== newNode) {
|
|
173
|
+
nodeResolveCache.set(node.id, newNode ? newNode : node);
|
|
174
|
+
const originalNode = originalNodeCache.get(node.id) ?? new Set([node]);
|
|
175
|
+
viewController.updateViewAST(originalNode);
|
|
163
176
|
}
|
|
164
177
|
}
|
|
165
178
|
|
|
166
179
|
private hasValidMapping(
|
|
167
|
-
cacheEntry: AsyncNodeInfo,
|
|
168
|
-
): cacheEntry is Required<AsyncNodeInfo> {
|
|
169
|
-
return (
|
|
170
|
-
cacheEntry.resolvedContent !== undefined &&
|
|
171
|
-
cacheEntry.resolvedContent !== cacheEntry.asyncNode
|
|
172
|
-
);
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
private getOrCreateAsyncNodeCacheEntry(
|
|
176
180
|
node: Node.Async,
|
|
177
181
|
context: AsyncPluginContext,
|
|
178
|
-
):
|
|
179
|
-
const {
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
asyncNode: node,
|
|
184
|
-
updateNodes: new Set(),
|
|
185
|
-
};
|
|
186
|
-
asyncNodeInfo.set(node.id, entry);
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
return entry;
|
|
182
|
+
): boolean {
|
|
183
|
+
const { nodeResolveCache } = context;
|
|
184
|
+
return (
|
|
185
|
+
nodeResolveCache.has(node.id) && nodeResolveCache.get(node.id) !== node
|
|
186
|
+
);
|
|
190
187
|
}
|
|
191
188
|
|
|
192
189
|
/**
|
|
@@ -196,32 +193,17 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
196
193
|
* @param view
|
|
197
194
|
*/
|
|
198
195
|
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
|
-
|
|
211
196
|
resolver.hooks.beforeResolve.tap(this.name, (node, options) => {
|
|
212
197
|
if (!this.isAsync(node)) {
|
|
213
198
|
return node === null ? node : this.resolveAsyncChildren(node, context);
|
|
214
199
|
}
|
|
215
|
-
|
|
216
|
-
const entry = this.getOrCreateAsyncNodeCacheEntry(node, context);
|
|
217
|
-
|
|
218
200
|
if (options.node) {
|
|
219
|
-
|
|
220
|
-
context.generatedByMap.set(options.node, node.id);
|
|
201
|
+
context.originalNodeCache.set(node.id, new Set([options.node]));
|
|
221
202
|
}
|
|
222
203
|
|
|
223
|
-
|
|
224
|
-
|
|
204
|
+
const resolvedNode = context.nodeResolveCache.get(node.id);
|
|
205
|
+
if (resolvedNode !== undefined) {
|
|
206
|
+
return this.resolveAsyncChildren(resolvedNode, context);
|
|
225
207
|
}
|
|
226
208
|
|
|
227
209
|
if (context.inProgressNodes.has(node.id)) {
|
|
@@ -254,24 +236,20 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
254
236
|
let index = 0;
|
|
255
237
|
while (index < node.values.length) {
|
|
256
238
|
const childNode = node.values[index];
|
|
257
|
-
if (
|
|
239
|
+
if (
|
|
240
|
+
childNode?.type !== NodeType.Async ||
|
|
241
|
+
!this.hasValidMapping(childNode, context)
|
|
242
|
+
) {
|
|
258
243
|
index++;
|
|
259
244
|
continue;
|
|
260
245
|
}
|
|
261
|
-
const entry = this.getOrCreateAsyncNodeCacheEntry(childNode, context);
|
|
262
246
|
|
|
263
|
-
|
|
264
|
-
index++;
|
|
265
|
-
continue;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
const mappedNode = entry.resolvedContent;
|
|
247
|
+
const mappedNode = context.nodeResolveCache.get(childNode.id)!;
|
|
269
248
|
const nodeSet = new Set<Node.Node>();
|
|
270
249
|
if (mappedNode.type === NodeType.MultiNode && childNode.flatten) {
|
|
271
250
|
mappedNode.values.forEach((v: Node.Node) => {
|
|
272
251
|
v.parent = node;
|
|
273
252
|
nodeSet.add(v);
|
|
274
|
-
context.originalParentMap.set(v, childNode);
|
|
275
253
|
});
|
|
276
254
|
node.values = [
|
|
277
255
|
...node.values.slice(0, index),
|
|
@@ -283,23 +261,17 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
283
261
|
mappedNode.parent = node;
|
|
284
262
|
nodeSet.add(mappedNode);
|
|
285
263
|
}
|
|
286
|
-
|
|
287
|
-
for (const n of nodeSet) {
|
|
288
|
-
context.generatedByMap.set(n, childNode.id);
|
|
289
|
-
}
|
|
264
|
+
context.originalNodeCache.set(childNode.id, nodeSet);
|
|
290
265
|
}
|
|
291
266
|
} else if ("children" in node) {
|
|
292
267
|
node.children?.forEach((c) => {
|
|
293
268
|
// Similar to above, using a while loop lets us handle when async nodes produce more async nodes.
|
|
294
|
-
while (
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const mappedNode = entry.resolvedContent;
|
|
301
|
-
entry.updateNodes = new Set([mappedNode]);
|
|
302
|
-
context.generatedByMap.set(mappedNode, c.value.id);
|
|
269
|
+
while (
|
|
270
|
+
c.value.type === NodeType.Async &&
|
|
271
|
+
this.hasValidMapping(c.value, context)
|
|
272
|
+
) {
|
|
273
|
+
const mappedNode = context.nodeResolveCache.get(c.value.id)!;
|
|
274
|
+
context.originalNodeCache.set(c.value.id, new Set([mappedNode]));
|
|
303
275
|
c.value = mappedNode;
|
|
304
276
|
c.value.parent = node;
|
|
305
277
|
}
|
|
@@ -318,31 +290,35 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
318
290
|
const result = await this.basePlugin?.hooks.onAsyncNode.call(
|
|
319
291
|
node,
|
|
320
292
|
(result) => {
|
|
321
|
-
this.parseNodeAndUpdate(node, context, result, options
|
|
293
|
+
this.parseNodeAndUpdate(node, context, result, options);
|
|
322
294
|
},
|
|
323
295
|
);
|
|
324
296
|
|
|
325
297
|
// Stop tracking before the next update is triggered
|
|
326
298
|
context.inProgressNodes.delete(node.id);
|
|
327
|
-
this.parseNodeAndUpdate(node, context, result, options
|
|
299
|
+
this.parseNodeAndUpdate(node, context, result, options);
|
|
328
300
|
} catch (e: unknown) {
|
|
329
|
-
const
|
|
330
|
-
const
|
|
331
|
-
|
|
332
|
-
if (
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
301
|
+
const error = e instanceof Error ? e : new Error(String(e));
|
|
302
|
+
const result = this.basePlugin?.hooks.onAsyncNodeError.call(error, node);
|
|
303
|
+
|
|
304
|
+
if (result === undefined) {
|
|
305
|
+
const playerState = this.basePlugin?.getPlayerInstance()?.getState();
|
|
306
|
+
|
|
307
|
+
if (playerState?.status === "in-progress") {
|
|
308
|
+
playerState.fail(error);
|
|
309
|
+
}
|
|
310
|
+
|
|
337
311
|
return;
|
|
338
312
|
}
|
|
339
313
|
|
|
340
|
-
|
|
341
|
-
node,
|
|
342
|
-
|
|
343
|
-
cause,
|
|
314
|
+
options.logger?.error(
|
|
315
|
+
"Async node handling failed and resolved with a fallback. Error:",
|
|
316
|
+
error,
|
|
344
317
|
);
|
|
345
|
-
|
|
318
|
+
|
|
319
|
+
// Stop tracking before the next update is triggered
|
|
320
|
+
context.inProgressNodes.delete(node.id);
|
|
321
|
+
this.parseNodeAndUpdate(node, context, result, options);
|
|
346
322
|
}
|
|
347
323
|
}
|
|
348
324
|
|
|
@@ -409,112 +385,15 @@ export class AsyncNodePluginPlugin implements AsyncNodeViewPlugin {
|
|
|
409
385
|
}
|
|
410
386
|
|
|
411
387
|
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
|
-
|
|
503
388
|
player.hooks.viewController.tap("async", (viewController) => {
|
|
504
389
|
viewController.hooks.view.tap("async", (view) => {
|
|
505
|
-
view.hooks.parser.tap(this.name, (p) => {
|
|
506
|
-
parser = p;
|
|
507
|
-
});
|
|
508
390
|
const context: AsyncPluginContext = {
|
|
391
|
+
nodeResolveCache: new Map(),
|
|
509
392
|
inProgressNodes: new Set(),
|
|
510
393
|
view,
|
|
511
394
|
viewController,
|
|
512
|
-
|
|
513
|
-
assetIdCache: new Map(),
|
|
514
|
-
asyncNodeCache: new Map(),
|
|
515
|
-
originalParentMap: new Map(),
|
|
395
|
+
originalNodeCache: new Map(),
|
|
516
396
|
};
|
|
517
|
-
currentContext = context;
|
|
518
397
|
|
|
519
398
|
view.hooks.resolver.tap("async", (resolver) => {
|
|
520
399
|
this.applyResolver(resolver, context);
|