Skip to content

Commit e0a3bd4

Browse files
authored
Merge pull request #340 from vim-denops/wait
👍 Automatically wait the target plugin on `denops.dispatch()`
2 parents 2a39384 + 897bda7 commit e0a3bd4

File tree

4 files changed

+81
-13
lines changed

4 files changed

+81
-13
lines changed

denops/@denops-private/denops.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ const isBatchReturn = is.TupleOf([is.Array, is.String] as const);
1313

1414
export type Host = Pick<HostOrigin, "redraw" | "call" | "batch">;
1515

16-
export type Service = Pick<ServiceOrigin, "dispatch">;
16+
export type Service = Pick<ServiceOrigin, "dispatch" | "waitLoaded">;
1717

1818
export class DenopsImpl implements Denops {
1919
readonly name: string;
@@ -67,12 +67,13 @@ export class DenopsImpl implements Denops {
6767
return this.#host.call("denops#api#eval", expr, ctx);
6868
}
6969

70-
dispatch(
70+
async dispatch(
7171
name: string,
7272
fn: string,
7373
...args: unknown[]
7474
): Promise<unknown> {
75-
return this.#service.dispatch(name, fn, args);
75+
await this.#service.waitLoaded(name);
76+
return await this.#service.dispatch(name, fn, args);
7677
}
7778
}
7879

denops/@denops-private/denops_test.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import type { Meta } from "https://deno.land/x/denops_core@v6.0.5/mod.ts";
2+
import { assertEquals } from "https://deno.land/std@0.217.0/assert/mod.ts";
23
import {
34
assertSpyCall,
45
stub,
56
} from "https://deno.land/std@0.217.0/testing/mock.ts";
67
import { DenopsImpl, Host, Service } from "./denops.ts";
8+
import { promiseState } from "https://deno.land/x/async@v2.1.0/mod.ts";
79
import { unimplemented } from "https://deno.land/x/errorutil@v0.1.1/mod.ts";
810

911
Deno.test("DenopsImpl", async (t) => {
@@ -20,6 +22,7 @@ Deno.test("DenopsImpl", async (t) => {
2022
};
2123
const service: Service = {
2224
dispatch: () => unimplemented(),
25+
waitLoaded: () => unimplemented(),
2326
};
2427
const denops = new DenopsImpl("dummy", meta, host, service);
2528

@@ -93,14 +96,41 @@ Deno.test("DenopsImpl", async (t) => {
9396
});
9497

9598
await t.step("dispatch() calls service.dispatch()", async () => {
96-
const s = stub(service, "dispatch");
99+
const s1 = stub(service, "waitLoaded", () => Promise.resolve());
100+
const s2 = stub(service, "dispatch", () => Promise.resolve());
97101
try {
98102
await denops.dispatch("dummy", "fn", "args");
99-
assertSpyCall(s, 0, {
103+
assertSpyCall(s1, 0, {
104+
args: ["dummy"],
105+
});
106+
assertSpyCall(s2, 0, {
100107
args: ["dummy", "fn", ["args"]],
101108
});
102109
} finally {
103-
s.restore();
110+
s1.restore();
111+
s2.restore();
104112
}
105113
});
114+
115+
await t.step(
116+
"dispatch() internally waits 'service.waitLoaded()' before 'service.dispatch()'",
117+
async () => {
118+
const { promise, resolve } = Promise.withResolvers<void>();
119+
const s1 = stub(service, "waitLoaded", () => promise);
120+
const s2 = stub(service, "dispatch", () => Promise.resolve());
121+
try {
122+
const p = denops.dispatch("dummy", "fn", "args");
123+
assertEquals(await promiseState(p), "pending");
124+
assertEquals(s1.calls.length, 1);
125+
assertEquals(s2.calls.length, 0);
126+
resolve();
127+
assertEquals(await promiseState(p), "fulfilled");
128+
assertEquals(s1.calls.length, 1);
129+
assertEquals(s2.calls.length, 1);
130+
} finally {
131+
s1.restore();
132+
s2.restore();
133+
}
134+
},
135+
);
106136
});

denops/@denops-private/service.ts

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,37 @@ import { toFileUrl } from "https://deno.land/std@0.217.0/path/mod.ts";
66
import { toErrorObject } from "https://deno.land/x/errorutil@v0.1.1/mod.ts";
77
import { DenopsImpl, Host } from "./denops.ts";
88

9+
// We can use `PromiseWithResolvers<void>` but Deno 1.38 doesn't have `PromiseWithResolvers`
10+
type Waiter = {
11+
promise: Promise<void>;
12+
resolve: () => void;
13+
};
14+
915
/**
1016
* Service manage plugins and is visible from the host (Vim/Neovim) through `invoke()` function.
1117
*/
1218
export class Service implements Disposable {
1319
#plugins: Map<string, Plugin> = new Map();
20+
#waiters: Map<string, Waiter> = new Map();
1421
#meta: Meta;
1522
#host?: Host;
1623

1724
constructor(meta: Meta) {
1825
this.#meta = meta;
1926
}
2027

28+
#getWaiter(name: string): Waiter {
29+
if (!this.#waiters.has(name)) {
30+
this.#waiters.set(name, Promise.withResolvers());
31+
}
32+
return this.#waiters.get(name)!;
33+
}
34+
2135
bind(host: Host): void {
2236
this.#host = host;
2337
}
2438

25-
load(
39+
async load(
2640
name: string,
2741
script: string,
2842
suffix = "",
@@ -35,12 +49,13 @@ export class Service implements Disposable {
3549
if (this.#meta.mode === "debug") {
3650
console.log(`A denops plugin '${name}' is already loaded. Skip`);
3751
}
38-
return Promise.resolve();
52+
return;
3953
}
4054
const denops = new DenopsImpl(name, this.#meta, this.#host, this);
4155
plugin = new Plugin(denops, name, script);
4256
this.#plugins.set(name, plugin);
43-
return plugin.load(suffix);
57+
await plugin.load(suffix);
58+
this.#getWaiter(name).resolve();
4459
}
4560

4661
reload(
@@ -54,12 +69,17 @@ export class Service implements Disposable {
5469
return Promise.resolve();
5570
}
5671
this.#plugins.delete(name);
72+
this.#waiters.delete(name);
5773
// Import module with fragment so that reload works properly
5874
// https://github.com/vim-denops/denops.vim/issues/227
5975
const suffix = `#${performance.now()}`;
6076
return this.load(name, plugin.script, suffix);
6177
}
6278

79+
waitLoaded(name: string): Promise<void> {
80+
return this.#getWaiter(name).promise;
81+
}
82+
6383
async #dispatch(name: string, fn: string, args: unknown[]): Promise<unknown> {
6484
const plugin = this.#plugins.get(name);
6585
if (!plugin) {

denops/@denops-private/service_test.ts

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
import {
22
assert,
3+
assertEquals,
34
assertMatch,
45
assertRejects,
5-
assertThrows,
66
} from "https://deno.land/std@0.217.0/assert/mod.ts";
77
import {
88
assertSpyCall,
99
assertSpyCalls,
1010
stub,
1111
} from "https://deno.land/std@0.217.0/testing/mock.ts";
1212
import type { Meta } from "https://deno.land/x/denops_core@v6.0.5/mod.ts";
13+
import { promiseState } from "https://deno.land/x/async@v2.1.0/mod.ts";
14+
import { unimplemented } from "https://deno.land/x/errorutil@v0.1.1/mod.ts";
1315
import type { Host } from "./denops.ts";
1416
import { Service } from "./service.ts";
15-
import { unimplemented } from "https://deno.land/x/errorutil@v0.1.1/mod.ts";
1617

1718
const scriptValid =
1819
new URL("./testdata/dummy_valid_plugin.ts", import.meta.url).href;
@@ -33,8 +34,8 @@ Deno.test("Service", async (t) => {
3334
};
3435
const service = new Service(meta);
3536

36-
await t.step("load() throws an error when no host is bound", () => {
37-
assertThrows(
37+
await t.step("load() rejects an error when no host is bound", async () => {
38+
await assertRejects(
3839
() => service.load("dummy", scriptValid),
3940
Error,
4041
"No host is bound to the service",
@@ -61,6 +62,15 @@ Deno.test("Service", async (t) => {
6162

6263
service.bind(host);
6364

65+
const waitLoaded = service.waitLoaded("dummy");
66+
67+
await t.step(
68+
"the result promise of waitLoaded() is 'pending' when the plugin is not loaded yet",
69+
async () => {
70+
assertEquals(await promiseState(waitLoaded), "pending");
71+
},
72+
);
73+
6474
await t.step("load() loads plugin and emits autocmd events", async () => {
6575
const s = stub(host, "call");
6676
try {
@@ -92,6 +102,13 @@ Deno.test("Service", async (t) => {
92102
}
93103
});
94104

105+
await t.step(
106+
"the result promise of waitLoaded() become 'fulfilled' when the plugin is loaded",
107+
async () => {
108+
assertEquals(await promiseState(waitLoaded), "fulfilled");
109+
},
110+
);
111+
95112
await t.step(
96113
"load() loads plugin and emits autocmd events (failure)",
97114
async () => {

0 commit comments

Comments
 (0)