@blokjs/capabilities 2.2.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/README.md +24 -0
- package/dist/WorkspaceFilesystemCapability.d.ts +35 -0
- package/dist/WorkspaceFilesystemCapability.js +871 -0
- package/dist/contracts.d.ts +184 -0
- package/dist/contracts.js +22 -0
- package/dist/errors.d.ts +11 -0
- package/dist/errors.js +39 -0
- package/dist/graph/BoundedGraphIndexer.d.ts +44 -0
- package/dist/graph/BoundedGraphIndexer.js +157 -0
- package/dist/graph/FakeGraphProvider.d.ts +35 -0
- package/dist/graph/FakeGraphProvider.js +280 -0
- package/dist/graph/GraphCapabilityManifests.d.ts +5 -0
- package/dist/graph/GraphCapabilityManifests.js +22 -0
- package/dist/graph/GraphProviderError.d.ts +12 -0
- package/dist/graph/GraphProviderError.js +20 -0
- package/dist/graph/GraphProviderSupport.d.ts +20 -0
- package/dist/graph/GraphProviderSupport.js +95 -0
- package/dist/graph/TetrixGraphProvider.d.ts +35 -0
- package/dist/graph/TetrixGraphProvider.js +99 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +8 -0
- package/package.json +31 -0
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { ArtifactVersionIdentity, CapabilityAuthority, EvidenceProvenanceIdentity, PolicyDecision, PolicyEvaluationResult, PolicyLayer, PolicyProvider, PolicyRequest, PrincipalIdentity, SessionIdentity, TurnIdentity, WorkflowIdentity } from "@blokjs/shared";
|
|
2
|
+
export declare const WORKSPACE_FILESYSTEM_CONTRACT_VERSION: "1";
|
|
3
|
+
export declare const WORKSPACE_FILESYSTEM_OPERATIONS: readonly ["metadata", "list", "read", "search", "write", "patch", "watch"];
|
|
4
|
+
export type WorkspaceFilesystemOperation = (typeof WORKSPACE_FILESYSTEM_OPERATIONS)[number];
|
|
5
|
+
export declare const WORKSPACE_FILESYSTEM_MAX_PATH_LENGTH = 4096;
|
|
6
|
+
export declare const WORKSPACE_FILESYSTEM_MAX_QUERY_LENGTH = 8192;
|
|
7
|
+
export declare const WORKSPACE_FILESYSTEM_MAX_READ_BYTES: number;
|
|
8
|
+
export declare const WORKSPACE_FILESYSTEM_MAX_WRITE_BYTES: number;
|
|
9
|
+
export declare const WORKSPACE_FILESYSTEM_MAX_LIST_FILES = 10000;
|
|
10
|
+
export declare const WORKSPACE_FILESYSTEM_MAX_SEARCH_FILES = 2000;
|
|
11
|
+
export declare const WORKSPACE_FILESYSTEM_MAX_SEARCH_MATCHES = 10000;
|
|
12
|
+
export declare const WORKSPACE_FILESYSTEM_MAX_SEARCH_BYTES: number;
|
|
13
|
+
export declare const WORKSPACE_FILESYSTEM_MAX_LINES = 100000;
|
|
14
|
+
export declare const WORKSPACE_FILESYSTEM_MAX_WATCH_EVENTS = 1000;
|
|
15
|
+
export declare const WORKSPACE_FILESYSTEM_MAX_DURATION_MS = 30000;
|
|
16
|
+
export declare const WORKSPACE_FILESYSTEM_MAX_WATCH_DEBOUNCE_MS = 5000;
|
|
17
|
+
export type WorkspaceRootInput = {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly path: string;
|
|
20
|
+
};
|
|
21
|
+
export type WorkspaceRoot = {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
/** Canonical host path. It is never returned in operation results. */
|
|
24
|
+
readonly path: string;
|
|
25
|
+
};
|
|
26
|
+
export type WorkspaceFilesystemLimits = {
|
|
27
|
+
readonly maxReadBytes?: number;
|
|
28
|
+
readonly maxWriteBytes?: number;
|
|
29
|
+
readonly maxListFiles?: number;
|
|
30
|
+
readonly maxSearchFiles?: number;
|
|
31
|
+
readonly maxSearchMatches?: number;
|
|
32
|
+
readonly maxSearchBytes?: number;
|
|
33
|
+
readonly maxLines?: number;
|
|
34
|
+
readonly maxWatchEvents?: number;
|
|
35
|
+
readonly maxDurationMs?: number;
|
|
36
|
+
};
|
|
37
|
+
export type WorkspaceFilesystemPolicyRequest = PolicyRequest & {
|
|
38
|
+
readonly capability: "workspace-filesystem";
|
|
39
|
+
readonly operation: WorkspaceFilesystemOperation;
|
|
40
|
+
readonly workspaceId: string;
|
|
41
|
+
/** Workspace-relative, canonical path. Host paths are never sent to policy. */
|
|
42
|
+
readonly relativePath: string;
|
|
43
|
+
};
|
|
44
|
+
export type WorkspaceFilesystemPolicy = {
|
|
45
|
+
readonly provider: PolicyProvider;
|
|
46
|
+
readonly principal: PrincipalIdentity;
|
|
47
|
+
readonly session: SessionIdentity;
|
|
48
|
+
readonly turn: TurnIdentity;
|
|
49
|
+
readonly policyVersion: string;
|
|
50
|
+
readonly workflow: WorkflowIdentity;
|
|
51
|
+
readonly step: {
|
|
52
|
+
readonly id: string;
|
|
53
|
+
readonly index?: number;
|
|
54
|
+
readonly attempt?: number;
|
|
55
|
+
};
|
|
56
|
+
readonly layers: readonly PolicyLayer[];
|
|
57
|
+
readonly authority?: CapabilityAuthority;
|
|
58
|
+
};
|
|
59
|
+
export type WorkspaceFilesystemOptions = {
|
|
60
|
+
readonly roots: readonly WorkspaceRootInput[];
|
|
61
|
+
readonly limits?: WorkspaceFilesystemLimits;
|
|
62
|
+
readonly policy?: WorkspaceFilesystemPolicy;
|
|
63
|
+
readonly provenance?: EvidenceProvenanceIdentity;
|
|
64
|
+
};
|
|
65
|
+
export type WorkspacePathInput = {
|
|
66
|
+
readonly workspaceId: string;
|
|
67
|
+
/** Workspace-relative path. `/`, drive paths, UNC paths, and `..` are rejected. */
|
|
68
|
+
readonly path: string;
|
|
69
|
+
readonly signal?: AbortSignal;
|
|
70
|
+
readonly maxDurationMs?: number;
|
|
71
|
+
};
|
|
72
|
+
export type WorkspaceFileKind = "file" | "directory";
|
|
73
|
+
export type WorkspaceArtifact = ArtifactVersionIdentity & {
|
|
74
|
+
readonly workspaceId: string;
|
|
75
|
+
readonly relativePath: string;
|
|
76
|
+
readonly sizeBytes: number;
|
|
77
|
+
readonly observedAt: string;
|
|
78
|
+
readonly provenance?: EvidenceProvenanceIdentity;
|
|
79
|
+
};
|
|
80
|
+
export type WorkspaceFileMetadata = {
|
|
81
|
+
readonly path: string;
|
|
82
|
+
readonly kind: WorkspaceFileKind;
|
|
83
|
+
readonly sizeBytes: number;
|
|
84
|
+
readonly modifiedAt: string;
|
|
85
|
+
readonly version?: string;
|
|
86
|
+
readonly mediaType?: string;
|
|
87
|
+
readonly artifact?: WorkspaceArtifact;
|
|
88
|
+
};
|
|
89
|
+
export type WorkspaceMetadataInput = WorkspacePathInput;
|
|
90
|
+
export type WorkspaceListInput = WorkspacePathInput & {
|
|
91
|
+
readonly recursive?: boolean;
|
|
92
|
+
readonly maxFiles?: number;
|
|
93
|
+
readonly maxBytes?: number;
|
|
94
|
+
};
|
|
95
|
+
export type WorkspaceListResult = {
|
|
96
|
+
readonly workspaceId: string;
|
|
97
|
+
readonly path: string;
|
|
98
|
+
readonly entries: readonly WorkspaceFileMetadata[];
|
|
99
|
+
readonly bytesScanned: number;
|
|
100
|
+
readonly truncated: boolean;
|
|
101
|
+
};
|
|
102
|
+
export type WorkspaceReadEncoding = "utf8" | "base64" | "bytes";
|
|
103
|
+
export type WorkspaceReadInput = WorkspacePathInput & {
|
|
104
|
+
readonly encoding?: WorkspaceReadEncoding;
|
|
105
|
+
readonly startLine?: number;
|
|
106
|
+
readonly endLine?: number;
|
|
107
|
+
readonly maxBytes?: number;
|
|
108
|
+
readonly maxLines?: number;
|
|
109
|
+
};
|
|
110
|
+
export type WorkspaceReadResult = {
|
|
111
|
+
readonly workspaceId: string;
|
|
112
|
+
readonly path: string;
|
|
113
|
+
readonly encoding: WorkspaceReadEncoding;
|
|
114
|
+
readonly bytes: Uint8Array;
|
|
115
|
+
readonly content: string | Uint8Array;
|
|
116
|
+
readonly sizeBytes: number;
|
|
117
|
+
readonly version: string;
|
|
118
|
+
readonly artifact: WorkspaceArtifact;
|
|
119
|
+
};
|
|
120
|
+
export type WorkspaceSearchInput = WorkspacePathInput & {
|
|
121
|
+
readonly query: string;
|
|
122
|
+
readonly regex?: boolean;
|
|
123
|
+
readonly caseSensitive?: boolean;
|
|
124
|
+
readonly maxFiles?: number;
|
|
125
|
+
readonly maxMatches?: number;
|
|
126
|
+
readonly maxBytes?: number;
|
|
127
|
+
readonly maxLines?: number;
|
|
128
|
+
};
|
|
129
|
+
export type WorkspaceSearchMatch = {
|
|
130
|
+
readonly path: string;
|
|
131
|
+
readonly line: number;
|
|
132
|
+
readonly column: number;
|
|
133
|
+
readonly text: string;
|
|
134
|
+
readonly artifact?: WorkspaceArtifact;
|
|
135
|
+
};
|
|
136
|
+
export type WorkspaceSearchResult = {
|
|
137
|
+
readonly workspaceId: string;
|
|
138
|
+
readonly root: string;
|
|
139
|
+
readonly matches: readonly WorkspaceSearchMatch[];
|
|
140
|
+
readonly filesScanned: number;
|
|
141
|
+
readonly bytesScanned: number;
|
|
142
|
+
readonly truncated: boolean;
|
|
143
|
+
};
|
|
144
|
+
export type WorkspaceWriteInput = WorkspacePathInput & {
|
|
145
|
+
readonly content: string | Uint8Array;
|
|
146
|
+
/** Required when replacing an existing file. */
|
|
147
|
+
readonly expectedVersion?: string;
|
|
148
|
+
readonly maxBytes?: number;
|
|
149
|
+
};
|
|
150
|
+
export type WorkspaceWriteResult = {
|
|
151
|
+
readonly workspaceId: string;
|
|
152
|
+
readonly path: string;
|
|
153
|
+
readonly created: boolean;
|
|
154
|
+
readonly bytesWritten: number;
|
|
155
|
+
readonly version: string;
|
|
156
|
+
readonly artifact: WorkspaceArtifact;
|
|
157
|
+
};
|
|
158
|
+
export type WorkspaceTextPatch = {
|
|
159
|
+
readonly start: number;
|
|
160
|
+
readonly end: number;
|
|
161
|
+
readonly replacement: string;
|
|
162
|
+
};
|
|
163
|
+
export type WorkspacePatchInput = WorkspacePathInput & {
|
|
164
|
+
readonly patches: readonly WorkspaceTextPatch[];
|
|
165
|
+
readonly expectedVersion: string;
|
|
166
|
+
readonly maxBytes?: number;
|
|
167
|
+
};
|
|
168
|
+
export type WorkspaceWatchInput = WorkspacePathInput & {
|
|
169
|
+
readonly recursive?: boolean;
|
|
170
|
+
readonly debounceMs?: number;
|
|
171
|
+
readonly maxEvents?: number;
|
|
172
|
+
};
|
|
173
|
+
export type WorkspaceWatchEvent = {
|
|
174
|
+
readonly type: "created" | "changed" | "deleted" | "renamed" | "overflow";
|
|
175
|
+
readonly workspaceId: string;
|
|
176
|
+
readonly path?: string;
|
|
177
|
+
readonly version?: string;
|
|
178
|
+
readonly artifact?: WorkspaceArtifact;
|
|
179
|
+
readonly requiresRescan: boolean;
|
|
180
|
+
readonly observedAt: string;
|
|
181
|
+
};
|
|
182
|
+
export type WorkspacePolicyDecision = PolicyEvaluationResult & {
|
|
183
|
+
readonly decision: PolicyDecision;
|
|
184
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const WORKSPACE_FILESYSTEM_CONTRACT_VERSION = "1";
|
|
2
|
+
export const WORKSPACE_FILESYSTEM_OPERATIONS = [
|
|
3
|
+
"metadata",
|
|
4
|
+
"list",
|
|
5
|
+
"read",
|
|
6
|
+
"search",
|
|
7
|
+
"write",
|
|
8
|
+
"patch",
|
|
9
|
+
"watch",
|
|
10
|
+
];
|
|
11
|
+
export const WORKSPACE_FILESYSTEM_MAX_PATH_LENGTH = 4096;
|
|
12
|
+
export const WORKSPACE_FILESYSTEM_MAX_QUERY_LENGTH = 8192;
|
|
13
|
+
export const WORKSPACE_FILESYSTEM_MAX_READ_BYTES = 8 * 1024 * 1024;
|
|
14
|
+
export const WORKSPACE_FILESYSTEM_MAX_WRITE_BYTES = 8 * 1024 * 1024;
|
|
15
|
+
export const WORKSPACE_FILESYSTEM_MAX_LIST_FILES = 10_000;
|
|
16
|
+
export const WORKSPACE_FILESYSTEM_MAX_SEARCH_FILES = 2_000;
|
|
17
|
+
export const WORKSPACE_FILESYSTEM_MAX_SEARCH_MATCHES = 10_000;
|
|
18
|
+
export const WORKSPACE_FILESYSTEM_MAX_SEARCH_BYTES = 32 * 1024 * 1024;
|
|
19
|
+
export const WORKSPACE_FILESYSTEM_MAX_LINES = 100_000;
|
|
20
|
+
export const WORKSPACE_FILESYSTEM_MAX_WATCH_EVENTS = 1_000;
|
|
21
|
+
export const WORKSPACE_FILESYSTEM_MAX_DURATION_MS = 30_000;
|
|
22
|
+
export const WORKSPACE_FILESYSTEM_MAX_WATCH_DEBOUNCE_MS = 5_000;
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const WORKSPACE_FILESYSTEM_ERROR_CODES: readonly ["WORKSPACE_FS_INVALID_ROOT", "WORKSPACE_FS_INVALID_PATH", "WORKSPACE_FS_PATH_ESCAPE", "WORKSPACE_FS_SYMLINK_DISALLOWED", "WORKSPACE_FS_HARDLINK_DISALLOWED", "WORKSPACE_FS_SPECIAL_FILE_DISALLOWED", "WORKSPACE_FS_NOT_FOUND", "WORKSPACE_FS_PERMISSION_DENIED", "WORKSPACE_FS_INVALID_TARGET", "WORKSPACE_FS_BINARY_FILE", "WORKSPACE_FS_INVALID_ENCODING", "WORKSPACE_FS_QUERY_INVALID", "WORKSPACE_FS_SIZE_LIMIT", "WORKSPACE_FS_FILE_LIMIT", "WORKSPACE_FS_MATCH_LIMIT", "WORKSPACE_FS_LINE_LIMIT", "WORKSPACE_FS_TIME_LIMIT", "WORKSPACE_FS_CANCELLED", "WORKSPACE_FS_VERSION_REQUIRED", "WORKSPACE_FS_VERSION_CONFLICT", "WORKSPACE_FS_PATCH_INVALID", "WORKSPACE_FS_WATCH_OVERFLOW", "WORKSPACE_FS_POLICY_DENIED", "WORKSPACE_FS_POLICY_INVALID", "WORKSPACE_FS_ATOMIC_REPLACE_UNSUPPORTED"];
|
|
2
|
+
export type WorkspaceFilesystemErrorCode = (typeof WORKSPACE_FILESYSTEM_ERROR_CODES)[number];
|
|
3
|
+
export declare class WorkspaceFilesystemError extends Error {
|
|
4
|
+
readonly code: WorkspaceFilesystemErrorCode;
|
|
5
|
+
readonly operation?: string;
|
|
6
|
+
readonly relativePath?: string;
|
|
7
|
+
constructor(code: WorkspaceFilesystemErrorCode, message?: string, details?: {
|
|
8
|
+
operation?: string;
|
|
9
|
+
relativePath?: string;
|
|
10
|
+
});
|
|
11
|
+
}
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const WORKSPACE_FILESYSTEM_ERROR_CODES = [
|
|
2
|
+
"WORKSPACE_FS_INVALID_ROOT",
|
|
3
|
+
"WORKSPACE_FS_INVALID_PATH",
|
|
4
|
+
"WORKSPACE_FS_PATH_ESCAPE",
|
|
5
|
+
"WORKSPACE_FS_SYMLINK_DISALLOWED",
|
|
6
|
+
"WORKSPACE_FS_HARDLINK_DISALLOWED",
|
|
7
|
+
"WORKSPACE_FS_SPECIAL_FILE_DISALLOWED",
|
|
8
|
+
"WORKSPACE_FS_NOT_FOUND",
|
|
9
|
+
"WORKSPACE_FS_PERMISSION_DENIED",
|
|
10
|
+
"WORKSPACE_FS_INVALID_TARGET",
|
|
11
|
+
"WORKSPACE_FS_BINARY_FILE",
|
|
12
|
+
"WORKSPACE_FS_INVALID_ENCODING",
|
|
13
|
+
"WORKSPACE_FS_QUERY_INVALID",
|
|
14
|
+
"WORKSPACE_FS_SIZE_LIMIT",
|
|
15
|
+
"WORKSPACE_FS_FILE_LIMIT",
|
|
16
|
+
"WORKSPACE_FS_MATCH_LIMIT",
|
|
17
|
+
"WORKSPACE_FS_LINE_LIMIT",
|
|
18
|
+
"WORKSPACE_FS_TIME_LIMIT",
|
|
19
|
+
"WORKSPACE_FS_CANCELLED",
|
|
20
|
+
"WORKSPACE_FS_VERSION_REQUIRED",
|
|
21
|
+
"WORKSPACE_FS_VERSION_CONFLICT",
|
|
22
|
+
"WORKSPACE_FS_PATCH_INVALID",
|
|
23
|
+
"WORKSPACE_FS_WATCH_OVERFLOW",
|
|
24
|
+
"WORKSPACE_FS_POLICY_DENIED",
|
|
25
|
+
"WORKSPACE_FS_POLICY_INVALID",
|
|
26
|
+
"WORKSPACE_FS_ATOMIC_REPLACE_UNSUPPORTED",
|
|
27
|
+
];
|
|
28
|
+
export class WorkspaceFilesystemError extends Error {
|
|
29
|
+
code;
|
|
30
|
+
operation;
|
|
31
|
+
relativePath;
|
|
32
|
+
constructor(code, message, details) {
|
|
33
|
+
super(message ?? code);
|
|
34
|
+
this.name = "WorkspaceFilesystemError";
|
|
35
|
+
this.code = code;
|
|
36
|
+
this.operation = details?.operation;
|
|
37
|
+
this.relativePath = details?.relativePath;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type GraphIndexResponse, type GraphProvider } from "@blokjs/shared";
|
|
2
|
+
export interface GraphIndexerEvent {
|
|
3
|
+
readonly type: "enqueued" | "started" | "completed" | "failed" | "cancelled";
|
|
4
|
+
readonly key: string;
|
|
5
|
+
readonly queued: number;
|
|
6
|
+
readonly active: number;
|
|
7
|
+
readonly error?: string;
|
|
8
|
+
}
|
|
9
|
+
export interface GraphIndexJobHandle {
|
|
10
|
+
readonly key: string;
|
|
11
|
+
readonly promise: Promise<GraphIndexResponse>;
|
|
12
|
+
cancel(): void;
|
|
13
|
+
}
|
|
14
|
+
export interface GraphIndexerEnqueueOptions {
|
|
15
|
+
readonly signal?: AbortSignal;
|
|
16
|
+
}
|
|
17
|
+
export interface BoundedGraphIndexerOptions {
|
|
18
|
+
readonly maxQueueSize?: number;
|
|
19
|
+
readonly maxConcurrent?: number;
|
|
20
|
+
readonly maxFilesPerJob?: number;
|
|
21
|
+
readonly maxBytesPerJob?: number;
|
|
22
|
+
readonly onEvent?: (event: GraphIndexerEvent) => void;
|
|
23
|
+
}
|
|
24
|
+
/** A bounded, cancellable and content-deduplicating index work queue. */
|
|
25
|
+
export declare class BoundedGraphIndexer {
|
|
26
|
+
private readonly provider;
|
|
27
|
+
private readonly maxQueueSize;
|
|
28
|
+
private readonly maxConcurrent;
|
|
29
|
+
private readonly maxFilesPerJob;
|
|
30
|
+
private readonly maxBytesPerJob;
|
|
31
|
+
private readonly onEvent?;
|
|
32
|
+
private readonly jobs;
|
|
33
|
+
private readonly queue;
|
|
34
|
+
private active;
|
|
35
|
+
constructor(provider: GraphProvider, options?: BoundedGraphIndexerOptions);
|
|
36
|
+
get queued(): number;
|
|
37
|
+
get running(): number;
|
|
38
|
+
enqueue(input: unknown, options?: GraphIndexerEnqueueOptions): GraphIndexJobHandle;
|
|
39
|
+
close(): void;
|
|
40
|
+
private cancel;
|
|
41
|
+
private emit;
|
|
42
|
+
private pump;
|
|
43
|
+
private run;
|
|
44
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { GRAPH_MAX_FILES_PER_INDEX, GRAPH_MAX_INDEX_BYTES, parseGraphIndexRequest, } from "@blokjs/shared";
|
|
2
|
+
import { GraphProviderError } from "./GraphProviderError.js";
|
|
3
|
+
function identityKey(request) {
|
|
4
|
+
const files = [...request.files]
|
|
5
|
+
.sort((left, right) => left.path.localeCompare(right.path))
|
|
6
|
+
.map((file) => `${file.path}:${file.contentHash}`);
|
|
7
|
+
return JSON.stringify({
|
|
8
|
+
repository: request.scope.repository,
|
|
9
|
+
worktree: request.scope.worktree,
|
|
10
|
+
commit: request.scope.commit,
|
|
11
|
+
files,
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
function requestBytes(request) {
|
|
15
|
+
return new TextEncoder().encode(JSON.stringify(request)).byteLength;
|
|
16
|
+
}
|
|
17
|
+
/** A bounded, cancellable and content-deduplicating index work queue. */
|
|
18
|
+
export class BoundedGraphIndexer {
|
|
19
|
+
provider;
|
|
20
|
+
maxQueueSize;
|
|
21
|
+
maxConcurrent;
|
|
22
|
+
maxFilesPerJob;
|
|
23
|
+
maxBytesPerJob;
|
|
24
|
+
onEvent;
|
|
25
|
+
jobs = new Map();
|
|
26
|
+
queue = [];
|
|
27
|
+
active = 0;
|
|
28
|
+
constructor(provider, options = {}) {
|
|
29
|
+
this.provider = provider;
|
|
30
|
+
this.maxQueueSize = options.maxQueueSize ?? 64;
|
|
31
|
+
this.maxConcurrent = options.maxConcurrent ?? 2;
|
|
32
|
+
this.maxFilesPerJob = options.maxFilesPerJob ?? GRAPH_MAX_FILES_PER_INDEX;
|
|
33
|
+
this.maxBytesPerJob = options.maxBytesPerJob ?? GRAPH_MAX_INDEX_BYTES;
|
|
34
|
+
this.onEvent = options.onEvent;
|
|
35
|
+
if (!Number.isSafeInteger(this.maxQueueSize) || this.maxQueueSize <= 0)
|
|
36
|
+
throw new Error("maxQueueSize must be positive");
|
|
37
|
+
if (!Number.isSafeInteger(this.maxConcurrent) || this.maxConcurrent <= 0)
|
|
38
|
+
throw new Error("maxConcurrent must be positive");
|
|
39
|
+
}
|
|
40
|
+
get queued() {
|
|
41
|
+
return this.queue.length;
|
|
42
|
+
}
|
|
43
|
+
get running() {
|
|
44
|
+
return this.active;
|
|
45
|
+
}
|
|
46
|
+
enqueue(input, options = {}) {
|
|
47
|
+
if (options.signal?.aborted)
|
|
48
|
+
throw GraphProviderError.cancelled();
|
|
49
|
+
let request;
|
|
50
|
+
try {
|
|
51
|
+
request = parseGraphIndexRequest(input);
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw new GraphProviderError("invalid-query", "GRAPH_INVALID_INDEX_REQUEST", error instanceof Error ? error.message : "Invalid graph index request", { guidance: "narrow-query" });
|
|
55
|
+
}
|
|
56
|
+
if (request.files.length > this.maxFilesPerJob) {
|
|
57
|
+
throw new GraphProviderError("limit-exceeded", "GRAPH_INDEX_FILE_LIMIT", "Index request exceeds the configured file limit", {
|
|
58
|
+
guidance: "narrow-query",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
if (requestBytes(request) > this.maxBytesPerJob) {
|
|
62
|
+
throw new GraphProviderError("limit-exceeded", "GRAPH_INDEX_BYTE_LIMIT", "Index request exceeds the configured byte limit", {
|
|
63
|
+
guidance: "narrow-query",
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const key = identityKey(request);
|
|
67
|
+
const existing = this.jobs.get(key);
|
|
68
|
+
if (existing)
|
|
69
|
+
return { key, promise: existing.promise, cancel: () => this.cancel(existing) };
|
|
70
|
+
if (this.jobs.size >= this.maxQueueSize) {
|
|
71
|
+
throw new GraphProviderError("limit-exceeded", "GRAPH_INDEX_QUEUE_FULL", "Graph index queue is full", {
|
|
72
|
+
retryable: true,
|
|
73
|
+
guidance: "retry",
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
let resolve;
|
|
77
|
+
let reject;
|
|
78
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
79
|
+
resolve = resolvePromise;
|
|
80
|
+
reject = rejectPromise;
|
|
81
|
+
});
|
|
82
|
+
const job = { key, request, controller: new AbortController(), promise, resolve, reject, started: false };
|
|
83
|
+
this.jobs.set(key, job);
|
|
84
|
+
this.queue.push(job);
|
|
85
|
+
if (options.signal) {
|
|
86
|
+
if (options.signal.aborted) {
|
|
87
|
+
this.cancel(job);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
options.signal.addEventListener("abort", () => this.cancel(job), { once: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
this.emit({ type: "enqueued", key });
|
|
94
|
+
this.pump();
|
|
95
|
+
return { key, promise, cancel: () => this.cancel(job) };
|
|
96
|
+
}
|
|
97
|
+
close() {
|
|
98
|
+
for (const job of this.jobs.values())
|
|
99
|
+
this.cancel(job);
|
|
100
|
+
}
|
|
101
|
+
cancel(job) {
|
|
102
|
+
if (job.controller.signal.aborted)
|
|
103
|
+
return;
|
|
104
|
+
job.controller.abort();
|
|
105
|
+
if (job.started)
|
|
106
|
+
return;
|
|
107
|
+
const index = this.queue.indexOf(job);
|
|
108
|
+
if (index >= 0)
|
|
109
|
+
this.queue.splice(index, 1);
|
|
110
|
+
this.jobs.delete(job.key);
|
|
111
|
+
job.reject(GraphProviderError.cancelled());
|
|
112
|
+
this.emit({ type: "cancelled", key: job.key });
|
|
113
|
+
this.pump();
|
|
114
|
+
}
|
|
115
|
+
emit(event) {
|
|
116
|
+
this.onEvent?.({ ...event, queued: this.queue.length, active: this.active });
|
|
117
|
+
}
|
|
118
|
+
pump() {
|
|
119
|
+
while (this.active < this.maxConcurrent && this.queue.length > 0) {
|
|
120
|
+
const job = this.queue.shift();
|
|
121
|
+
if (!job)
|
|
122
|
+
return;
|
|
123
|
+
if (job.controller.signal.aborted) {
|
|
124
|
+
this.jobs.delete(job.key);
|
|
125
|
+
job.reject(GraphProviderError.cancelled());
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
job.started = true;
|
|
129
|
+
this.active += 1;
|
|
130
|
+
this.emit({ type: "started", key: job.key });
|
|
131
|
+
void this.run(job);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
async run(job) {
|
|
135
|
+
try {
|
|
136
|
+
const result = await this.provider.index(job.request, { signal: job.controller.signal });
|
|
137
|
+
if (job.controller.signal.aborted)
|
|
138
|
+
throw GraphProviderError.cancelled();
|
|
139
|
+
job.resolve(result);
|
|
140
|
+
this.emit({ type: "completed", key: job.key });
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
const failure = job.controller.signal.aborted ? GraphProviderError.cancelled() : error;
|
|
144
|
+
job.reject(failure);
|
|
145
|
+
this.emit({
|
|
146
|
+
type: failure instanceof GraphProviderError && failure.category === "cancelled" ? "cancelled" : "failed",
|
|
147
|
+
key: job.key,
|
|
148
|
+
error: String(failure),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
finally {
|
|
152
|
+
this.active -= 1;
|
|
153
|
+
this.jobs.delete(job.key);
|
|
154
|
+
this.pump();
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type GraphFreshnessRequest, type GraphImpactRequest, type GraphIndexFile, type GraphIndexRequest, type GraphIndexResponse, type GraphLocation, type GraphProvider, type GraphQueryOptions, type GraphQueryResponse, type GraphRelation, type GraphRelationRequest, type GraphScope, type GraphSearchHit, type GraphSearchRequest, type GraphSymbol, type GraphSymbolRequest } from "@blokjs/shared";
|
|
2
|
+
type FakeOperation = "search" | "symbol" | "relations" | "impact" | "freshness" | "index";
|
|
3
|
+
export interface FakeGraphProviderOptions {
|
|
4
|
+
readonly scope?: GraphScope;
|
|
5
|
+
readonly files?: readonly GraphIndexFile[];
|
|
6
|
+
readonly supportedOperations?: readonly FakeOperation[];
|
|
7
|
+
readonly now?: () => string;
|
|
8
|
+
}
|
|
9
|
+
/** Deterministic in-memory provider used for contract tests and local adapters. */
|
|
10
|
+
export declare class FakeGraphProvider implements GraphProvider {
|
|
11
|
+
readonly id = "fake";
|
|
12
|
+
readonly version = "fake-1";
|
|
13
|
+
private readonly symbols;
|
|
14
|
+
private readonly relationsById;
|
|
15
|
+
private readonly indexedHashes;
|
|
16
|
+
private readonly supported;
|
|
17
|
+
private readonly now;
|
|
18
|
+
private indexedScope?;
|
|
19
|
+
private indexedAt?;
|
|
20
|
+
private indexVersion;
|
|
21
|
+
constructor(options?: FakeGraphProviderOptions);
|
|
22
|
+
seed(scope: GraphScope, files: readonly GraphIndexFile[]): void;
|
|
23
|
+
search(input: GraphSearchRequest, options?: GraphQueryOptions): Promise<GraphQueryResponse<GraphSearchHit>>;
|
|
24
|
+
findSymbol(input: GraphSymbolRequest, options?: GraphQueryOptions): Promise<GraphQueryResponse<GraphSymbol>>;
|
|
25
|
+
relations(input: GraphRelationRequest, options?: GraphQueryOptions): Promise<GraphQueryResponse<GraphRelation>>;
|
|
26
|
+
impact(input: GraphImpactRequest, options?: GraphQueryOptions): Promise<GraphQueryResponse<GraphSymbol>>;
|
|
27
|
+
freshness(input: GraphFreshnessRequest, options?: GraphQueryOptions): Promise<GraphQueryResponse<GraphLocation>>;
|
|
28
|
+
index(input: GraphIndexRequest, options?: GraphQueryOptions): Promise<GraphIndexResponse>;
|
|
29
|
+
private applyIndex;
|
|
30
|
+
private score;
|
|
31
|
+
private parse;
|
|
32
|
+
private unsupported;
|
|
33
|
+
private scopeMetadata;
|
|
34
|
+
}
|
|
35
|
+
export {};
|