octocode-ai-shared 5.0.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.
- package/package.json +42 -0
- package/src/filesystem.ts +236 -0
- package/src/global.ts +84 -0
- package/src/types.d.ts +46 -0
- package/src/util/array.ts +10 -0
- package/src/util/binary.ts +41 -0
- package/src/util/effect-flock.ts +283 -0
- package/src/util/encode.ts +51 -0
- package/src/util/error.ts +60 -0
- package/src/util/flock.ts +358 -0
- package/src/util/fn.ts +11 -0
- package/src/util/glob.ts +34 -0
- package/src/util/hash.ts +7 -0
- package/src/util/identifier.ts +53 -0
- package/src/util/iife.ts +3 -0
- package/src/util/lazy.ts +11 -0
- package/src/util/module.ts +10 -0
- package/src/util/path.ts +37 -0
- package/src/util/retry.ts +42 -0
- package/src/util/slug.ts +74 -0
- package/test/filesystem/filesystem.test.ts +338 -0
- package/test/fixture/effect-flock-worker.ts +63 -0
- package/test/fixture/flock-worker.ts +72 -0
- package/test/global.test.ts +54 -0
- package/test/lib/effect.ts +53 -0
- package/test/util/effect-flock.test.ts +389 -0
- package/test/util/flock.test.ts +426 -0
- package/test/util/identifier.test.ts +30 -0
- package/tsconfig.json +14 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"version": "5.0.0",
|
|
4
|
+
"name": "octocode-ai-shared",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"private": false,
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "bun test",
|
|
13
|
+
"typecheck": "tsgo --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"bin": {
|
|
16
|
+
"opencode": "./bin/opencode"
|
|
17
|
+
},
|
|
18
|
+
"exports": {
|
|
19
|
+
"./*": "./src/*.ts"
|
|
20
|
+
},
|
|
21
|
+
"imports": {},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@tsconfig/bun": "1.0.9",
|
|
24
|
+
"@types/semver": "7.7.1",
|
|
25
|
+
"@types/bun": "1.3.11",
|
|
26
|
+
"@types/npmcli__arborist": "6.3.3"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@effect/platform-node": "4.0.0-beta.48",
|
|
30
|
+
"@npmcli/arborist": "9.4.0",
|
|
31
|
+
"effect": "4.0.0-beta.48",
|
|
32
|
+
"glob": "13.0.5",
|
|
33
|
+
"mime-types": "3.0.2",
|
|
34
|
+
"minimatch": "10.2.5",
|
|
35
|
+
"semver": "7.7.4",
|
|
36
|
+
"xdg-basedir": "5.1.0",
|
|
37
|
+
"zod": "4.1.8"
|
|
38
|
+
},
|
|
39
|
+
"overrides": {
|
|
40
|
+
"drizzle-orm": "1.0.0-beta.19-d95b7a4"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { NodeFileSystem } from "@effect/platform-node"
|
|
2
|
+
import { dirname, join, relative, resolve as pathResolve } from "path"
|
|
3
|
+
import { realpathSync } from "fs"
|
|
4
|
+
import * as NFS from "fs/promises"
|
|
5
|
+
import { lookup } from "mime-types"
|
|
6
|
+
import { Effect, FileSystem, Layer, Schema, Context } from "effect"
|
|
7
|
+
import type { PlatformError } from "effect/PlatformError"
|
|
8
|
+
import { Glob } from "./util/glob"
|
|
9
|
+
|
|
10
|
+
export namespace AppFileSystem {
|
|
11
|
+
export class FileSystemError extends Schema.TaggedErrorClass<FileSystemError>()("FileSystemError", {
|
|
12
|
+
method: Schema.String,
|
|
13
|
+
cause: Schema.optional(Schema.Defect),
|
|
14
|
+
}) {}
|
|
15
|
+
|
|
16
|
+
export type Error = PlatformError | FileSystemError
|
|
17
|
+
|
|
18
|
+
export interface DirEntry {
|
|
19
|
+
readonly name: string
|
|
20
|
+
readonly type: "file" | "directory" | "symlink" | "other"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface Interface extends FileSystem.FileSystem {
|
|
24
|
+
readonly isDir: (path: string) => Effect.Effect<boolean>
|
|
25
|
+
readonly isFile: (path: string) => Effect.Effect<boolean>
|
|
26
|
+
readonly existsSafe: (path: string) => Effect.Effect<boolean>
|
|
27
|
+
readonly readJson: (path: string) => Effect.Effect<unknown, Error>
|
|
28
|
+
readonly writeJson: (path: string, data: unknown, mode?: number) => Effect.Effect<void, Error>
|
|
29
|
+
readonly ensureDir: (path: string) => Effect.Effect<void, Error>
|
|
30
|
+
readonly writeWithDirs: (path: string, content: string | Uint8Array, mode?: number) => Effect.Effect<void, Error>
|
|
31
|
+
readonly readDirectoryEntries: (path: string) => Effect.Effect<DirEntry[], Error>
|
|
32
|
+
readonly findUp: (target: string, start: string, stop?: string) => Effect.Effect<string[], Error>
|
|
33
|
+
readonly up: (options: { targets: string[]; start: string; stop?: string }) => Effect.Effect<string[], Error>
|
|
34
|
+
readonly globUp: (pattern: string, start: string, stop?: string) => Effect.Effect<string[], Error>
|
|
35
|
+
readonly glob: (pattern: string, options?: Glob.Options) => Effect.Effect<string[], Error>
|
|
36
|
+
readonly globMatch: (pattern: string, filepath: string) => boolean
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export class Service extends Context.Service<Service, Interface>()("@opencode/FileSystem") {}
|
|
40
|
+
|
|
41
|
+
export const layer = Layer.effect(
|
|
42
|
+
Service,
|
|
43
|
+
Effect.gen(function* () {
|
|
44
|
+
const fs = yield* FileSystem.FileSystem
|
|
45
|
+
|
|
46
|
+
const existsSafe = Effect.fn("FileSystem.existsSafe")(function* (path: string) {
|
|
47
|
+
return yield* fs.exists(path).pipe(Effect.orElseSucceed(() => false))
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
const isDir = Effect.fn("FileSystem.isDir")(function* (path: string) {
|
|
51
|
+
const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void))
|
|
52
|
+
return info?.type === "Directory"
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
const isFile = Effect.fn("FileSystem.isFile")(function* (path: string) {
|
|
56
|
+
const info = yield* fs.stat(path).pipe(Effect.catch(() => Effect.void))
|
|
57
|
+
return info?.type === "File"
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
const readDirectoryEntries = Effect.fn("FileSystem.readDirectoryEntries")(function* (dirPath: string) {
|
|
61
|
+
return yield* Effect.tryPromise({
|
|
62
|
+
try: async () => {
|
|
63
|
+
const entries = await NFS.readdir(dirPath, { withFileTypes: true })
|
|
64
|
+
return entries.map(
|
|
65
|
+
(e): DirEntry => ({
|
|
66
|
+
name: e.name,
|
|
67
|
+
type: e.isDirectory() ? "directory" : e.isSymbolicLink() ? "symlink" : e.isFile() ? "file" : "other",
|
|
68
|
+
}),
|
|
69
|
+
)
|
|
70
|
+
},
|
|
71
|
+
catch: (cause) => new FileSystemError({ method: "readDirectoryEntries", cause }),
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const readJson = Effect.fn("FileSystem.readJson")(function* (path: string) {
|
|
76
|
+
const text = yield* fs.readFileString(path)
|
|
77
|
+
return JSON.parse(text)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
const writeJson = Effect.fn("FileSystem.writeJson")(function* (path: string, data: unknown, mode?: number) {
|
|
81
|
+
const content = JSON.stringify(data, null, 2)
|
|
82
|
+
yield* fs.writeFileString(path, content)
|
|
83
|
+
if (mode) yield* fs.chmod(path, mode)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const ensureDir = Effect.fn("FileSystem.ensureDir")(function* (path: string) {
|
|
87
|
+
yield* fs.makeDirectory(path, { recursive: true })
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
const writeWithDirs = Effect.fn("FileSystem.writeWithDirs")(function* (
|
|
91
|
+
path: string,
|
|
92
|
+
content: string | Uint8Array,
|
|
93
|
+
mode?: number,
|
|
94
|
+
) {
|
|
95
|
+
const write = typeof content === "string" ? fs.writeFileString(path, content) : fs.writeFile(path, content)
|
|
96
|
+
|
|
97
|
+
yield* write.pipe(
|
|
98
|
+
Effect.catchIf(
|
|
99
|
+
(e) => e.reason._tag === "NotFound",
|
|
100
|
+
() =>
|
|
101
|
+
Effect.gen(function* () {
|
|
102
|
+
yield* fs.makeDirectory(dirname(path), { recursive: true })
|
|
103
|
+
yield* write
|
|
104
|
+
}),
|
|
105
|
+
),
|
|
106
|
+
)
|
|
107
|
+
if (mode) yield* fs.chmod(path, mode)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
const glob = Effect.fn("FileSystem.glob")(function* (pattern: string, options?: Glob.Options) {
|
|
111
|
+
return yield* Effect.tryPromise({
|
|
112
|
+
try: () => Glob.scan(pattern, options),
|
|
113
|
+
catch: (cause) => new FileSystemError({ method: "glob", cause }),
|
|
114
|
+
})
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
const findUp = Effect.fn("FileSystem.findUp")(function* (target: string, start: string, stop?: string) {
|
|
118
|
+
const result: string[] = []
|
|
119
|
+
let current = start
|
|
120
|
+
while (true) {
|
|
121
|
+
const search = join(current, target)
|
|
122
|
+
if (yield* fs.exists(search)) result.push(search)
|
|
123
|
+
if (stop === current) break
|
|
124
|
+
const parent = dirname(current)
|
|
125
|
+
if (parent === current) break
|
|
126
|
+
current = parent
|
|
127
|
+
}
|
|
128
|
+
return result
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const up = Effect.fn("FileSystem.up")(function* (options: { targets: string[]; start: string; stop?: string }) {
|
|
132
|
+
const result: string[] = []
|
|
133
|
+
let current = options.start
|
|
134
|
+
while (true) {
|
|
135
|
+
for (const target of options.targets) {
|
|
136
|
+
const search = join(current, target)
|
|
137
|
+
if (yield* fs.exists(search)) result.push(search)
|
|
138
|
+
}
|
|
139
|
+
if (options.stop === current) break
|
|
140
|
+
const parent = dirname(current)
|
|
141
|
+
if (parent === current) break
|
|
142
|
+
current = parent
|
|
143
|
+
}
|
|
144
|
+
return result
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
const globUp = Effect.fn("FileSystem.globUp")(function* (pattern: string, start: string, stop?: string) {
|
|
148
|
+
const result: string[] = []
|
|
149
|
+
let current = start
|
|
150
|
+
while (true) {
|
|
151
|
+
const matches = yield* glob(pattern, { cwd: current, absolute: true, include: "file", dot: true }).pipe(
|
|
152
|
+
Effect.catch(() => Effect.succeed([] as string[])),
|
|
153
|
+
)
|
|
154
|
+
result.push(...matches)
|
|
155
|
+
if (stop === current) break
|
|
156
|
+
const parent = dirname(current)
|
|
157
|
+
if (parent === current) break
|
|
158
|
+
current = parent
|
|
159
|
+
}
|
|
160
|
+
return result
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
return Service.of({
|
|
164
|
+
...fs,
|
|
165
|
+
existsSafe,
|
|
166
|
+
isDir,
|
|
167
|
+
isFile,
|
|
168
|
+
readDirectoryEntries,
|
|
169
|
+
readJson,
|
|
170
|
+
writeJson,
|
|
171
|
+
ensureDir,
|
|
172
|
+
writeWithDirs,
|
|
173
|
+
findUp,
|
|
174
|
+
up,
|
|
175
|
+
globUp,
|
|
176
|
+
glob,
|
|
177
|
+
globMatch: Glob.match,
|
|
178
|
+
})
|
|
179
|
+
}),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
export const defaultLayer = layer.pipe(Layer.provide(NodeFileSystem.layer))
|
|
183
|
+
|
|
184
|
+
// Pure helpers that don't need Effect (path manipulation, sync operations)
|
|
185
|
+
export function mimeType(p: string): string {
|
|
186
|
+
return lookup(p) || "application/octet-stream"
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function normalizePath(p: string): string {
|
|
190
|
+
if (process.platform !== "win32") return p
|
|
191
|
+
const resolved = pathResolve(windowsPath(p))
|
|
192
|
+
try {
|
|
193
|
+
return realpathSync.native(resolved)
|
|
194
|
+
} catch {
|
|
195
|
+
return resolved
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export function normalizePathPattern(p: string): string {
|
|
200
|
+
if (process.platform !== "win32") return p
|
|
201
|
+
if (p === "*") return p
|
|
202
|
+
const match = p.match(/^(.*)[\\/]\*$/)
|
|
203
|
+
if (!match) return normalizePath(p)
|
|
204
|
+
const dir = /^[A-Za-z]:$/.test(match[1]) ? match[1] + "\\" : match[1]
|
|
205
|
+
return join(normalizePath(dir), "*")
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function resolve(p: string): string {
|
|
209
|
+
const resolved = pathResolve(windowsPath(p))
|
|
210
|
+
try {
|
|
211
|
+
return normalizePath(realpathSync(resolved))
|
|
212
|
+
} catch (e: any) {
|
|
213
|
+
if (e?.code === "ENOENT") return normalizePath(resolved)
|
|
214
|
+
throw e
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function windowsPath(p: string): string {
|
|
219
|
+
if (process.platform !== "win32") return p
|
|
220
|
+
return p
|
|
221
|
+
.replace(/^\/([a-zA-Z]):(?:[\\/]|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
|
|
222
|
+
.replace(/^\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
|
|
223
|
+
.replace(/^\/cygdrive\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
|
|
224
|
+
.replace(/^\/mnt\/([a-zA-Z])(?:\/|$)/, (_, drive) => `${drive.toUpperCase()}:/`)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function overlaps(a: string, b: string) {
|
|
228
|
+
const relA = relative(a, b)
|
|
229
|
+
const relB = relative(b, a)
|
|
230
|
+
return !relA || !relA.startsWith("..") || !relB || !relB.startsWith("..")
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function contains(parent: string, child: string) {
|
|
234
|
+
return !relative(parent, child).startsWith("..")
|
|
235
|
+
}
|
|
236
|
+
}
|
package/src/global.ts
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import path from "path"
|
|
2
|
+
import { xdgData, xdgCache, xdgConfig, xdgState } from "xdg-basedir"
|
|
3
|
+
import os from "os"
|
|
4
|
+
import { Context, Effect, Layer } from "effect"
|
|
5
|
+
|
|
6
|
+
const APP = "octocode"
|
|
7
|
+
|
|
8
|
+
export type ResolvedPaths = {
|
|
9
|
+
mode: "octocode_home" | "xdg"
|
|
10
|
+
root?: string
|
|
11
|
+
data: string
|
|
12
|
+
cache: string
|
|
13
|
+
config: string
|
|
14
|
+
state: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve octocode's four base directories (config/data/state/cache)
|
|
19
|
+
* from environment variables.
|
|
20
|
+
*
|
|
21
|
+
* If OCTOCODE_HOME is set and non-empty, the four paths are subdirectories
|
|
22
|
+
* of it. Otherwise, falls through to XDG Base Directory defaults.
|
|
23
|
+
*
|
|
24
|
+
* @throws if OCTOCODE_HOME is set but not an absolute path
|
|
25
|
+
*/
|
|
26
|
+
export function resolveOctocodeHome(env: NodeJS.ProcessEnv = process.env): ResolvedPaths {
|
|
27
|
+
const home = env.OCTOCODE_HOME
|
|
28
|
+
if (home) {
|
|
29
|
+
if (!path.isAbsolute(home)) {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`OCTOCODE_HOME must be an absolute path, got: ${JSON.stringify(home)}`,
|
|
32
|
+
)
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
mode: "octocode_home",
|
|
36
|
+
root: home,
|
|
37
|
+
data: path.join(home, "data"),
|
|
38
|
+
cache: path.join(home, "cache"),
|
|
39
|
+
config: path.join(home, "config"),
|
|
40
|
+
state: path.join(home, "state"),
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
mode: "xdg",
|
|
45
|
+
data: path.join(xdgData!, APP),
|
|
46
|
+
cache: path.join(xdgCache!, APP),
|
|
47
|
+
config: path.join(xdgConfig!, APP),
|
|
48
|
+
state: path.join(xdgState!, APP),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export namespace Global {
|
|
53
|
+
export class Service extends Context.Service<Service, Interface>()("@opencode/Global") {}
|
|
54
|
+
|
|
55
|
+
export interface Interface {
|
|
56
|
+
readonly home: string
|
|
57
|
+
readonly data: string
|
|
58
|
+
readonly cache: string
|
|
59
|
+
readonly config: string
|
|
60
|
+
readonly state: string
|
|
61
|
+
readonly bin: string
|
|
62
|
+
readonly log: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export const layer = Layer.effect(
|
|
66
|
+
Service,
|
|
67
|
+
Effect.gen(function* () {
|
|
68
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir()
|
|
69
|
+
const { data, cache, config, state } = yield* Effect.sync(() => resolveOctocodeHome())
|
|
70
|
+
const bin = path.join(cache, "bin")
|
|
71
|
+
const log = path.join(data, "log")
|
|
72
|
+
|
|
73
|
+
return Service.of({
|
|
74
|
+
home,
|
|
75
|
+
data,
|
|
76
|
+
cache,
|
|
77
|
+
config,
|
|
78
|
+
state,
|
|
79
|
+
bin,
|
|
80
|
+
log,
|
|
81
|
+
})
|
|
82
|
+
}),
|
|
83
|
+
)
|
|
84
|
+
}
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
declare module "@npmcli/arborist" {
|
|
2
|
+
export interface ArboristOptions {
|
|
3
|
+
path: string
|
|
4
|
+
binLinks?: boolean
|
|
5
|
+
progress?: boolean
|
|
6
|
+
savePrefix?: string
|
|
7
|
+
ignoreScripts?: boolean
|
|
8
|
+
[key: string]: unknown
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ArboristNode {
|
|
12
|
+
name: string
|
|
13
|
+
path: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ArboristEdge {
|
|
17
|
+
to?: ArboristNode
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ArboristTree {
|
|
21
|
+
edgesOut: Map<string, ArboristEdge>
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ReifyOptions {
|
|
25
|
+
add?: string[]
|
|
26
|
+
save?: boolean
|
|
27
|
+
saveType?: "prod" | "dev" | "optional" | "peer"
|
|
28
|
+
[key: string]: unknown
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class Arborist {
|
|
32
|
+
constructor(options: ArboristOptions)
|
|
33
|
+
loadVirtual(): Promise<ArboristTree | undefined>
|
|
34
|
+
reify(options?: ReifyOptions): Promise<ArboristTree>
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
declare var Bun:
|
|
39
|
+
| {
|
|
40
|
+
file(path: string): {
|
|
41
|
+
text(): Promise<string>
|
|
42
|
+
json(): Promise<unknown>
|
|
43
|
+
}
|
|
44
|
+
write(path: string, content: string | Uint8Array): Promise<void>
|
|
45
|
+
}
|
|
46
|
+
| undefined
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export function findLast<T>(
|
|
2
|
+
items: readonly T[],
|
|
3
|
+
predicate: (item: T, index: number, items: readonly T[]) => boolean,
|
|
4
|
+
): T | undefined {
|
|
5
|
+
for (let i = items.length - 1; i >= 0; i -= 1) {
|
|
6
|
+
const item = items[i]
|
|
7
|
+
if (predicate(item, i, items)) return item
|
|
8
|
+
}
|
|
9
|
+
return undefined
|
|
10
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export namespace Binary {
|
|
2
|
+
export function search<T>(array: T[], id: string, compare: (item: T) => string): { found: boolean; index: number } {
|
|
3
|
+
let left = 0
|
|
4
|
+
let right = array.length - 1
|
|
5
|
+
|
|
6
|
+
while (left <= right) {
|
|
7
|
+
const mid = Math.floor((left + right) / 2)
|
|
8
|
+
const midId = compare(array[mid])
|
|
9
|
+
|
|
10
|
+
if (midId === id) {
|
|
11
|
+
return { found: true, index: mid }
|
|
12
|
+
} else if (midId < id) {
|
|
13
|
+
left = mid + 1
|
|
14
|
+
} else {
|
|
15
|
+
right = mid - 1
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return { found: false, index: left }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function insert<T>(array: T[], item: T, compare: (item: T) => string): T[] {
|
|
23
|
+
const id = compare(item)
|
|
24
|
+
let left = 0
|
|
25
|
+
let right = array.length
|
|
26
|
+
|
|
27
|
+
while (left < right) {
|
|
28
|
+
const mid = Math.floor((left + right) / 2)
|
|
29
|
+
const midId = compare(array[mid])
|
|
30
|
+
|
|
31
|
+
if (midId < id) {
|
|
32
|
+
left = mid + 1
|
|
33
|
+
} else {
|
|
34
|
+
right = mid
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
array.splice(left, 0, item)
|
|
39
|
+
return array
|
|
40
|
+
}
|
|
41
|
+
}
|