@akashic/headless-driver 2.6.2 → 2.7.0

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.
@@ -1,10 +1,17 @@
1
1
  import type { Permission } from "@akashic/amflow";
2
2
  import { AMFlowClient } from "./amflow/AMFlowClient";
3
+ import type { AMFlowStoreOptions } from "./amflow/AMFlowStore";
3
4
  export declare class AMFlowClientManager {
4
5
  /**
5
6
  * PlayId と AMFlowStore を紐付けるマップ情報。
6
7
  */
7
8
  private storeMap;
9
+ /**
10
+ * 対象の PlayID の オプションを設定する。
11
+ * @param playId PlayID
12
+ * @param options オプション
13
+ */
14
+ setAMFlowStoreOptions(playId: string, options: AMFlowStoreOptions | null): void;
8
15
  /**
9
16
  * 対象の PlayID の AMFlowClient を作成する。
10
17
  * @param playId PlayID
@@ -10,6 +10,18 @@ class AMFlowClientManager {
10
10
  */
11
11
  this.storeMap = new Map();
12
12
  }
13
+ /**
14
+ * 対象の PlayID の オプションを設定する。
15
+ * @param playId PlayID
16
+ * @param options オプション
17
+ */
18
+ setAMFlowStoreOptions(playId, options) {
19
+ let store = this.storeMap.get(playId);
20
+ if (!store) {
21
+ store = this.createAMFlowStore(playId);
22
+ }
23
+ store.setOptions(options);
24
+ }
13
25
  /**
14
26
  * 対象の PlayID の AMFlowClient を作成する。
15
27
  * @param playId PlayID
@@ -1,4 +1,5 @@
1
1
  import type { Permission } from "@akashic/amflow";
2
+ import type { AMFlowStoreOptions } from "./amflow";
2
3
  import type { AMFlowClient } from "./amflow/AMFlowClient";
3
4
  import type { DumpedPlaylog } from "./amflow/types";
4
5
  import type { ContentLocation } from "./Content";
@@ -18,7 +19,7 @@ export declare class PlayManager {
18
19
  * Play を作成する。
19
20
  * @param playLocation パラメータ
20
21
  */
21
- createPlay(playLocation: PlayManagerParameters, playlog?: DumpedPlaylog): Promise<string>;
22
+ createPlay(playLocation: PlayManagerParameters, playlog?: DumpedPlaylog, options?: AMFlowStoreOptions): Promise<string>;
22
23
  /**
23
24
  * Play を削除する。
24
25
  * @param playID PlayID
@@ -24,10 +24,12 @@ class PlayManager {
24
24
  * Play を作成する。
25
25
  * @param playLocation パラメータ
26
26
  */
27
- createPlay(playLocation, playlog) {
27
+ createPlay(playLocation, playlog, options) {
28
28
  return __awaiter(this, void 0, void 0, function* () {
29
29
  const playId = `${this.nextPlayId++}`;
30
30
  this.plays.push(Object.assign({ playId, status: "running", createdAt: Date.now(), lastSuspendedAt: null }, playLocation));
31
+ if (options)
32
+ this.amflowClientManager.setAMFlowStoreOptions(playId, options);
31
33
  if (playlog) {
32
34
  const amflow = this.createAMFlow(playId);
33
35
  amflow.setDumpedPlaylog(playlog);
@@ -15,7 +15,7 @@ export declare class AMFlowClient implements AMFlow {
15
15
  private permission;
16
16
  private tickHandlers;
17
17
  private eventHandlers;
18
- private unconsumedEvents;
18
+ private waitingStartPointRequests;
19
19
  constructor(playId: string, store: AMFlowStore);
20
20
  open(playId: string, callback?: (error: Error | null) => void): void;
21
21
  close(callback?: (error: Error | null) => void): void;
@@ -14,14 +14,39 @@ class AMFlowClient {
14
14
  this.permission = null;
15
15
  this.tickHandlers = [];
16
16
  this.eventHandlers = [];
17
- this.unconsumedEvents = [];
17
+ this.waitingStartPointRequests = [];
18
+ this.handleSendTick = (tick) => {
19
+ this.tickHandlers.forEach((h) => h(tick));
20
+ };
21
+ this.handleSendEvent = (event) => {
22
+ this.eventHandlers.forEach((h) => h(event));
23
+ };
24
+ this.handlePutStartPoint = (startPoint) => {
25
+ this.onPutStartPoint.fire(startPoint);
26
+ if (this.waitingStartPointRequests.length) {
27
+ const remains = [];
28
+ this.waitingStartPointRequests.forEach((req) => {
29
+ const { frame, timestamp } = req.options;
30
+ if ((frame != null && startPoint.frame <= frame) ||
31
+ (timestamp != null && startPoint.timestamp < timestamp) ||
32
+ (frame == null && timestamp == null && startPoint.frame === 0)) {
33
+ req.callback(null, startPoint);
34
+ return;
35
+ }
36
+ if (startPoint.frame === 0) {
37
+ req.callback((0, ErrorFactory_1.createError)("bad_request"));
38
+ return;
39
+ }
40
+ remains.push(req);
41
+ });
42
+ this.waitingStartPointRequests = remains;
43
+ }
44
+ };
18
45
  this.playId = playId;
19
46
  this.store = store;
20
47
  }
21
48
  open(playId, callback) {
22
49
  (0, Logger_1.getSystemLogger)().info("AMFlowClient#open()", playId);
23
- this.store.sendEventTrigger.add(this.handleSendEvent, this);
24
- this.store.sendTickTrigger.add(this.handleSendTick, this);
25
50
  this.store.putStartPointTrigger.add(this.handlePutStartPoint, this);
26
51
  this.state = "open";
27
52
  if (callback) {
@@ -101,6 +126,8 @@ class AMFlowClient {
101
126
  throw (0, ErrorFactory_1.createError)("permission_error", "Permission denied");
102
127
  }
103
128
  this.tickHandlers.push(handler);
129
+ if (this.tickHandlers.length === 1)
130
+ this.store.onTick(this.handleSendTick);
104
131
  }
105
132
  offTick(handler) {
106
133
  if (this.state !== "open") {
@@ -110,6 +137,8 @@ class AMFlowClient {
110
137
  throw (0, ErrorFactory_1.createError)("invalid_status", "Not authenticated");
111
138
  }
112
139
  this.tickHandlers = this.tickHandlers.filter((h) => h !== handler);
140
+ if (this.tickHandlers.length === 0)
141
+ this.store.offTick(this.handleSendTick);
113
142
  }
114
143
  sendEvent(event) {
115
144
  if (this.state !== "open") {
@@ -138,12 +167,8 @@ class AMFlowClient {
138
167
  throw (0, ErrorFactory_1.createError)("permission_error", "Permission denied");
139
168
  }
140
169
  this.eventHandlers.push(handler);
141
- if (0 < this.unconsumedEvents.length) {
142
- this.eventHandlers.forEach((h) => {
143
- this.unconsumedEvents.forEach((ev) => h(ev));
144
- });
145
- this.unconsumedEvents = [];
146
- }
170
+ if (this.eventHandlers.length === 1)
171
+ this.store.onEvent(this.handleSendEvent);
147
172
  }
148
173
  offEvent(handler) {
149
174
  if (this.state !== "open") {
@@ -153,6 +178,8 @@ class AMFlowClient {
153
178
  throw (0, ErrorFactory_1.createError)("invalid_status", "Not authenticated");
154
179
  }
155
180
  this.eventHandlers = this.eventHandlers.filter((h) => h !== handler);
181
+ if (this.eventHandlers.length === 0)
182
+ this.store.offEvent(this.handleSendEvent);
156
183
  }
157
184
  getTickList(optsOrBegin, endOrCallback, callbackOrUndefined) {
158
185
  setImmediate(() => {
@@ -229,12 +256,11 @@ class AMFlowClient {
229
256
  return;
230
257
  }
231
258
  const startPoint = this.store.getStartPoint(opts);
232
- if (startPoint) {
233
- callback(null, startPoint);
234
- }
235
- else {
236
- callback((0, ErrorFactory_1.createError)("runtime_error", "No start point"), undefined);
259
+ if (!startPoint) {
260
+ this.waitingStartPointRequests.push({ options: opts, callback });
261
+ return;
237
262
  }
263
+ callback(null, startPoint);
238
264
  });
239
265
  }
240
266
  /* eslint-disable @typescript-eslint/no-unused-vars */
@@ -258,15 +284,15 @@ class AMFlowClient {
258
284
  return;
259
285
  }
260
286
  if (!this.store.isDestroyed()) {
261
- this.store.sendEventTrigger.remove(this.handleSendEvent, this);
262
- this.store.sendTickTrigger.remove(this.handleSendTick, this);
287
+ this.store.offEvent(this.handleSendEvent);
288
+ this.store.offTick(this.handleSendTick);
263
289
  this.store.putStartPointTrigger.remove(this.handlePutStartPoint, this);
264
290
  }
265
291
  this.store = null;
266
292
  this.permission = null;
267
293
  this.tickHandlers = null;
268
294
  this.eventHandlers = null;
269
- this.unconsumedEvents = null;
295
+ this.waitingStartPointRequests = null;
270
296
  this.onPutStartPoint = null;
271
297
  }
272
298
  isDestroyed() {
@@ -275,18 +301,5 @@ class AMFlowClient {
275
301
  dump() {
276
302
  return this.store.dump();
277
303
  }
278
- handleSendTick(tick) {
279
- this.tickHandlers.forEach((h) => h(tick));
280
- }
281
- handleSendEvent(event) {
282
- if (this.eventHandlers.length <= 0) {
283
- this.unconsumedEvents.push(event);
284
- return;
285
- }
286
- this.eventHandlers.forEach((h) => h(event));
287
- }
288
- handlePutStartPoint(startPoint) {
289
- this.onPutStartPoint.fire(startPoint);
290
- }
291
304
  }
292
305
  exports.AMFlowClient = AMFlowClient;
@@ -2,24 +2,40 @@ import type { GetStartPointOptions, GetTickListOptions, Permission, StartPoint }
2
2
  import type { Event, Tick, TickList } from "@akashic/playlog";
3
3
  import { Trigger } from "@akashic/trigger";
4
4
  import type { DumpedPlaylog } from "./types";
5
+ export interface AMFlowStoreOptions {
6
+ /**
7
+ * 受信されないイベントをバッファリングするか。
8
+ *
9
+ * 真の場合、受信者が一つもない状態で送信されたイベントをバッファに保持する。
10
+ * 保持されたイベントは、最初のイベントハンドラが (AMFlow#onEvent() で) 登録された時、そのハンドラにすべて引き渡される。
11
+ * (二つ目以降のハンドラは受け取れないことに注意)
12
+ */
13
+ preservesUnhandledEvents?: boolean;
14
+ }
5
15
  /**
6
16
  * AMFlow のストア。
7
17
  * 一つのプレーに対して一つ存在する。
8
18
  */
9
19
  export declare class AMFlowStore {
10
20
  playId: string;
21
+ putStartPointTrigger: Trigger<StartPoint>;
11
22
  sendEventTrigger: Trigger<Event>;
12
23
  sendTickTrigger: Trigger<Tick>;
13
- putStartPointTrigger: Trigger<StartPoint>;
14
24
  private permissionMap;
15
25
  private startPoints;
16
26
  private unfilteredTickList;
17
27
  private filteredTickList;
18
28
  private suspended;
29
+ private options;
30
+ private unhandledEvents;
19
31
  constructor(playId: string);
20
32
  authenticate(token: string, revoke?: boolean): Permission;
21
33
  sendTick(tick: Tick): void;
22
34
  sendEvent(event: Event): void;
35
+ onTick(handler: (tick: Tick) => void): void;
36
+ offTick(handler: (tick: Tick) => void): void;
37
+ onEvent(handler: (event: Event) => void): void;
38
+ offEvent(handler: (event: Event) => void): void;
23
39
  getTickList(opts: GetTickListOptions): TickList | null;
24
40
  putStartPoint(startPoint: StartPoint): void;
25
41
  getStartPoint(opts: GetStartPointOptions): StartPoint | null;
@@ -41,6 +57,7 @@ export declare class AMFlowStore {
41
57
  resume(): void;
42
58
  setTickList(tickList: TickList | null): void;
43
59
  setDumpedPlaylog(dumped: DumpedPlaylog): void;
60
+ setOptions(options: AMFlowStoreOptions | null): void;
44
61
  destroy(): void;
45
62
  isDestroyed(): boolean;
46
63
  createPlayToken(permission: Permission): string;
@@ -11,13 +11,16 @@ const ErrorFactory_1 = require("./ErrorFactory");
11
11
  */
12
12
  class AMFlowStore {
13
13
  constructor(playId) {
14
+ this.putStartPointTrigger = new trigger_1.Trigger();
15
+ // 現状外部から参照する必要はないが、互換性のため少なくとも 2.x.x の間は公開する
14
16
  this.sendEventTrigger = new trigger_1.Trigger();
15
17
  this.sendTickTrigger = new trigger_1.Trigger();
16
- this.putStartPointTrigger = new trigger_1.Trigger();
17
18
  this.permissionMap = new Map();
18
19
  this.startPoints = [];
19
20
  this.unfilteredTickList = null;
20
21
  this.filteredTickList = null;
22
+ this.options = null;
23
+ this.unhandledEvents = [];
21
24
  this.playId = playId;
22
25
  this.suspended = false;
23
26
  }
@@ -39,10 +42,32 @@ class AMFlowStore {
39
42
  this.sendTickTrigger.fire(tick);
40
43
  }
41
44
  sendEvent(event) {
45
+ var _a;
42
46
  if (this.isSuspended()) {
43
47
  throw (0, ErrorFactory_1.createError)("bad_request", "Play may be suspended");
44
48
  }
45
- this.sendEventTrigger.fire(this.cloneDeep(event));
49
+ const ev = this.cloneDeep(event);
50
+ if (((_a = this.options) === null || _a === void 0 ? void 0 : _a.preservesUnhandledEvents) && this.sendEventTrigger.length === 0) {
51
+ this.unhandledEvents.push(ev);
52
+ return;
53
+ }
54
+ this.sendEventTrigger.fire(ev);
55
+ }
56
+ onTick(handler) {
57
+ this.sendTickTrigger.add(handler);
58
+ }
59
+ offTick(handler) {
60
+ this.sendTickTrigger.remove(handler);
61
+ }
62
+ onEvent(handler) {
63
+ this.sendEventTrigger.add(handler);
64
+ if (0 < this.unhandledEvents.length) {
65
+ this.unhandledEvents.forEach((ev) => this.sendEventTrigger.fire(ev));
66
+ this.unhandledEvents = [];
67
+ }
68
+ }
69
+ offEvent(handler) {
70
+ this.sendEventTrigger.remove(handler);
46
71
  }
47
72
  getTickList(opts) {
48
73
  if (!(this.unfilteredTickList && this.filteredTickList)) {
@@ -133,6 +158,9 @@ class AMFlowStore {
133
158
  this.setTickList(dumped.tickList);
134
159
  dumped.startPoints.forEach((sp) => this.putStartPoint(sp));
135
160
  }
161
+ setOptions(options) {
162
+ this.options = options;
163
+ }
136
164
  destroy() {
137
165
  if (this.isDestroyed()) {
138
166
  return;
@@ -143,6 +171,7 @@ class AMFlowStore {
143
171
  this.sendTickTrigger = null;
144
172
  this.permissionMap = null;
145
173
  this.startPoints = null;
174
+ this.unhandledEvents = null;
146
175
  this.putStartPointTrigger = null;
147
176
  }
148
177
  isDestroyed() {
package/lib/utils.d.ts CHANGED
@@ -2,7 +2,19 @@
2
2
  * ファイルを読み込む。
3
3
  *
4
4
  * @param url url または path
5
- * @param opt オプション
6
5
  */
7
6
  export declare function loadFile(url: string): Promise<string>;
7
+ /**
8
+ * loadFile() の内部実装のうち、特にテストのため外部から参照するものをまとめた namespace 。
9
+ * 通常の利用では参照する必要はない。
10
+ */
11
+ export declare namespace LoadFileInternal {
12
+ /**
13
+ * 最大並列ロード数。
14
+ * 環境によって、node-fetch を一定数以上並列に実行すると応答がなくなったりエラーになる場合がある。
15
+ * 具体的な値は調整の余地がある。少なくとも Windows 環境で、128 以上で問題が起きることがわかっている。
16
+ */
17
+ const MAX_PARALLEL_LOAD = 32;
18
+ function loadImpl(url: string): Promise<string>;
19
+ }
8
20
  export declare function isHttpProtocol(url: string): boolean;
package/lib/utils.js CHANGED
@@ -9,28 +9,70 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.isHttpProtocol = exports.loadFile = void 0;
12
+ exports.isHttpProtocol = exports.LoadFileInternal = exports.loadFile = void 0;
13
13
  const fs = require("fs");
14
14
  const node_fetch_1 = require("node-fetch");
15
+ const waitings = [];
16
+ let loadingCount = 0;
15
17
  /**
16
18
  * ファイルを読み込む。
17
19
  *
18
20
  * @param url url または path
19
- * @param opt オプション
20
21
  */
21
22
  function loadFile(url) {
22
23
  return __awaiter(this, void 0, void 0, function* () {
23
- if (isHttpProtocol(url)) {
24
- const res = yield (0, node_fetch_1.default)(url, { method: "GET" });
25
- return res.text();
24
+ const promise = new Promise((resolve, reject) => {
25
+ waitings.push({ url, resolve, reject });
26
+ });
27
+ processWaitingLoad();
28
+ return promise;
29
+ });
30
+ }
31
+ exports.loadFile = loadFile;
32
+ function processWaitingLoad() {
33
+ return __awaiter(this, void 0, void 0, function* () {
34
+ if (loadingCount >= LoadFileInternal.MAX_PARALLEL_LOAD || waitings.length === 0)
35
+ return;
36
+ const { url, resolve, reject } = waitings.shift();
37
+ try {
38
+ ++loadingCount;
39
+ resolve(yield LoadFileInternal.loadImpl(url));
26
40
  }
27
- else {
28
- const str = fs.readFileSync(url, { encoding: "utf8" });
29
- return str;
41
+ catch (e) {
42
+ reject(e);
43
+ }
44
+ finally {
45
+ --loadingCount;
46
+ processWaitingLoad();
30
47
  }
31
48
  });
32
49
  }
33
- exports.loadFile = loadFile;
50
+ /**
51
+ * loadFile() の内部実装のうち、特にテストのため外部から参照するものをまとめた namespace 。
52
+ * 通常の利用では参照する必要はない。
53
+ */
54
+ var LoadFileInternal;
55
+ (function (LoadFileInternal) {
56
+ /**
57
+ * 最大並列ロード数。
58
+ * 環境によって、node-fetch を一定数以上並列に実行すると応答がなくなったりエラーになる場合がある。
59
+ * 具体的な値は調整の余地がある。少なくとも Windows 環境で、128 以上で問題が起きることがわかっている。
60
+ */
61
+ LoadFileInternal.MAX_PARALLEL_LOAD = 32;
62
+ function loadImpl(url) {
63
+ return __awaiter(this, void 0, void 0, function* () {
64
+ if (isHttpProtocol(url)) {
65
+ const res = yield (0, node_fetch_1.default)(url, { method: "GET" });
66
+ return res.text();
67
+ }
68
+ else {
69
+ const str = fs.readFileSync(url, { encoding: "utf8" });
70
+ return str;
71
+ }
72
+ });
73
+ }
74
+ LoadFileInternal.loadImpl = loadImpl;
75
+ })(LoadFileInternal = exports.LoadFileInternal || (exports.LoadFileInternal = {}));
34
76
  function isHttpProtocol(url) {
35
77
  return /^(http|https)\:\/\//.test(url);
36
78
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@akashic/headless-driver",
3
- "version": "2.6.2",
3
+ "version": "2.7.0",
4
4
  "description": "A library to execute contents using Akashic Engine headlessly",
5
5
  "main": "lib/index.js",
6
6
  "author": "DWANGO Co., Ltd.",
@@ -55,7 +55,7 @@
55
55
  "jest": "^29.1.2",
56
56
  "npm-run-all": "^4.1.5",
57
57
  "pixelmatch": "^5.3.0",
58
- "pngjs": "^6.0.0",
58
+ "pngjs": "^7.0.0",
59
59
  "prettier": "^2.7.1",
60
60
  "remark-cli": "^11.0.0",
61
61
  "rimraf": "^4.0.0",