git-edge 0.4.1 → 0.4.2
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 +1 -0
- package/dist/index.js +57 -15
- package/dist/index.js.map +1 -1
- package/dist/types/merge.d.ts +10 -0
- package/dist/types/semaphore.d.ts +27 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -109,6 +109,7 @@ Per-repo isomorphic-git packfile `cache` object management, keyed `` `${ownerKey
|
|
|
109
109
|
|
|
110
110
|
- `threeWayMerge`'s content merge (for paths both sides changed from the merge base — most changes are added/deleted/single-side-modified and resolve without touching it at all) uses [`node-diff3`](https://www.npmjs.com/package/node-diff3)'s LCS-based diff3 algorithm, the same approach GNU diffutils' `diff3` and git's own default merge driver use.
|
|
111
111
|
- Binary files (a NUL byte in any version) are never line-merged; both sides changing one is a conflict. No rename detection; criss-cross histories use the first merge base.
|
|
112
|
+
- Object reads and writes are bounded (`MAX_CONCURRENT_READS = 16` in-flight per merge, held for each individual `readTree`/`readBlob`/`writeBlob`/`writeTree` call rather than across the recursive subtree merge). A directory's entries are merged concurrently, so an unbounded fan-out over a repo with tens of thousands of entries in one directory would otherwise issue that many simultaneous store requests from a single merge, with each `readBlob` holding a whole blob in memory.
|
|
112
113
|
- The parsed-object cache and `getRepoCache`/`invalidateRepoCache` are general-purpose helpers, not required by `threeWayMerge` — use whichever pieces you need independently.
|
|
113
114
|
|
|
114
115
|
## License
|
package/dist/index.js
CHANGED
|
@@ -63,19 +63,57 @@ var GitMergeConflictError = class extends GitEdgeError {
|
|
|
63
63
|
import { withPackedWrites } from "git-fs-s3/ops";
|
|
64
64
|
import git from "isomorphic-git";
|
|
65
65
|
import { merge as diff3Merge } from "node-diff3";
|
|
66
|
+
|
|
67
|
+
// src/semaphore.ts
|
|
68
|
+
function createSemaphore(limit) {
|
|
69
|
+
if (!Number.isInteger(limit) || limit < 1) {
|
|
70
|
+
throw new RangeError(
|
|
71
|
+
`Semaphore limit must be a positive integer, got ${limit}`
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
let active = 0;
|
|
75
|
+
const waiting = [];
|
|
76
|
+
function acquire() {
|
|
77
|
+
if (active < limit) {
|
|
78
|
+
active += 1;
|
|
79
|
+
return Promise.resolve();
|
|
80
|
+
}
|
|
81
|
+
return new Promise((resolve) => waiting.push(resolve));
|
|
82
|
+
}
|
|
83
|
+
function release() {
|
|
84
|
+
const next = waiting.shift();
|
|
85
|
+
if (next) next();
|
|
86
|
+
else active -= 1;
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
async run(fn) {
|
|
90
|
+
await acquire();
|
|
91
|
+
try {
|
|
92
|
+
return await fn();
|
|
93
|
+
} finally {
|
|
94
|
+
release();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/merge.ts
|
|
101
|
+
var MAX_CONCURRENT_READS = 16;
|
|
66
102
|
var isTree = (entry) => entry?.type === "tree";
|
|
67
103
|
var isFile = (entry) => entry?.type === "blob";
|
|
68
104
|
function same(a, b) {
|
|
69
105
|
if (a === void 0 || b === void 0) return a === b;
|
|
70
106
|
return a.oid === b.oid && a.mode === b.mode;
|
|
71
107
|
}
|
|
72
|
-
async function readEntries(repo, treeOid) {
|
|
108
|
+
async function readEntries(repo, reads, treeOid) {
|
|
73
109
|
if (treeOid === void 0) return /* @__PURE__ */ new Map();
|
|
74
|
-
const { tree } = await
|
|
110
|
+
const { tree } = await reads.run(
|
|
111
|
+
() => git.readTree({ ...repo, oid: treeOid })
|
|
112
|
+
);
|
|
75
113
|
return new Map(tree.map((entry) => [entry.path, entry]));
|
|
76
114
|
}
|
|
77
|
-
async function readBlob(repo, oid) {
|
|
78
|
-
const { blob } = await git.readBlob({ ...repo, oid });
|
|
115
|
+
async function readBlob(repo, reads, oid) {
|
|
116
|
+
const { blob } = await reads.run(() => git.readBlob({ ...repo, oid }));
|
|
79
117
|
return blob;
|
|
80
118
|
}
|
|
81
119
|
function mergeText(base, ours, theirs) {
|
|
@@ -98,10 +136,11 @@ function mergeMode(base, ours, theirs) {
|
|
|
98
136
|
if (base?.mode === theirs.mode) return ours.mode;
|
|
99
137
|
return null;
|
|
100
138
|
}
|
|
101
|
-
async function mergeChangedEntry(repo, path, base, ours, theirs, conflicts) {
|
|
139
|
+
async function mergeChangedEntry(repo, reads, path, base, ours, theirs, conflicts) {
|
|
102
140
|
if (isTree(ours) && isTree(theirs)) {
|
|
103
141
|
const oid2 = await mergeTrees(
|
|
104
142
|
repo,
|
|
143
|
+
reads,
|
|
105
144
|
path,
|
|
106
145
|
isTree(base) ? base.oid : void 0,
|
|
107
146
|
ours.oid,
|
|
@@ -121,9 +160,9 @@ async function mergeChangedEntry(repo, path, base, ours, theirs, conflicts) {
|
|
|
121
160
|
}
|
|
122
161
|
if (ours.oid === theirs.oid) return { mode, oid: ours.oid, type: "blob" };
|
|
123
162
|
const [baseContent, oursContent, theirsContent] = await Promise.all([
|
|
124
|
-
isFile(base) ? readBlob(repo, base.oid) : new Uint8Array(),
|
|
125
|
-
readBlob(repo, ours.oid),
|
|
126
|
-
readBlob(repo, theirs.oid)
|
|
163
|
+
isFile(base) ? readBlob(repo, reads, base.oid) : new Uint8Array(),
|
|
164
|
+
readBlob(repo, reads, ours.oid),
|
|
165
|
+
readBlob(repo, reads, theirs.oid)
|
|
127
166
|
]);
|
|
128
167
|
if (isBinary(baseContent) || isBinary(oursContent) || isBinary(theirsContent)) {
|
|
129
168
|
conflicts.push(path);
|
|
@@ -138,14 +177,14 @@ async function mergeChangedEntry(repo, path, base, ours, theirs, conflicts) {
|
|
|
138
177
|
conflicts.push(path);
|
|
139
178
|
return null;
|
|
140
179
|
}
|
|
141
|
-
const oid = await git.writeBlob({ ...repo, blob: content });
|
|
180
|
+
const oid = await reads.run(() => git.writeBlob({ ...repo, blob: content }));
|
|
142
181
|
return { mode, oid, type: "blob" };
|
|
143
182
|
}
|
|
144
|
-
async function mergeTrees(repo, prefix, baseOid, oursOid, theirsOid, conflicts) {
|
|
183
|
+
async function mergeTrees(repo, reads, prefix, baseOid, oursOid, theirsOid, conflicts) {
|
|
145
184
|
const [base, ours, theirs] = await Promise.all([
|
|
146
|
-
readEntries(repo, baseOid),
|
|
147
|
-
readEntries(repo, oursOid),
|
|
148
|
-
readEntries(repo, theirsOid)
|
|
185
|
+
readEntries(repo, reads, baseOid),
|
|
186
|
+
readEntries(repo, reads, oursOid),
|
|
187
|
+
readEntries(repo, reads, theirsOid)
|
|
149
188
|
]);
|
|
150
189
|
const names = /* @__PURE__ */ new Set([...base.keys(), ...ours.keys(), ...theirs.keys()]);
|
|
151
190
|
const merged = [];
|
|
@@ -160,12 +199,13 @@ async function mergeTrees(repo, prefix, baseOid, oursOid, theirsOid, conflicts)
|
|
|
160
199
|
if (same(o, t)) result = o;
|
|
161
200
|
else if (same(b, o)) result = t;
|
|
162
201
|
else if (same(b, t)) result = o;
|
|
163
|
-
else
|
|
202
|
+
else
|
|
203
|
+
result = await mergeChangedEntry(repo, reads, path, b, o, t, conflicts);
|
|
164
204
|
if (result) merged.push({ ...result, path: name });
|
|
165
205
|
})
|
|
166
206
|
);
|
|
167
207
|
if (conflicts.length > before) return null;
|
|
168
|
-
return git.writeTree({ ...repo, tree: merged });
|
|
208
|
+
return reads.run(() => git.writeTree({ ...repo, tree: merged }));
|
|
169
209
|
}
|
|
170
210
|
async function threeWayMerge(repo, sourceRef, targetRef, opts) {
|
|
171
211
|
const [sourceOid, targetOid] = await Promise.all([
|
|
@@ -199,10 +239,12 @@ async function threeWayMerge(repo, sourceRef, targetRef, opts) {
|
|
|
199
239
|
git.readCommit({ ...repo, oid: sourceOid }),
|
|
200
240
|
git.readCommit({ ...repo, oid: targetOid })
|
|
201
241
|
]);
|
|
242
|
+
const reads = createSemaphore(MAX_CONCURRENT_READS);
|
|
202
243
|
return withPackedWrites(repo, async (packed) => {
|
|
203
244
|
const conflicts = [];
|
|
204
245
|
const treeOid = await mergeTrees(
|
|
205
246
|
packed,
|
|
247
|
+
reads,
|
|
206
248
|
"",
|
|
207
249
|
baseCommit.commit.tree,
|
|
208
250
|
targetCommit.commit.tree,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/cache.ts","../src/errors.ts","../src/merge.ts","../src/repo.ts"],"sourcesContent":["/**\n * Generalized parsed-object LRU cache for isomorphic-git.\n *\n * Stores JS values directly — avoids JSON.parse on every hot-path read.\n * Configurable maxSize (rough byte estimate) and TTL.\n *\n * Edge-compatible: no node: imports.\n */\nimport { LRUCache } from \"lru-cache\";\n\nexport interface ParsedObjectCacheOptions {\n\t/** Maximum estimated size in bytes (default: 256 MB). */\n\tmaxSize?: number;\n\t/** Time-to-live in milliseconds (default: 1 hour). */\n\tttl?: number;\n}\n\nexport interface ParsedObjectStore {\n\tget<T extends object>(key: string): T | null;\n\tset<T extends object>(key: string, value: T): void;\n\tdelete(key: string): void;\n\t/** Delete all keys matching a prefix. */\n\tinvalidatePrefix(prefix: string): void;\n}\n\n/**\n * Create a parsed-object cache instance.\n *\n * `key` convention: `\"${oid}:${format}\"` or any caller-chosen string.\n * `sizeCalculation` walks the value for a rough byte estimate.\n */\n// Rough byte estimate without serializing: JSON.stringify on every set\n// allocated a full copy of large values (commit logs, trees) and expanded\n// byte arrays into decimal text, both on the hot path.\nfunction estimateSize(value: unknown): number {\n\tif (typeof value === \"string\") return value.length;\n\tif (value === null || typeof value !== \"object\") return 8;\n\tif (ArrayBuffer.isView(value)) return value.byteLength;\n\tlet size = 0;\n\tif (Array.isArray(value)) {\n\t\tfor (const item of value) size += estimateSize(item);\n\t\treturn size;\n\t}\n\tfor (const [k, v] of Object.entries(value)) {\n\t\tsize += k.length + estimateSize(v);\n\t}\n\treturn size;\n}\n\nexport function createParsedObjectCache(\n\toptions?: ParsedObjectCacheOptions,\n): ParsedObjectStore {\n\tconst maxSize = options?.maxSize ?? 256 * 1024 * 1024;\n\tconst ttl = options?.ttl ?? 3600 * 1000;\n\n\tconst cache = new LRUCache<string, object>({\n\t\tmaxSize,\n\t\tsizeCalculation: (v) => Math.max(1, estimateSize(v)),\n\t\tttl,\n\t});\n\n\treturn {\n\t\tget<T extends object>(key: string): T | null {\n\t\t\treturn (cache.get(key) as T) ?? null;\n\t\t},\n\t\tset<T extends object>(key: string, value: T): void {\n\t\t\tcache.set(key, value);\n\t\t},\n\t\tdelete(key: string): void {\n\t\t\tcache.delete(key);\n\t\t},\n\t\tinvalidatePrefix(prefix: string): void {\n\t\t\tfor (const k of cache.keys()) {\n\t\t\t\tif (k.startsWith(prefix)) cache.delete(k);\n\t\t\t}\n\t\t},\n\t};\n}\n","/**\n * Error types for git-edge.\n *\n * Edge-compatible: no node: imports.\n */\n\n/** Base class for all git-edge errors. */\nexport class GitEdgeError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"GitEdgeError\";\n\t\tError.captureStackTrace?.(this, this.constructor);\n\t}\n}\n\n/** Thrown when a three-way merge has unresolvable conflicts. */\nexport class GitMergeConflictError extends GitEdgeError {\n\treadonly conflictingPaths: string[];\n\n\tconstructor(conflictingPaths: string[]) {\n\t\tsuper(\n\t\t\t`Merge conflict in ${conflictingPaths.length} file(s): ${conflictingPaths.join(\", \")}`,\n\t\t);\n\t\tthis.name = \"GitMergeConflictError\";\n\t\tthis.conflictingPaths = conflictingPaths;\n\t}\n}\n","/**\n * Object-level three-way merge for isomorphic-git.\n *\n * Merges two commits by walking their trees directly — no worktree, no disk.\n * Returns the merged commit OID, or throws GitMergeConflictError with the\n * list of conflicting paths.\n *\n * Edge-compatible: no node: imports, uses only isomorphic-git object APIs.\n */\n\nimport { type Repo, withPackedWrites } from \"git-fs-s3/ops\";\nimport git from \"isomorphic-git\";\nimport { merge as diff3Merge } from \"node-diff3\";\nimport { GitEdgeError, GitMergeConflictError } from \"./errors.js\";\n\nexport type { Repo };\n\n/** Options for threeWayMerge. */\nexport interface MergeOpts {\n\t/** Merge commit message (default: \"Merge <source> into <target>\"). */\n\tmessage?: string;\n\t/** Author name (default: \"Git Edge\"). */\n\tauthorName?: string;\n\t/** Author email (default: \"git-edge@local\"). */\n\tauthorEmail?: string;\n}\n\n/** Result of a successful merge (or of a no-op: target already up to date). */\nexport interface MergeResult {\n\tcommitOid: string;\n}\n\ninterface Entry {\n\tmode: string;\n\toid: string;\n\ttype: \"blob\" | \"tree\" | \"commit\";\n}\n\nconst isTree = (entry: Entry | undefined): entry is Entry =>\n\tentry?.type === \"tree\";\nconst isFile = (entry: Entry | undefined): entry is Entry =>\n\tentry?.type === \"blob\";\n\n/** Same tree entry: both absent, or identical oid and mode. */\nfunction same(a: Entry | undefined, b: Entry | undefined): boolean {\n\tif (a === undefined || b === undefined) return a === b;\n\treturn a.oid === b.oid && a.mode === b.mode;\n}\n\nasync function readEntries(\n\trepo: Repo,\n\ttreeOid: string | undefined,\n): Promise<Map<string, Entry>> {\n\tif (treeOid === undefined) return new Map();\n\tconst { tree } = await git.readTree({ ...repo, oid: treeOid });\n\treturn new Map(tree.map((entry) => [entry.path, entry]));\n}\n\nasync function readBlob(repo: Repo, oid: string): Promise<Uint8Array> {\n\tconst { blob } = await git.readBlob({ ...repo, oid });\n\treturn blob;\n}\n\n/**\n * Line-level 3-way merge, via node-diff3's LCS-based diff3 algorithm — the\n * same approach GNU diffutils' `diff3` (and git's own default merge driver)\n * use. `conflict` reports whether any hunk couldn't be merged.\n */\nfunction mergeText(\n\tbase: Uint8Array,\n\tours: Uint8Array,\n\ttheirs: Uint8Array,\n): { content: Uint8Array; conflict: boolean } {\n\tconst { conflict, result } = diff3Merge(\n\t\tdecodeLines(ours),\n\t\tdecodeLines(base),\n\t\tdecodeLines(theirs),\n\t\t{ label: { a: \"ours\", b: \"theirs\" } },\n\t);\n\treturn { content: new TextEncoder().encode(result.join(\"\\n\")), conflict };\n}\n\nfunction decodeLines(data: Uint8Array): string[] {\n\tconst text = new TextDecoder().decode(data);\n\treturn text === \"\" ? [] : text.split(\"\\n\");\n}\n\n/** Git's own heuristic: a NUL byte means binary, which is never line-merged. */\nconst isBinary = (data: Uint8Array) => data.includes(0);\n\n/**\n * The merged mode when both sides changed a file: a mode only one side\n * changed wins; both changing it differently is a conflict (null).\n */\nfunction mergeMode(\n\tbase: Entry | undefined,\n\tours: Entry,\n\ttheirs: Entry,\n): string | null {\n\tif (ours.mode === theirs.mode) return ours.mode;\n\tif (base?.mode === ours.mode) return theirs.mode;\n\tif (base?.mode === theirs.mode) return ours.mode;\n\treturn null;\n}\n\n/**\n * Merge one path whose two sides both changed it differently. Returns the\n * merged entry, or null for a conflict (pushed onto `conflicts`).\n */\nasync function mergeChangedEntry(\n\trepo: Repo,\n\tpath: string,\n\tbase: Entry | undefined,\n\tours: Entry | undefined,\n\ttheirs: Entry | undefined,\n\tconflicts: string[],\n): Promise<Entry | null> {\n\tif (isTree(ours) && isTree(theirs)) {\n\t\tconst oid = await mergeTrees(\n\t\t\trepo,\n\t\t\tpath,\n\t\t\tisTree(base) ? base.oid : undefined,\n\t\t\tours.oid,\n\t\t\ttheirs.oid,\n\t\t\tconflicts,\n\t\t);\n\t\treturn oid === null ? null : { mode: \"040000\", oid, type: \"tree\" };\n\t}\n\t// Anything else — modify/delete, file/directory, submodule changes — is a\n\t// conflict unless both sides are regular files with mergeable content.\n\tif (!isFile(ours) || !isFile(theirs) || ours.mode === \"120000\") {\n\t\tconflicts.push(path);\n\t\treturn null;\n\t}\n\tconst mode = mergeMode(base, ours, theirs);\n\tif (mode === null) {\n\t\tconflicts.push(path);\n\t\treturn null;\n\t}\n\tif (ours.oid === theirs.oid) return { mode, oid: ours.oid, type: \"blob\" };\n\tconst [baseContent, oursContent, theirsContent] = await Promise.all([\n\t\tisFile(base) ? readBlob(repo, base.oid) : new Uint8Array(),\n\t\treadBlob(repo, ours.oid),\n\t\treadBlob(repo, theirs.oid),\n\t]);\n\tif (\n\t\tisBinary(baseContent) ||\n\t\tisBinary(oursContent) ||\n\t\tisBinary(theirsContent)\n\t) {\n\t\tconflicts.push(path);\n\t\treturn null;\n\t}\n\tconst { content, conflict } = mergeText(\n\t\tbaseContent,\n\t\toursContent,\n\t\ttheirsContent,\n\t);\n\tif (conflict) {\n\t\tconflicts.push(path);\n\t\treturn null;\n\t}\n\tconst oid = await git.writeBlob({ ...repo, blob: content });\n\treturn { mode, oid, type: \"blob\" };\n}\n\n/**\n * Three-way merge of trees, recursing only into subtrees both sides changed\n * — an untouched or one-sided subtree is taken by oid without being read.\n * Returns the merged tree's oid, or null when any path conflicted.\n */\nasync function mergeTrees(\n\trepo: Repo,\n\tprefix: string,\n\tbaseOid: string | undefined,\n\toursOid: string,\n\ttheirsOid: string,\n\tconflicts: string[],\n): Promise<string | null> {\n\tconst [base, ours, theirs] = await Promise.all([\n\t\treadEntries(repo, baseOid),\n\t\treadEntries(repo, oursOid),\n\t\treadEntries(repo, theirsOid),\n\t]);\n\tconst names = new Set([...base.keys(), ...ours.keys(), ...theirs.keys()]);\n\tconst merged: Array<Entry & { path: string }> = [];\n\tconst before = conflicts.length;\n\n\tawait Promise.all(\n\t\t[...names].map(async (name) => {\n\t\t\tconst path = prefix ? `${prefix}/${name}` : name;\n\t\t\tconst b = base.get(name);\n\t\t\tconst o = ours.get(name);\n\t\t\tconst t = theirs.get(name);\n\t\t\tlet result: Entry | undefined | null;\n\t\t\tif (same(o, t)) result = o;\n\t\t\telse if (same(b, o)) result = t;\n\t\t\telse if (same(b, t)) result = o;\n\t\t\telse result = await mergeChangedEntry(repo, path, b, o, t, conflicts);\n\t\t\tif (result) merged.push({ ...result, path: name });\n\t\t}),\n\t);\n\n\tif (conflicts.length > before) return null;\n\treturn git.writeTree({ ...repo, tree: merged });\n}\n\n/**\n * Merge `sourceRef` into `targetRef`, entirely at the object level.\n *\n * - Target already contains source (same commit, or source is an ancestor):\n * nothing to do — returns the target's commit, refs untouched.\n * - Source contains target: fast-forward the target ref to source.\n * - Otherwise: a real three-way merge against the merge base. Paths only one\n * side changed take that side (deletions included); paths both changed are\n * merged — subtrees recursively, text files line by line (diff3), modes\n * kept unless both sides changed them differently. Binary files, symlinks,\n * submodules, modify/delete and file/directory clashes that both sides\n * touched are conflicts. Any conflict throws `GitMergeConflictError`\n * without writing anything.\n *\n * New objects are stored as one pack, written before the target ref moves.\n * Serialize concurrent writers to the same repository externally.\n */\nexport async function threeWayMerge(\n\trepo: Repo,\n\tsourceRef: string,\n\ttargetRef: string,\n\topts?: MergeOpts,\n): Promise<MergeResult> {\n\tconst [sourceOid, targetOid] = await Promise.all([\n\t\tgit.resolveRef({ ...repo, ref: sourceRef }),\n\t\tgit.resolveRef({ ...repo, ref: targetRef }),\n\t]);\n\n\tif (\n\t\tsourceOid === targetOid ||\n\t\t(await git.isDescendent({ ...repo, oid: targetOid, ancestor: sourceOid }))\n\t) {\n\t\treturn { commitOid: targetOid };\n\t}\n\n\tif (\n\t\tawait git.isDescendent({ ...repo, oid: sourceOid, ancestor: targetOid })\n\t) {\n\t\tawait git.writeRef({\n\t\t\t...repo,\n\t\t\tref: targetRef,\n\t\t\tvalue: sourceOid,\n\t\t\tforce: true,\n\t\t});\n\t\treturn { commitOid: sourceOid };\n\t}\n\n\tconst baseOids = await git.findMergeBase({\n\t\t...repo,\n\t\toids: [sourceOid, targetOid],\n\t});\n\tconst baseOid = baseOids?.[0];\n\tif (baseOid === undefined) {\n\t\tthrow new GitEdgeError(\n\t\t\t`Refusing to merge unrelated histories (${sourceRef} and ${targetRef} share no commit)`,\n\t\t);\n\t}\n\n\tconst [baseCommit, sourceCommit, targetCommit] = await Promise.all([\n\t\tgit.readCommit({ ...repo, oid: baseOid }),\n\t\tgit.readCommit({ ...repo, oid: sourceOid }),\n\t\tgit.readCommit({ ...repo, oid: targetOid }),\n\t]);\n\n\treturn withPackedWrites(repo, async (packed) => {\n\t\tconst conflicts: string[] = [];\n\t\tconst treeOid = await mergeTrees(\n\t\t\tpacked,\n\t\t\t\"\",\n\t\t\tbaseCommit.commit.tree,\n\t\t\ttargetCommit.commit.tree,\n\t\t\tsourceCommit.commit.tree,\n\t\t\tconflicts,\n\t\t);\n\t\tif (treeOid === null) {\n\t\t\tthrow new GitMergeConflictError(conflicts.sort());\n\t\t}\n\n\t\tconst author = {\n\t\t\tname: opts?.authorName || \"Git Edge\",\n\t\t\temail: opts?.authorEmail || \"git-edge@local\",\n\t\t\ttimestamp: Math.floor(Date.now() / 1000),\n\t\t\ttimezoneOffset: 0,\n\t\t};\n\t\t// writeCommit, not git.commit: git.commit also advances whatever\n\t\t// branch HEAD names, which may not be the target.\n\t\tconst commitOid = await git.writeCommit({\n\t\t\t...packed,\n\t\t\tcommit: {\n\t\t\t\tmessage: opts?.message || `Merge ${sourceRef} into ${targetRef}`,\n\t\t\t\ttree: treeOid,\n\t\t\t\tparent: [targetOid, sourceOid],\n\t\t\t\tauthor,\n\t\t\t\tcommitter: author,\n\t\t\t},\n\t\t});\n\t\tawait git.writeRef({\n\t\t\t...packed,\n\t\t\tref: targetRef,\n\t\t\tvalue: commitOid,\n\t\t\tforce: true,\n\t\t});\n\t\treturn { commitOid };\n\t});\n}\n","/**\n * isomorphic-git per-repo parse cache management.\n *\n * No node: imports.\n */\n\n// ---------------------------------------------------------------------------\n// isomorphic-git cache management\n// ---------------------------------------------------------------------------\n\n/**\n * Per-repo isomorphic-git packfile cache.\n *\n * isomorphic-git re-parses a packfile index from scratch on every\n * readTree/log/readObject call unless callers share a `cache` object\n * across calls. Without this, operations that touch many objects\n * (e.g. walking commit history) pay that parse cost hundreds of times.\n * Objects are content-addressed/immutable so a long-lived per-repo\n * cache is safe.\n */\n// Each cache retains every packfile it has read, in full. Bound the number of\n// repos held so a long-lived process serving many repos doesn't grow without\n// limit; least-recently-used repos are dropped first (Map keeps insertion\n// order, and a hit re-inserts its key at the end).\nconst MAX_REPO_CACHES = 32;\nconst repoCaches = new Map<string, object>();\n\nexport function getRepoCache(ownerKey: string, repoName: string): object {\n\tconst key = `${ownerKey}/${repoName}`;\n\tlet cache = repoCaches.get(key);\n\tif (cache) {\n\t\trepoCaches.delete(key);\n\t} else {\n\t\tcache = {};\n\t\tif (repoCaches.size >= MAX_REPO_CACHES) {\n\t\t\tconst oldest = repoCaches.keys().next().value;\n\t\t\tif (oldest !== undefined) repoCaches.delete(oldest);\n\t\t}\n\t}\n\trepoCaches.set(key, cache);\n\treturn cache;\n}\n\nexport function invalidateRepoCache(ownerKey: string, repoName: string): void {\n\trepoCaches.delete(`${ownerKey}/${repoName}`);\n}\n"],"mappings":"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","names":["oid"]}
|
|
1
|
+
{"version":3,"sources":["../src/cache.ts","../src/errors.ts","../src/merge.ts","../src/semaphore.ts","../src/repo.ts"],"sourcesContent":["/**\n * Generalized parsed-object LRU cache for isomorphic-git.\n *\n * Stores JS values directly — avoids JSON.parse on every hot-path read.\n * Configurable maxSize (rough byte estimate) and TTL.\n *\n * Edge-compatible: no node: imports.\n */\nimport { LRUCache } from \"lru-cache\";\n\nexport interface ParsedObjectCacheOptions {\n\t/** Maximum estimated size in bytes (default: 256 MB). */\n\tmaxSize?: number;\n\t/** Time-to-live in milliseconds (default: 1 hour). */\n\tttl?: number;\n}\n\nexport interface ParsedObjectStore {\n\tget<T extends object>(key: string): T | null;\n\tset<T extends object>(key: string, value: T): void;\n\tdelete(key: string): void;\n\t/** Delete all keys matching a prefix. */\n\tinvalidatePrefix(prefix: string): void;\n}\n\n/**\n * Create a parsed-object cache instance.\n *\n * `key` convention: `\"${oid}:${format}\"` or any caller-chosen string.\n * `sizeCalculation` walks the value for a rough byte estimate.\n */\n// Rough byte estimate without serializing: JSON.stringify on every set\n// allocated a full copy of large values (commit logs, trees) and expanded\n// byte arrays into decimal text, both on the hot path.\nfunction estimateSize(value: unknown): number {\n\tif (typeof value === \"string\") return value.length;\n\tif (value === null || typeof value !== \"object\") return 8;\n\tif (ArrayBuffer.isView(value)) return value.byteLength;\n\tlet size = 0;\n\tif (Array.isArray(value)) {\n\t\tfor (const item of value) size += estimateSize(item);\n\t\treturn size;\n\t}\n\tfor (const [k, v] of Object.entries(value)) {\n\t\tsize += k.length + estimateSize(v);\n\t}\n\treturn size;\n}\n\nexport function createParsedObjectCache(\n\toptions?: ParsedObjectCacheOptions,\n): ParsedObjectStore {\n\tconst maxSize = options?.maxSize ?? 256 * 1024 * 1024;\n\tconst ttl = options?.ttl ?? 3600 * 1000;\n\n\tconst cache = new LRUCache<string, object>({\n\t\tmaxSize,\n\t\tsizeCalculation: (v) => Math.max(1, estimateSize(v)),\n\t\tttl,\n\t});\n\n\treturn {\n\t\tget<T extends object>(key: string): T | null {\n\t\t\treturn (cache.get(key) as T) ?? null;\n\t\t},\n\t\tset<T extends object>(key: string, value: T): void {\n\t\t\tcache.set(key, value);\n\t\t},\n\t\tdelete(key: string): void {\n\t\t\tcache.delete(key);\n\t\t},\n\t\tinvalidatePrefix(prefix: string): void {\n\t\t\tfor (const k of cache.keys()) {\n\t\t\t\tif (k.startsWith(prefix)) cache.delete(k);\n\t\t\t}\n\t\t},\n\t};\n}\n","/**\n * Error types for git-edge.\n *\n * Edge-compatible: no node: imports.\n */\n\n/** Base class for all git-edge errors. */\nexport class GitEdgeError extends Error {\n\tconstructor(message: string) {\n\t\tsuper(message);\n\t\tthis.name = \"GitEdgeError\";\n\t\tError.captureStackTrace?.(this, this.constructor);\n\t}\n}\n\n/** Thrown when a three-way merge has unresolvable conflicts. */\nexport class GitMergeConflictError extends GitEdgeError {\n\treadonly conflictingPaths: string[];\n\n\tconstructor(conflictingPaths: string[]) {\n\t\tsuper(\n\t\t\t`Merge conflict in ${conflictingPaths.length} file(s): ${conflictingPaths.join(\", \")}`,\n\t\t);\n\t\tthis.name = \"GitMergeConflictError\";\n\t\tthis.conflictingPaths = conflictingPaths;\n\t}\n}\n","/**\n * Object-level three-way merge for isomorphic-git.\n *\n * Merges two commits by walking their trees directly — no worktree, no disk.\n * Returns the merged commit OID, or throws GitMergeConflictError with the\n * list of conflicting paths.\n *\n * Edge-compatible: no node: imports, uses only isomorphic-git object APIs.\n */\n\nimport { type Repo, withPackedWrites } from \"git-fs-s3/ops\";\nimport git from \"isomorphic-git\";\nimport { merge as diff3Merge } from \"node-diff3\";\nimport { GitEdgeError, GitMergeConflictError } from \"./errors.js\";\nimport { createSemaphore, type Semaphore } from \"./semaphore.js\";\n\nexport type { Repo };\n\n/**\n * How many object reads/writes a merge may have in flight at once. Each\n * `readBlob` materialises a whole blob, so an unbounded fan-out over a wide\n * directory holds as many object-store round trips (and bytes) open as the\n * directory has entries — enough to exhaust a serverless function's sockets\n * and trip storage rate limits. One shared pool keeps that flat regardless of\n * tree width or recursion depth, while staying wide enough to overlap enough\n * round trips to hide object-store latency.\n */\nexport const MAX_CONCURRENT_READS = 16;\n\n/** Options for threeWayMerge. */\nexport interface MergeOpts {\n\t/** Merge commit message (default: \"Merge <source> into <target>\"). */\n\tmessage?: string;\n\t/** Author name (default: \"Git Edge\"). */\n\tauthorName?: string;\n\t/** Author email (default: \"git-edge@local\"). */\n\tauthorEmail?: string;\n}\n\n/** Result of a successful merge (or of a no-op: target already up to date). */\nexport interface MergeResult {\n\tcommitOid: string;\n}\n\ninterface Entry {\n\tmode: string;\n\toid: string;\n\ttype: \"blob\" | \"tree\" | \"commit\";\n}\n\nconst isTree = (entry: Entry | undefined): entry is Entry =>\n\tentry?.type === \"tree\";\nconst isFile = (entry: Entry | undefined): entry is Entry =>\n\tentry?.type === \"blob\";\n\n/** Same tree entry: both absent, or identical oid and mode. */\nfunction same(a: Entry | undefined, b: Entry | undefined): boolean {\n\tif (a === undefined || b === undefined) return a === b;\n\treturn a.oid === b.oid && a.mode === b.mode;\n}\n\nasync function readEntries(\n\trepo: Repo,\n\treads: Semaphore,\n\ttreeOid: string | undefined,\n): Promise<Map<string, Entry>> {\n\tif (treeOid === undefined) return new Map();\n\tconst { tree } = await reads.run(() =>\n\t\tgit.readTree({ ...repo, oid: treeOid }),\n\t);\n\treturn new Map(tree.map((entry) => [entry.path, entry]));\n}\n\nasync function readBlob(\n\trepo: Repo,\n\treads: Semaphore,\n\toid: string,\n): Promise<Uint8Array> {\n\tconst { blob } = await reads.run(() => git.readBlob({ ...repo, oid }));\n\treturn blob;\n}\n\n/**\n * Line-level 3-way merge, via node-diff3's LCS-based diff3 algorithm — the\n * same approach GNU diffutils' `diff3` (and git's own default merge driver)\n * use. `conflict` reports whether any hunk couldn't be merged.\n */\nfunction mergeText(\n\tbase: Uint8Array,\n\tours: Uint8Array,\n\ttheirs: Uint8Array,\n): { content: Uint8Array; conflict: boolean } {\n\tconst { conflict, result } = diff3Merge(\n\t\tdecodeLines(ours),\n\t\tdecodeLines(base),\n\t\tdecodeLines(theirs),\n\t\t{ label: { a: \"ours\", b: \"theirs\" } },\n\t);\n\treturn { content: new TextEncoder().encode(result.join(\"\\n\")), conflict };\n}\n\nfunction decodeLines(data: Uint8Array): string[] {\n\tconst text = new TextDecoder().decode(data);\n\treturn text === \"\" ? [] : text.split(\"\\n\");\n}\n\n/** Git's own heuristic: a NUL byte means binary, which is never line-merged. */\nconst isBinary = (data: Uint8Array) => data.includes(0);\n\n/**\n * The merged mode when both sides changed a file: a mode only one side\n * changed wins; both changing it differently is a conflict (null).\n */\nfunction mergeMode(\n\tbase: Entry | undefined,\n\tours: Entry,\n\ttheirs: Entry,\n): string | null {\n\tif (ours.mode === theirs.mode) return ours.mode;\n\tif (base?.mode === ours.mode) return theirs.mode;\n\tif (base?.mode === theirs.mode) return ours.mode;\n\treturn null;\n}\n\n/**\n * Merge one path whose two sides both changed it differently. Returns the\n * merged entry, or null for a conflict (pushed onto `conflicts`).\n */\nasync function mergeChangedEntry(\n\trepo: Repo,\n\treads: Semaphore,\n\tpath: string,\n\tbase: Entry | undefined,\n\tours: Entry | undefined,\n\ttheirs: Entry | undefined,\n\tconflicts: string[],\n): Promise<Entry | null> {\n\tif (isTree(ours) && isTree(theirs)) {\n\t\tconst oid = await mergeTrees(\n\t\t\trepo,\n\t\t\treads,\n\t\t\tpath,\n\t\t\tisTree(base) ? base.oid : undefined,\n\t\t\tours.oid,\n\t\t\ttheirs.oid,\n\t\t\tconflicts,\n\t\t);\n\t\treturn oid === null ? null : { mode: \"040000\", oid, type: \"tree\" };\n\t}\n\t// Anything else — modify/delete, file/directory, submodule changes — is a\n\t// conflict unless both sides are regular files with mergeable content.\n\tif (!isFile(ours) || !isFile(theirs) || ours.mode === \"120000\") {\n\t\tconflicts.push(path);\n\t\treturn null;\n\t}\n\tconst mode = mergeMode(base, ours, theirs);\n\tif (mode === null) {\n\t\tconflicts.push(path);\n\t\treturn null;\n\t}\n\tif (ours.oid === theirs.oid) return { mode, oid: ours.oid, type: \"blob\" };\n\tconst [baseContent, oursContent, theirsContent] = await Promise.all([\n\t\tisFile(base) ? readBlob(repo, reads, base.oid) : new Uint8Array(),\n\t\treadBlob(repo, reads, ours.oid),\n\t\treadBlob(repo, reads, theirs.oid),\n\t]);\n\tif (\n\t\tisBinary(baseContent) ||\n\t\tisBinary(oursContent) ||\n\t\tisBinary(theirsContent)\n\t) {\n\t\tconflicts.push(path);\n\t\treturn null;\n\t}\n\tconst { content, conflict } = mergeText(\n\t\tbaseContent,\n\t\toursContent,\n\t\ttheirsContent,\n\t);\n\tif (conflict) {\n\t\tconflicts.push(path);\n\t\treturn null;\n\t}\n\tconst oid = await reads.run(() => git.writeBlob({ ...repo, blob: content }));\n\treturn { mode, oid, type: \"blob\" };\n}\n\n/**\n * Three-way merge of trees, recursing only into subtrees both sides changed\n * — an untouched or one-sided subtree is taken by oid without being read.\n * Returns the merged tree's oid, or null when any path conflicted.\n */\nasync function mergeTrees(\n\trepo: Repo,\n\treads: Semaphore,\n\tprefix: string,\n\tbaseOid: string | undefined,\n\toursOid: string,\n\ttheirsOid: string,\n\tconflicts: string[],\n): Promise<string | null> {\n\tconst [base, ours, theirs] = await Promise.all([\n\t\treadEntries(repo, reads, baseOid),\n\t\treadEntries(repo, reads, oursOid),\n\t\treadEntries(repo, reads, theirsOid),\n\t]);\n\tconst names = new Set([...base.keys(), ...ours.keys(), ...theirs.keys()]);\n\tconst merged: Array<Entry & { path: string }> = [];\n\tconst before = conflicts.length;\n\n\tawait Promise.all(\n\t\t[...names].map(async (name) => {\n\t\t\tconst path = prefix ? `${prefix}/${name}` : name;\n\t\t\tconst b = base.get(name);\n\t\t\tconst o = ours.get(name);\n\t\t\tconst t = theirs.get(name);\n\t\t\tlet result: Entry | undefined | null;\n\t\t\tif (same(o, t)) result = o;\n\t\t\telse if (same(b, o)) result = t;\n\t\t\telse if (same(b, t)) result = o;\n\t\t\telse\n\t\t\t\tresult = await mergeChangedEntry(repo, reads, path, b, o, t, conflicts);\n\t\t\tif (result) merged.push({ ...result, path: name });\n\t\t}),\n\t);\n\n\tif (conflicts.length > before) return null;\n\treturn reads.run(() => git.writeTree({ ...repo, tree: merged }));\n}\n\n/**\n * Merge `sourceRef` into `targetRef`, entirely at the object level.\n *\n * - Target already contains source (same commit, or source is an ancestor):\n * nothing to do — returns the target's commit, refs untouched.\n * - Source contains target: fast-forward the target ref to source.\n * - Otherwise: a real three-way merge against the merge base. Paths only one\n * side changed take that side (deletions included); paths both changed are\n * merged — subtrees recursively, text files line by line (diff3), modes\n * kept unless both sides changed them differently. Binary files, symlinks,\n * submodules, modify/delete and file/directory clashes that both sides\n * touched are conflicts. Any conflict throws `GitMergeConflictError`\n * without writing anything.\n *\n * New objects are stored as one pack, written before the target ref moves.\n * Serialize concurrent writers to the same repository externally.\n */\nexport async function threeWayMerge(\n\trepo: Repo,\n\tsourceRef: string,\n\ttargetRef: string,\n\topts?: MergeOpts,\n): Promise<MergeResult> {\n\tconst [sourceOid, targetOid] = await Promise.all([\n\t\tgit.resolveRef({ ...repo, ref: sourceRef }),\n\t\tgit.resolveRef({ ...repo, ref: targetRef }),\n\t]);\n\n\tif (\n\t\tsourceOid === targetOid ||\n\t\t(await git.isDescendent({ ...repo, oid: targetOid, ancestor: sourceOid }))\n\t) {\n\t\treturn { commitOid: targetOid };\n\t}\n\n\tif (\n\t\tawait git.isDescendent({ ...repo, oid: sourceOid, ancestor: targetOid })\n\t) {\n\t\tawait git.writeRef({\n\t\t\t...repo,\n\t\t\tref: targetRef,\n\t\t\tvalue: sourceOid,\n\t\t\tforce: true,\n\t\t});\n\t\treturn { commitOid: sourceOid };\n\t}\n\n\tconst baseOids = await git.findMergeBase({\n\t\t...repo,\n\t\toids: [sourceOid, targetOid],\n\t});\n\tconst baseOid = baseOids?.[0];\n\tif (baseOid === undefined) {\n\t\tthrow new GitEdgeError(\n\t\t\t`Refusing to merge unrelated histories (${sourceRef} and ${targetRef} share no commit)`,\n\t\t);\n\t}\n\n\tconst [baseCommit, sourceCommit, targetCommit] = await Promise.all([\n\t\tgit.readCommit({ ...repo, oid: baseOid }),\n\t\tgit.readCommit({ ...repo, oid: sourceOid }),\n\t\tgit.readCommit({ ...repo, oid: targetOid }),\n\t]);\n\n\t// One pool per merge: concurrent merges in the same process must not share\n\t// (or leak) each other's limit.\n\tconst reads = createSemaphore(MAX_CONCURRENT_READS);\n\treturn withPackedWrites(repo, async (packed) => {\n\t\tconst conflicts: string[] = [];\n\t\tconst treeOid = await mergeTrees(\n\t\t\tpacked,\n\t\t\treads,\n\t\t\t\"\",\n\t\t\tbaseCommit.commit.tree,\n\t\t\ttargetCommit.commit.tree,\n\t\t\tsourceCommit.commit.tree,\n\t\t\tconflicts,\n\t\t);\n\t\tif (treeOid === null) {\n\t\t\tthrow new GitMergeConflictError(conflicts.sort());\n\t\t}\n\n\t\tconst author = {\n\t\t\tname: opts?.authorName || \"Git Edge\",\n\t\t\temail: opts?.authorEmail || \"git-edge@local\",\n\t\t\ttimestamp: Math.floor(Date.now() / 1000),\n\t\t\ttimezoneOffset: 0,\n\t\t};\n\t\t// writeCommit, not git.commit: git.commit also advances whatever\n\t\t// branch HEAD names, which may not be the target.\n\t\tconst commitOid = await git.writeCommit({\n\t\t\t...packed,\n\t\t\tcommit: {\n\t\t\t\tmessage: opts?.message || `Merge ${sourceRef} into ${targetRef}`,\n\t\t\t\ttree: treeOid,\n\t\t\t\tparent: [targetOid, sourceOid],\n\t\t\t\tauthor,\n\t\t\t\tcommitter: author,\n\t\t\t},\n\t\t});\n\t\tawait git.writeRef({\n\t\t\t...packed,\n\t\t\tref: targetRef,\n\t\t\tvalue: commitOid,\n\t\t\tforce: true,\n\t\t});\n\t\treturn { commitOid };\n\t});\n}\n","/**\n * A tiny FIFO semaphore for bounding concurrent async I/O.\n *\n * Recursive callers (a tree merge recursing into subtrees) can't bound\n * in-flight I/O with a per-level limit — the level limit multiplies by depth.\n * A single shared instance caps the *total* instead.\n *\n * Edge-compatible: no node: imports, no timers, no polling.\n */\n\nexport interface Semaphore {\n\t/**\n\t * Run `fn` once a permit is free, releasing it when `fn` settles.\n\t *\n\t * The permit must not be held across work that itself needs one — with\n\t * every permit held by callers blocked on new permits, the pool deadlocks.\n\t * Wrap the individual I/O call, not a recursive subtree merge.\n\t */\n\trun<T>(fn: () => Promise<T>): Promise<T>;\n}\n\n/**\n * Create a semaphore that runs at most `limit` callbacks at once.\n *\n * `limit` must be a positive integer: a fractional or non-finite bound has no\n * useful meaning as a concurrency count, and would silently behave like an\n * unbounded pool (comparisons against it are never satisfied as intended).\n */\nexport function createSemaphore(limit: number): Semaphore {\n\tif (!Number.isInteger(limit) || limit < 1) {\n\t\tthrow new RangeError(\n\t\t\t`Semaphore limit must be a positive integer, got ${limit}`,\n\t\t);\n\t}\n\tlet active = 0;\n\tconst waiting: Array<() => void> = [];\n\n\tfunction acquire(): Promise<void> {\n\t\tif (active < limit) {\n\t\t\tactive += 1;\n\t\t\treturn Promise.resolve();\n\t\t}\n\t\treturn new Promise((resolve) => waiting.push(resolve));\n\t}\n\n\tfunction release(): void {\n\t\t// Hand the permit straight to the next waiter rather than dropping and\n\t\t// re-acquiring it: decrementing first would let a fresh caller jump the\n\t\t// queue and starve whoever has been waiting longest.\n\t\tconst next = waiting.shift();\n\t\tif (next) next();\n\t\telse active -= 1;\n\t}\n\n\treturn {\n\t\tasync run<T>(fn: () => Promise<T>): Promise<T> {\n\t\t\tawait acquire();\n\t\t\ttry {\n\t\t\t\treturn await fn();\n\t\t\t} finally {\n\t\t\t\trelease();\n\t\t\t}\n\t\t},\n\t};\n}\n","/**\n * isomorphic-git per-repo parse cache management.\n *\n * No node: imports.\n */\n\n// ---------------------------------------------------------------------------\n// isomorphic-git cache management\n// ---------------------------------------------------------------------------\n\n/**\n * Per-repo isomorphic-git packfile cache.\n *\n * isomorphic-git re-parses a packfile index from scratch on every\n * readTree/log/readObject call unless callers share a `cache` object\n * across calls. Without this, operations that touch many objects\n * (e.g. walking commit history) pay that parse cost hundreds of times.\n * Objects are content-addressed/immutable so a long-lived per-repo\n * cache is safe.\n */\n// Each cache retains every packfile it has read, in full. Bound the number of\n// repos held so a long-lived process serving many repos doesn't grow without\n// limit; least-recently-used repos are dropped first (Map keeps insertion\n// order, and a hit re-inserts its key at the end).\nconst MAX_REPO_CACHES = 32;\nconst repoCaches = new Map<string, object>();\n\nexport function getRepoCache(ownerKey: string, repoName: string): object {\n\tconst key = `${ownerKey}/${repoName}`;\n\tlet cache = repoCaches.get(key);\n\tif (cache) {\n\t\trepoCaches.delete(key);\n\t} else {\n\t\tcache = {};\n\t\tif (repoCaches.size >= MAX_REPO_CACHES) {\n\t\t\tconst oldest = repoCaches.keys().next().value;\n\t\t\tif (oldest !== undefined) repoCaches.delete(oldest);\n\t\t}\n\t}\n\trepoCaches.set(key, cache);\n\treturn cache;\n}\n\nexport function invalidateRepoCache(ownerKey: string, repoName: string): void {\n\trepoCaches.delete(`${ownerKey}/${repoName}`);\n}\n"],"mappings":"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","names":["oid"]}
|
package/dist/types/merge.d.ts
CHANGED
|
@@ -9,6 +9,16 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { type Repo } from "git-fs-s3/ops";
|
|
11
11
|
export type { Repo };
|
|
12
|
+
/**
|
|
13
|
+
* How many object reads/writes a merge may have in flight at once. Each
|
|
14
|
+
* `readBlob` materialises a whole blob, so an unbounded fan-out over a wide
|
|
15
|
+
* directory holds as many object-store round trips (and bytes) open as the
|
|
16
|
+
* directory has entries — enough to exhaust a serverless function's sockets
|
|
17
|
+
* and trip storage rate limits. One shared pool keeps that flat regardless of
|
|
18
|
+
* tree width or recursion depth, while staying wide enough to overlap enough
|
|
19
|
+
* round trips to hide object-store latency.
|
|
20
|
+
*/
|
|
21
|
+
export declare const MAX_CONCURRENT_READS = 16;
|
|
12
22
|
/** Options for threeWayMerge. */
|
|
13
23
|
export interface MergeOpts {
|
|
14
24
|
/** Merge commit message (default: "Merge <source> into <target>"). */
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A tiny FIFO semaphore for bounding concurrent async I/O.
|
|
3
|
+
*
|
|
4
|
+
* Recursive callers (a tree merge recursing into subtrees) can't bound
|
|
5
|
+
* in-flight I/O with a per-level limit — the level limit multiplies by depth.
|
|
6
|
+
* A single shared instance caps the *total* instead.
|
|
7
|
+
*
|
|
8
|
+
* Edge-compatible: no node: imports, no timers, no polling.
|
|
9
|
+
*/
|
|
10
|
+
export interface Semaphore {
|
|
11
|
+
/**
|
|
12
|
+
* Run `fn` once a permit is free, releasing it when `fn` settles.
|
|
13
|
+
*
|
|
14
|
+
* The permit must not be held across work that itself needs one — with
|
|
15
|
+
* every permit held by callers blocked on new permits, the pool deadlocks.
|
|
16
|
+
* Wrap the individual I/O call, not a recursive subtree merge.
|
|
17
|
+
*/
|
|
18
|
+
run<T>(fn: () => Promise<T>): Promise<T>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a semaphore that runs at most `limit` callbacks at once.
|
|
22
|
+
*
|
|
23
|
+
* `limit` must be a positive integer: a fractional or non-finite bound has no
|
|
24
|
+
* useful meaning as a concurrency count, and would silently behave like an
|
|
25
|
+
* unbounded pool (comparisons against it are never satisfied as intended).
|
|
26
|
+
*/
|
|
27
|
+
export declare function createSemaphore(limit: number): Semaphore;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-edge",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "High-level edge-compatible git operations
|
|
3
|
+
"version": "0.4.2",
|
|
4
|
+
"description": "High-level edge-compatible git operations \u2014 parsed-object cache, object-level three-way merge, and repo management utilities. Composes with git-fs-s3.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|