@link-assistant/agent 0.0.8 → 0.0.11
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/EXAMPLES.md +80 -1
- package/MODELS.md +72 -24
- package/README.md +95 -2
- package/TOOLS.md +20 -0
- package/package.json +36 -2
- package/src/agent/agent.ts +68 -54
- package/src/auth/claude-oauth.ts +426 -0
- package/src/auth/index.ts +28 -26
- package/src/auth/plugins.ts +876 -0
- package/src/bun/index.ts +53 -43
- package/src/bus/global.ts +5 -5
- package/src/bus/index.ts +59 -53
- package/src/cli/bootstrap.js +12 -12
- package/src/cli/bootstrap.ts +6 -6
- package/src/cli/cmd/agent.ts +97 -92
- package/src/cli/cmd/auth.ts +468 -0
- package/src/cli/cmd/cmd.ts +2 -2
- package/src/cli/cmd/export.ts +41 -41
- package/src/cli/cmd/mcp.ts +210 -53
- package/src/cli/cmd/models.ts +30 -29
- package/src/cli/cmd/run.ts +269 -213
- package/src/cli/cmd/stats.ts +185 -146
- package/src/cli/error.ts +17 -13
- package/src/cli/ui.ts +78 -0
- package/src/command/index.ts +26 -26
- package/src/config/config.ts +528 -288
- package/src/config/markdown.ts +15 -15
- package/src/file/ripgrep.ts +201 -169
- package/src/file/time.ts +21 -18
- package/src/file/watcher.ts +51 -42
- package/src/file.ts +1 -1
- package/src/flag/flag.ts +26 -11
- package/src/format/formatter.ts +206 -162
- package/src/format/index.ts +61 -61
- package/src/global/index.ts +21 -21
- package/src/id/id.ts +47 -33
- package/src/index.js +554 -332
- package/src/json-standard/index.ts +173 -0
- package/src/mcp/index.ts +135 -128
- package/src/patch/index.ts +336 -267
- package/src/project/bootstrap.ts +15 -15
- package/src/project/instance.ts +43 -36
- package/src/project/project.ts +47 -47
- package/src/project/state.ts +37 -33
- package/src/provider/models-macro.ts +5 -5
- package/src/provider/models.ts +32 -32
- package/src/provider/opencode.js +19 -19
- package/src/provider/provider.ts +518 -277
- package/src/provider/transform.ts +143 -102
- package/src/server/project.ts +21 -21
- package/src/server/server.ts +111 -105
- package/src/session/agent.js +66 -60
- package/src/session/compaction.ts +136 -111
- package/src/session/index.ts +189 -156
- package/src/session/message-v2.ts +312 -268
- package/src/session/message.ts +73 -57
- package/src/session/processor.ts +180 -166
- package/src/session/prompt.ts +678 -533
- package/src/session/retry.ts +26 -23
- package/src/session/revert.ts +76 -62
- package/src/session/status.ts +26 -26
- package/src/session/summary.ts +97 -76
- package/src/session/system.ts +77 -63
- package/src/session/todo.ts +22 -16
- package/src/snapshot/index.ts +92 -76
- package/src/storage/storage.ts +157 -120
- package/src/tool/bash.ts +116 -106
- package/src/tool/batch.ts +73 -59
- package/src/tool/codesearch.ts +60 -53
- package/src/tool/edit.ts +319 -263
- package/src/tool/glob.ts +32 -28
- package/src/tool/grep.ts +72 -53
- package/src/tool/invalid.ts +7 -7
- package/src/tool/ls.ts +77 -64
- package/src/tool/multiedit.ts +30 -21
- package/src/tool/patch.ts +121 -94
- package/src/tool/read.ts +140 -122
- package/src/tool/registry.ts +38 -38
- package/src/tool/task.ts +93 -60
- package/src/tool/todo.ts +16 -16
- package/src/tool/tool.ts +45 -36
- package/src/tool/webfetch.ts +97 -74
- package/src/tool/websearch.ts +78 -64
- package/src/tool/write.ts +21 -15
- package/src/util/binary.ts +27 -19
- package/src/util/context.ts +8 -8
- package/src/util/defer.ts +7 -5
- package/src/util/error.ts +24 -19
- package/src/util/eventloop.ts +16 -10
- package/src/util/filesystem.ts +37 -33
- package/src/util/fn.ts +11 -8
- package/src/util/iife.ts +1 -1
- package/src/util/keybind.ts +44 -44
- package/src/util/lazy.ts +7 -7
- package/src/util/locale.ts +20 -16
- package/src/util/lock.ts +43 -38
- package/src/util/log.ts +95 -85
- package/src/util/queue.ts +8 -8
- package/src/util/rpc.ts +35 -23
- package/src/util/scrap.ts +4 -4
- package/src/util/signal.ts +5 -5
- package/src/util/timeout.ts +6 -6
- package/src/util/token.ts +2 -2
- package/src/util/wildcard.ts +38 -27
package/src/util/error.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import z from
|
|
1
|
+
import z from 'zod';
|
|
2
2
|
|
|
3
3
|
export abstract class NamedError extends Error {
|
|
4
|
-
abstract schema(): z.core.$ZodType
|
|
5
|
-
abstract toObject(): { name: string; data: any }
|
|
4
|
+
abstract schema(): z.core.$ZodType;
|
|
5
|
+
abstract toObject(): { name: string; data: any };
|
|
6
6
|
|
|
7
|
-
static create<Name extends string, Data extends z.core.$ZodType>(
|
|
7
|
+
static create<Name extends string, Data extends z.core.$ZodType>(
|
|
8
|
+
name: Name,
|
|
9
|
+
data: Data
|
|
10
|
+
) {
|
|
8
11
|
const schema = z
|
|
9
12
|
.object({
|
|
10
13
|
name: z.literal(name),
|
|
@@ -12,43 +15,45 @@ export abstract class NamedError extends Error {
|
|
|
12
15
|
})
|
|
13
16
|
.meta({
|
|
14
17
|
ref: name,
|
|
15
|
-
})
|
|
18
|
+
});
|
|
16
19
|
const result = class extends NamedError {
|
|
17
|
-
public static readonly Schema = schema
|
|
20
|
+
public static readonly Schema = schema;
|
|
18
21
|
|
|
19
|
-
public override readonly name = name as Name
|
|
22
|
+
public override readonly name = name as Name;
|
|
20
23
|
|
|
21
24
|
constructor(
|
|
22
25
|
public readonly data: z.input<Data>,
|
|
23
|
-
options?: ErrorOptions
|
|
26
|
+
options?: ErrorOptions
|
|
24
27
|
) {
|
|
25
|
-
super(name, options)
|
|
26
|
-
this.name = name
|
|
28
|
+
super(name, options);
|
|
29
|
+
this.name = name;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
static isInstance(input: any): input is InstanceType<typeof result> {
|
|
30
|
-
return
|
|
33
|
+
return (
|
|
34
|
+
typeof input === 'object' && 'name' in input && input.name === name
|
|
35
|
+
);
|
|
31
36
|
}
|
|
32
37
|
|
|
33
38
|
schema() {
|
|
34
|
-
return schema
|
|
39
|
+
return schema;
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
toObject() {
|
|
38
43
|
return {
|
|
39
44
|
name: name,
|
|
40
45
|
data: this.data,
|
|
41
|
-
}
|
|
46
|
+
};
|
|
42
47
|
}
|
|
43
|
-
}
|
|
44
|
-
Object.defineProperty(result,
|
|
45
|
-
return result
|
|
48
|
+
};
|
|
49
|
+
Object.defineProperty(result, 'name', { value: name });
|
|
50
|
+
return result;
|
|
46
51
|
}
|
|
47
52
|
|
|
48
53
|
public static readonly Unknown = NamedError.create(
|
|
49
|
-
|
|
54
|
+
'UnknownError',
|
|
50
55
|
z.object({
|
|
51
56
|
message: z.string(),
|
|
52
|
-
})
|
|
53
|
-
)
|
|
57
|
+
})
|
|
58
|
+
);
|
|
54
59
|
}
|
package/src/util/eventloop.ts
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
|
-
import { Log } from
|
|
1
|
+
import { Log } from './log';
|
|
2
2
|
|
|
3
3
|
export namespace EventLoop {
|
|
4
4
|
export async function wait() {
|
|
5
5
|
return new Promise<void>((resolve) => {
|
|
6
6
|
const check = () => {
|
|
7
|
-
const active = [
|
|
8
|
-
|
|
7
|
+
const active = [
|
|
8
|
+
...(process as any)._getActiveHandles(),
|
|
9
|
+
...(process as any)._getActiveRequests(),
|
|
10
|
+
];
|
|
11
|
+
Log.Default.info('eventloop', {
|
|
9
12
|
active,
|
|
10
|
-
})
|
|
11
|
-
if (
|
|
12
|
-
|
|
13
|
+
});
|
|
14
|
+
if (
|
|
15
|
+
(process as any)._getActiveHandles().length === 0 &&
|
|
16
|
+
(process as any)._getActiveRequests().length === 0
|
|
17
|
+
) {
|
|
18
|
+
resolve();
|
|
13
19
|
} else {
|
|
14
|
-
setImmediate(check)
|
|
20
|
+
setImmediate(check);
|
|
15
21
|
}
|
|
16
|
-
}
|
|
17
|
-
check()
|
|
18
|
-
})
|
|
22
|
+
};
|
|
23
|
+
check();
|
|
24
|
+
});
|
|
19
25
|
}
|
|
20
26
|
}
|
package/src/util/filesystem.ts
CHANGED
|
@@ -1,52 +1,56 @@
|
|
|
1
|
-
import { exists } from
|
|
2
|
-
import { dirname, join, relative } from
|
|
1
|
+
import { exists } from 'fs/promises';
|
|
2
|
+
import { dirname, join, relative } from 'path';
|
|
3
3
|
|
|
4
4
|
export namespace Filesystem {
|
|
5
5
|
export function overlaps(a: string, b: string) {
|
|
6
|
-
const relA = relative(a, b)
|
|
7
|
-
const relB = relative(b, a)
|
|
8
|
-
return !relA || !relA.startsWith(
|
|
6
|
+
const relA = relative(a, b);
|
|
7
|
+
const relB = relative(b, a);
|
|
8
|
+
return !relA || !relA.startsWith('..') || !relB || !relB.startsWith('..');
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function contains(parent: string, child: string) {
|
|
12
|
-
return !relative(parent, child).startsWith(
|
|
12
|
+
return !relative(parent, child).startsWith('..');
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export async function findUp(target: string, start: string, stop?: string) {
|
|
16
|
-
let current = start
|
|
17
|
-
const result = []
|
|
16
|
+
let current = start;
|
|
17
|
+
const result = [];
|
|
18
18
|
while (true) {
|
|
19
|
-
const search = join(current, target)
|
|
20
|
-
if (await exists(search)) result.push(search)
|
|
21
|
-
if (stop === current) break
|
|
22
|
-
const parent = dirname(current)
|
|
23
|
-
if (parent === current) break
|
|
24
|
-
current = parent
|
|
19
|
+
const search = join(current, target);
|
|
20
|
+
if (await exists(search)) result.push(search);
|
|
21
|
+
if (stop === current) break;
|
|
22
|
+
const parent = dirname(current);
|
|
23
|
+
if (parent === current) break;
|
|
24
|
+
current = parent;
|
|
25
25
|
}
|
|
26
|
-
return result
|
|
26
|
+
return result;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export async function* up(options: {
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
export async function* up(options: {
|
|
30
|
+
targets: string[];
|
|
31
|
+
start: string;
|
|
32
|
+
stop?: string;
|
|
33
|
+
}) {
|
|
34
|
+
const { targets, start, stop } = options;
|
|
35
|
+
let current = start;
|
|
32
36
|
while (true) {
|
|
33
37
|
for (const target of targets) {
|
|
34
|
-
const search = join(current, target)
|
|
35
|
-
if (await exists(search)) yield search
|
|
38
|
+
const search = join(current, target);
|
|
39
|
+
if (await exists(search)) yield search;
|
|
36
40
|
}
|
|
37
|
-
if (stop === current) break
|
|
38
|
-
const parent = dirname(current)
|
|
39
|
-
if (parent === current) break
|
|
40
|
-
current = parent
|
|
41
|
+
if (stop === current) break;
|
|
42
|
+
const parent = dirname(current);
|
|
43
|
+
if (parent === current) break;
|
|
44
|
+
current = parent;
|
|
41
45
|
}
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
export async function globUp(pattern: string, start: string, stop?: string) {
|
|
45
|
-
let current = start
|
|
46
|
-
const result = []
|
|
49
|
+
let current = start;
|
|
50
|
+
const result = [];
|
|
47
51
|
while (true) {
|
|
48
52
|
try {
|
|
49
|
-
const glob = new Bun.Glob(pattern)
|
|
53
|
+
const glob = new Bun.Glob(pattern);
|
|
50
54
|
for await (const match of glob.scan({
|
|
51
55
|
cwd: current,
|
|
52
56
|
absolute: true,
|
|
@@ -54,16 +58,16 @@ export namespace Filesystem {
|
|
|
54
58
|
followSymlinks: true,
|
|
55
59
|
dot: true,
|
|
56
60
|
})) {
|
|
57
|
-
result.push(match)
|
|
61
|
+
result.push(match);
|
|
58
62
|
}
|
|
59
63
|
} catch {
|
|
60
64
|
// Skip invalid glob patterns
|
|
61
65
|
}
|
|
62
|
-
if (stop === current) break
|
|
63
|
-
const parent = dirname(current)
|
|
64
|
-
if (parent === current) break
|
|
65
|
-
current = parent
|
|
66
|
+
if (stop === current) break;
|
|
67
|
+
const parent = dirname(current);
|
|
68
|
+
if (parent === current) break;
|
|
69
|
+
current = parent;
|
|
66
70
|
}
|
|
67
|
-
return result
|
|
71
|
+
return result;
|
|
68
72
|
}
|
|
69
73
|
}
|
package/src/util/fn.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { z } from
|
|
1
|
+
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
-
export function fn<T extends z.ZodType, Result>(
|
|
3
|
+
export function fn<T extends z.ZodType, Result>(
|
|
4
|
+
schema: T,
|
|
5
|
+
cb: (input: z.infer<T>) => Result
|
|
6
|
+
) {
|
|
4
7
|
const result = (input: z.infer<T>) => {
|
|
5
|
-
const parsed = schema.parse(input)
|
|
6
|
-
return cb(parsed)
|
|
7
|
-
}
|
|
8
|
-
result.force = (input: z.infer<T>) => cb(input)
|
|
9
|
-
result.schema = schema
|
|
10
|
-
return result
|
|
8
|
+
const parsed = schema.parse(input);
|
|
9
|
+
return cb(parsed);
|
|
10
|
+
};
|
|
11
|
+
result.force = (input: z.infer<T>) => cb(input);
|
|
12
|
+
result.schema = schema;
|
|
13
|
+
return result;
|
|
11
14
|
}
|
package/src/util/iife.ts
CHANGED
package/src/util/keybind.ts
CHANGED
|
@@ -1,79 +1,79 @@
|
|
|
1
|
-
import { isDeepEqual } from
|
|
1
|
+
import { isDeepEqual } from 'remeda';
|
|
2
2
|
|
|
3
3
|
export namespace Keybind {
|
|
4
4
|
export type Info = {
|
|
5
|
-
ctrl: boolean
|
|
6
|
-
meta: boolean
|
|
7
|
-
shift: boolean
|
|
8
|
-
leader: boolean
|
|
9
|
-
name: string
|
|
10
|
-
}
|
|
5
|
+
ctrl: boolean;
|
|
6
|
+
meta: boolean;
|
|
7
|
+
shift: boolean;
|
|
8
|
+
leader: boolean;
|
|
9
|
+
name: string;
|
|
10
|
+
};
|
|
11
11
|
|
|
12
12
|
export function match(a: Info, b: Info): boolean {
|
|
13
|
-
return isDeepEqual(a, b)
|
|
13
|
+
return isDeepEqual(a, b);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export function toString(info: Info): string {
|
|
17
|
-
const parts: string[] = []
|
|
17
|
+
const parts: string[] = [];
|
|
18
18
|
|
|
19
|
-
if (info.ctrl) parts.push(
|
|
20
|
-
if (info.meta) parts.push(
|
|
21
|
-
if (info.shift) parts.push(
|
|
19
|
+
if (info.ctrl) parts.push('ctrl');
|
|
20
|
+
if (info.meta) parts.push('alt');
|
|
21
|
+
if (info.shift) parts.push('shift');
|
|
22
22
|
if (info.name) {
|
|
23
|
-
if (info.name ===
|
|
24
|
-
else parts.push(info.name)
|
|
23
|
+
if (info.name === 'delete') parts.push('del');
|
|
24
|
+
else parts.push(info.name);
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
let result = parts.join(
|
|
27
|
+
let result = parts.join('+');
|
|
28
28
|
|
|
29
29
|
if (info.leader) {
|
|
30
|
-
result = result ? `<leader> ${result}` : `<leader
|
|
30
|
+
result = result ? `<leader> ${result}` : `<leader>`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
return result
|
|
33
|
+
return result;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
export function parse(key: string): Info[] {
|
|
37
|
-
if (key ===
|
|
37
|
+
if (key === 'none') return [];
|
|
38
38
|
|
|
39
|
-
return key.split(
|
|
39
|
+
return key.split(',').map((combo) => {
|
|
40
40
|
// Handle <leader> syntax by replacing with leader+
|
|
41
|
-
const normalized = combo.replace(/<leader>/g,
|
|
42
|
-
const parts = normalized.toLowerCase().split(
|
|
41
|
+
const normalized = combo.replace(/<leader>/g, 'leader+');
|
|
42
|
+
const parts = normalized.toLowerCase().split('+');
|
|
43
43
|
const info: Info = {
|
|
44
44
|
ctrl: false,
|
|
45
45
|
meta: false,
|
|
46
46
|
shift: false,
|
|
47
47
|
leader: false,
|
|
48
|
-
name:
|
|
49
|
-
}
|
|
48
|
+
name: '',
|
|
49
|
+
};
|
|
50
50
|
|
|
51
51
|
for (const part of parts) {
|
|
52
52
|
switch (part) {
|
|
53
|
-
case
|
|
54
|
-
info.ctrl = true
|
|
55
|
-
break
|
|
56
|
-
case
|
|
57
|
-
case
|
|
58
|
-
case
|
|
59
|
-
info.meta = true
|
|
60
|
-
break
|
|
61
|
-
case
|
|
62
|
-
info.shift = true
|
|
63
|
-
break
|
|
64
|
-
case
|
|
65
|
-
info.leader = true
|
|
66
|
-
break
|
|
67
|
-
case
|
|
68
|
-
info.name =
|
|
69
|
-
break
|
|
53
|
+
case 'ctrl':
|
|
54
|
+
info.ctrl = true;
|
|
55
|
+
break;
|
|
56
|
+
case 'alt':
|
|
57
|
+
case 'meta':
|
|
58
|
+
case 'option':
|
|
59
|
+
info.meta = true;
|
|
60
|
+
break;
|
|
61
|
+
case 'shift':
|
|
62
|
+
info.shift = true;
|
|
63
|
+
break;
|
|
64
|
+
case 'leader':
|
|
65
|
+
info.leader = true;
|
|
66
|
+
break;
|
|
67
|
+
case 'esc':
|
|
68
|
+
info.name = 'escape';
|
|
69
|
+
break;
|
|
70
70
|
default:
|
|
71
|
-
info.name = part
|
|
72
|
-
break
|
|
71
|
+
info.name = part;
|
|
72
|
+
break;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
-
return info
|
|
77
|
-
})
|
|
76
|
+
return info;
|
|
77
|
+
});
|
|
78
78
|
}
|
|
79
79
|
}
|
package/src/util/lazy.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
export function lazy<T>(fn: () => T) {
|
|
2
|
-
let value: T | undefined
|
|
3
|
-
let loaded = false
|
|
2
|
+
let value: T | undefined;
|
|
3
|
+
let loaded = false;
|
|
4
4
|
|
|
5
5
|
return (): T => {
|
|
6
|
-
if (loaded) return value as T
|
|
7
|
-
loaded = true
|
|
8
|
-
value = fn()
|
|
9
|
-
return value as T
|
|
10
|
-
}
|
|
6
|
+
if (loaded) return value as T;
|
|
7
|
+
loaded = true;
|
|
8
|
+
value = fn();
|
|
9
|
+
return value as T;
|
|
10
|
+
};
|
|
11
11
|
}
|
package/src/util/locale.ts
CHANGED
|
@@ -1,39 +1,43 @@
|
|
|
1
1
|
export namespace Locale {
|
|
2
2
|
export function titlecase(str: string) {
|
|
3
|
-
return str.replace(/\b\w/g, (c) => c.toUpperCase())
|
|
3
|
+
return str.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
4
4
|
}
|
|
5
5
|
|
|
6
6
|
export function time(input: number) {
|
|
7
|
-
const date = new Date(input)
|
|
8
|
-
return date.toLocaleTimeString()
|
|
7
|
+
const date = new Date(input);
|
|
8
|
+
return date.toLocaleTimeString();
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export function number(num: number): string {
|
|
12
12
|
if (num >= 1000000) {
|
|
13
|
-
return (num / 1000000).toFixed(1) +
|
|
13
|
+
return (num / 1000000).toFixed(1) + 'M';
|
|
14
14
|
} else if (num >= 1000) {
|
|
15
|
-
return (num / 1000).toFixed(1) +
|
|
15
|
+
return (num / 1000).toFixed(1) + 'K';
|
|
16
16
|
}
|
|
17
|
-
return num.toString()
|
|
17
|
+
return num.toString();
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function truncate(str: string, len: number): string {
|
|
21
|
-
if (str.length <= len) return str
|
|
22
|
-
return str.slice(0, len - 1) +
|
|
21
|
+
if (str.length <= len) return str;
|
|
22
|
+
return str.slice(0, len - 1) + '…';
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
export function truncateMiddle(str: string, maxLength: number = 35): string {
|
|
26
|
-
if (str.length <= maxLength) return str
|
|
26
|
+
if (str.length <= maxLength) return str;
|
|
27
27
|
|
|
28
|
-
const ellipsis =
|
|
29
|
-
const keepStart = Math.ceil((maxLength - ellipsis.length) / 2)
|
|
30
|
-
const keepEnd = Math.floor((maxLength - ellipsis.length) / 2)
|
|
28
|
+
const ellipsis = '…';
|
|
29
|
+
const keepStart = Math.ceil((maxLength - ellipsis.length) / 2);
|
|
30
|
+
const keepEnd = Math.floor((maxLength - ellipsis.length) / 2);
|
|
31
31
|
|
|
32
|
-
return str.slice(0, keepStart) + ellipsis + str.slice(-keepEnd)
|
|
32
|
+
return str.slice(0, keepStart) + ellipsis + str.slice(-keepEnd);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
export function pluralize(
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
export function pluralize(
|
|
36
|
+
count: number,
|
|
37
|
+
singular: string,
|
|
38
|
+
plural: string
|
|
39
|
+
): string {
|
|
40
|
+
const template = count === 1 ? singular : plural;
|
|
41
|
+
return template.replace('{}', count.toString());
|
|
38
42
|
}
|
|
39
43
|
}
|
package/src/util/lock.ts
CHANGED
|
@@ -2,12 +2,12 @@ export namespace Lock {
|
|
|
2
2
|
const locks = new Map<
|
|
3
3
|
string,
|
|
4
4
|
{
|
|
5
|
-
readers: number
|
|
6
|
-
writer: boolean
|
|
7
|
-
waitingReaders: (() => void)[]
|
|
8
|
-
waitingWriters: (() => void)[]
|
|
5
|
+
readers: number;
|
|
6
|
+
writer: boolean;
|
|
7
|
+
waitingReaders: (() => void)[];
|
|
8
|
+
waitingWriters: (() => void)[];
|
|
9
9
|
}
|
|
10
|
-
>()
|
|
10
|
+
>();
|
|
11
11
|
|
|
12
12
|
function get(key: string) {
|
|
13
13
|
if (!locks.has(key)) {
|
|
@@ -16,83 +16,88 @@ export namespace Lock {
|
|
|
16
16
|
writer: false,
|
|
17
17
|
waitingReaders: [],
|
|
18
18
|
waitingWriters: [],
|
|
19
|
-
})
|
|
19
|
+
});
|
|
20
20
|
}
|
|
21
|
-
return locks.get(key)
|
|
21
|
+
return locks.get(key)!;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
function process(key: string) {
|
|
25
|
-
const lock = locks.get(key)
|
|
26
|
-
if (!lock || lock.writer || lock.readers > 0) return
|
|
25
|
+
const lock = locks.get(key);
|
|
26
|
+
if (!lock || lock.writer || lock.readers > 0) return;
|
|
27
27
|
|
|
28
28
|
// Prioritize writers to prevent starvation
|
|
29
29
|
if (lock.waitingWriters.length > 0) {
|
|
30
|
-
const nextWriter = lock.waitingWriters.shift()
|
|
31
|
-
nextWriter()
|
|
32
|
-
return
|
|
30
|
+
const nextWriter = lock.waitingWriters.shift()!;
|
|
31
|
+
nextWriter();
|
|
32
|
+
return;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Wake up all waiting readers
|
|
36
36
|
while (lock.waitingReaders.length > 0) {
|
|
37
|
-
const nextReader = lock.waitingReaders.shift()
|
|
38
|
-
nextReader()
|
|
37
|
+
const nextReader = lock.waitingReaders.shift()!;
|
|
38
|
+
nextReader();
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
// Clean up empty locks
|
|
42
|
-
if (
|
|
43
|
-
|
|
42
|
+
if (
|
|
43
|
+
lock.readers === 0 &&
|
|
44
|
+
!lock.writer &&
|
|
45
|
+
lock.waitingReaders.length === 0 &&
|
|
46
|
+
lock.waitingWriters.length === 0
|
|
47
|
+
) {
|
|
48
|
+
locks.delete(key);
|
|
44
49
|
}
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
export async function read(key: string): Promise<Disposable> {
|
|
48
|
-
const lock = get(key)
|
|
53
|
+
const lock = get(key);
|
|
49
54
|
|
|
50
55
|
return new Promise((resolve) => {
|
|
51
56
|
if (!lock.writer && lock.waitingWriters.length === 0) {
|
|
52
|
-
lock.readers
|
|
57
|
+
lock.readers++;
|
|
53
58
|
resolve({
|
|
54
59
|
[Symbol.dispose]: () => {
|
|
55
|
-
lock.readers
|
|
56
|
-
process(key)
|
|
60
|
+
lock.readers--;
|
|
61
|
+
process(key);
|
|
57
62
|
},
|
|
58
|
-
})
|
|
63
|
+
});
|
|
59
64
|
} else {
|
|
60
65
|
lock.waitingReaders.push(() => {
|
|
61
|
-
lock.readers
|
|
66
|
+
lock.readers++;
|
|
62
67
|
resolve({
|
|
63
68
|
[Symbol.dispose]: () => {
|
|
64
|
-
lock.readers
|
|
65
|
-
process(key)
|
|
69
|
+
lock.readers--;
|
|
70
|
+
process(key);
|
|
66
71
|
},
|
|
67
|
-
})
|
|
68
|
-
})
|
|
72
|
+
});
|
|
73
|
+
});
|
|
69
74
|
}
|
|
70
|
-
})
|
|
75
|
+
});
|
|
71
76
|
}
|
|
72
77
|
|
|
73
78
|
export async function write(key: string): Promise<Disposable> {
|
|
74
|
-
const lock = get(key)
|
|
79
|
+
const lock = get(key);
|
|
75
80
|
|
|
76
81
|
return new Promise((resolve) => {
|
|
77
82
|
if (!lock.writer && lock.readers === 0) {
|
|
78
|
-
lock.writer = true
|
|
83
|
+
lock.writer = true;
|
|
79
84
|
resolve({
|
|
80
85
|
[Symbol.dispose]: () => {
|
|
81
|
-
lock.writer = false
|
|
82
|
-
process(key)
|
|
86
|
+
lock.writer = false;
|
|
87
|
+
process(key);
|
|
83
88
|
},
|
|
84
|
-
})
|
|
89
|
+
});
|
|
85
90
|
} else {
|
|
86
91
|
lock.waitingWriters.push(() => {
|
|
87
|
-
lock.writer = true
|
|
92
|
+
lock.writer = true;
|
|
88
93
|
resolve({
|
|
89
94
|
[Symbol.dispose]: () => {
|
|
90
|
-
lock.writer = false
|
|
91
|
-
process(key)
|
|
95
|
+
lock.writer = false;
|
|
96
|
+
process(key);
|
|
92
97
|
},
|
|
93
|
-
})
|
|
94
|
-
})
|
|
98
|
+
});
|
|
99
|
+
});
|
|
95
100
|
}
|
|
96
|
-
})
|
|
101
|
+
});
|
|
97
102
|
}
|
|
98
103
|
}
|