gitx.do 0.0.3 → 0.1.1

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.
Files changed (231) hide show
  1. package/README.md +319 -92
  2. package/dist/cli/commands/add.d.ts +176 -0
  3. package/dist/cli/commands/add.d.ts.map +1 -0
  4. package/dist/cli/commands/add.js +979 -0
  5. package/dist/cli/commands/add.js.map +1 -0
  6. package/dist/cli/commands/blame.d.ts +1 -1
  7. package/dist/cli/commands/blame.d.ts.map +1 -1
  8. package/dist/cli/commands/blame.js +1 -1
  9. package/dist/cli/commands/blame.js.map +1 -1
  10. package/dist/cli/commands/branch.d.ts +1 -1
  11. package/dist/cli/commands/branch.d.ts.map +1 -1
  12. package/dist/cli/commands/branch.js +2 -2
  13. package/dist/cli/commands/branch.js.map +1 -1
  14. package/dist/cli/commands/checkout.d.ts +73 -0
  15. package/dist/cli/commands/checkout.d.ts.map +1 -0
  16. package/dist/cli/commands/checkout.js +725 -0
  17. package/dist/cli/commands/checkout.js.map +1 -0
  18. package/dist/cli/commands/commit.d.ts.map +1 -1
  19. package/dist/cli/commands/commit.js +22 -2
  20. package/dist/cli/commands/commit.js.map +1 -1
  21. package/dist/cli/commands/diff.d.ts +4 -4
  22. package/dist/cli/commands/diff.d.ts.map +1 -1
  23. package/dist/cli/commands/diff.js +9 -8
  24. package/dist/cli/commands/diff.js.map +1 -1
  25. package/dist/cli/commands/log.d.ts +1 -1
  26. package/dist/cli/commands/log.d.ts.map +1 -1
  27. package/dist/cli/commands/log.js +1 -1
  28. package/dist/cli/commands/log.js.map +1 -1
  29. package/dist/cli/commands/merge.d.ts +106 -0
  30. package/dist/cli/commands/merge.d.ts.map +1 -0
  31. package/dist/cli/commands/merge.js +852 -0
  32. package/dist/cli/commands/merge.js.map +1 -0
  33. package/dist/cli/commands/review.d.ts +1 -1
  34. package/dist/cli/commands/review.d.ts.map +1 -1
  35. package/dist/cli/commands/review.js +26 -1
  36. package/dist/cli/commands/review.js.map +1 -1
  37. package/dist/cli/commands/stash.d.ts +157 -0
  38. package/dist/cli/commands/stash.d.ts.map +1 -0
  39. package/dist/cli/commands/stash.js +655 -0
  40. package/dist/cli/commands/stash.js.map +1 -0
  41. package/dist/cli/commands/status.d.ts.map +1 -1
  42. package/dist/cli/commands/status.js +1 -2
  43. package/dist/cli/commands/status.js.map +1 -1
  44. package/dist/cli/commands/web.d.ts.map +1 -1
  45. package/dist/cli/commands/web.js +3 -2
  46. package/dist/cli/commands/web.js.map +1 -1
  47. package/dist/cli/fs-adapter.d.ts.map +1 -1
  48. package/dist/cli/fs-adapter.js +3 -5
  49. package/dist/cli/fs-adapter.js.map +1 -1
  50. package/dist/cli/fsx-cli-adapter.d.ts +359 -0
  51. package/dist/cli/fsx-cli-adapter.d.ts.map +1 -0
  52. package/dist/cli/fsx-cli-adapter.js +619 -0
  53. package/dist/cli/fsx-cli-adapter.js.map +1 -0
  54. package/dist/cli/index.d.ts.map +1 -1
  55. package/dist/cli/index.js +68 -12
  56. package/dist/cli/index.js.map +1 -1
  57. package/dist/cli/ui/components/DiffView.d.ts +7 -2
  58. package/dist/cli/ui/components/DiffView.d.ts.map +1 -1
  59. package/dist/cli/ui/components/DiffView.js.map +1 -1
  60. package/dist/cli/ui/components/ErrorDisplay.d.ts +6 -2
  61. package/dist/cli/ui/components/ErrorDisplay.d.ts.map +1 -1
  62. package/dist/cli/ui/components/ErrorDisplay.js.map +1 -1
  63. package/dist/cli/ui/components/FuzzySearch.d.ts +8 -2
  64. package/dist/cli/ui/components/FuzzySearch.d.ts.map +1 -1
  65. package/dist/cli/ui/components/FuzzySearch.js.map +1 -1
  66. package/dist/cli/ui/components/LoadingSpinner.d.ts +6 -2
  67. package/dist/cli/ui/components/LoadingSpinner.d.ts.map +1 -1
  68. package/dist/cli/ui/components/LoadingSpinner.js.map +1 -1
  69. package/dist/cli/ui/components/NavigationList.d.ts +7 -2
  70. package/dist/cli/ui/components/NavigationList.d.ts.map +1 -1
  71. package/dist/cli/ui/components/NavigationList.js.map +1 -1
  72. package/dist/cli/ui/components/ScrollableContent.d.ts +7 -2
  73. package/dist/cli/ui/components/ScrollableContent.d.ts.map +1 -1
  74. package/dist/cli/ui/components/ScrollableContent.js.map +1 -1
  75. package/dist/cli/ui/terminal-ui.d.ts +42 -9
  76. package/dist/cli/ui/terminal-ui.d.ts.map +1 -1
  77. package/dist/cli/ui/terminal-ui.js.map +1 -1
  78. package/dist/do/BashModule.d.ts +871 -0
  79. package/dist/do/BashModule.d.ts.map +1 -0
  80. package/dist/do/BashModule.js +1143 -0
  81. package/dist/do/BashModule.js.map +1 -0
  82. package/dist/do/FsModule.d.ts +612 -0
  83. package/dist/do/FsModule.d.ts.map +1 -0
  84. package/dist/do/FsModule.js +1120 -0
  85. package/dist/do/FsModule.js.map +1 -0
  86. package/dist/do/GitModule.d.ts +635 -0
  87. package/dist/do/GitModule.d.ts.map +1 -0
  88. package/dist/do/GitModule.js +784 -0
  89. package/dist/do/GitModule.js.map +1 -0
  90. package/dist/do/GitRepoDO.d.ts +281 -0
  91. package/dist/do/GitRepoDO.d.ts.map +1 -0
  92. package/dist/do/GitRepoDO.js +479 -0
  93. package/dist/do/GitRepoDO.js.map +1 -0
  94. package/dist/do/bash-ast.d.ts +246 -0
  95. package/dist/do/bash-ast.d.ts.map +1 -0
  96. package/dist/do/bash-ast.js +888 -0
  97. package/dist/do/bash-ast.js.map +1 -0
  98. package/dist/do/container-executor.d.ts +491 -0
  99. package/dist/do/container-executor.d.ts.map +1 -0
  100. package/dist/do/container-executor.js +731 -0
  101. package/dist/do/container-executor.js.map +1 -0
  102. package/dist/do/index.d.ts +53 -0
  103. package/dist/do/index.d.ts.map +1 -0
  104. package/dist/do/index.js +91 -0
  105. package/dist/do/index.js.map +1 -0
  106. package/dist/do/tiered-storage.d.ts +403 -0
  107. package/dist/do/tiered-storage.d.ts.map +1 -0
  108. package/dist/do/tiered-storage.js +689 -0
  109. package/dist/do/tiered-storage.js.map +1 -0
  110. package/dist/do/withBash.d.ts +231 -0
  111. package/dist/do/withBash.d.ts.map +1 -0
  112. package/dist/do/withBash.js +244 -0
  113. package/dist/do/withBash.js.map +1 -0
  114. package/dist/do/withFs.d.ts +237 -0
  115. package/dist/do/withFs.d.ts.map +1 -0
  116. package/dist/do/withFs.js +387 -0
  117. package/dist/do/withFs.js.map +1 -0
  118. package/dist/do/withGit.d.ts +180 -0
  119. package/dist/do/withGit.d.ts.map +1 -0
  120. package/dist/do/withGit.js +271 -0
  121. package/dist/do/withGit.js.map +1 -0
  122. package/dist/durable-object/object-store.d.ts +157 -15
  123. package/dist/durable-object/object-store.d.ts.map +1 -1
  124. package/dist/durable-object/object-store.js +435 -47
  125. package/dist/durable-object/object-store.js.map +1 -1
  126. package/dist/durable-object/schema.d.ts +12 -1
  127. package/dist/durable-object/schema.d.ts.map +1 -1
  128. package/dist/durable-object/schema.js +87 -2
  129. package/dist/durable-object/schema.js.map +1 -1
  130. package/dist/index.d.ts +84 -1
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +34 -0
  133. package/dist/index.js.map +1 -1
  134. package/dist/mcp/sandbox/miniflare-evaluator.d.ts +22 -0
  135. package/dist/mcp/sandbox/miniflare-evaluator.d.ts.map +1 -0
  136. package/dist/mcp/sandbox/miniflare-evaluator.js +140 -0
  137. package/dist/mcp/sandbox/miniflare-evaluator.js.map +1 -0
  138. package/dist/mcp/sandbox/object-store-proxy.d.ts +32 -0
  139. package/dist/mcp/sandbox/object-store-proxy.d.ts.map +1 -0
  140. package/dist/mcp/sandbox/object-store-proxy.js +30 -0
  141. package/dist/mcp/sandbox/object-store-proxy.js.map +1 -0
  142. package/dist/mcp/sandbox/template.d.ts +17 -0
  143. package/dist/mcp/sandbox/template.d.ts.map +1 -0
  144. package/dist/mcp/sandbox/template.js +71 -0
  145. package/dist/mcp/sandbox/template.js.map +1 -0
  146. package/dist/mcp/sandbox.d.ts.map +1 -1
  147. package/dist/mcp/sandbox.js +16 -4
  148. package/dist/mcp/sandbox.js.map +1 -1
  149. package/dist/mcp/tools/do.d.ts +32 -0
  150. package/dist/mcp/tools/do.d.ts.map +1 -0
  151. package/dist/mcp/tools/do.js +117 -0
  152. package/dist/mcp/tools/do.js.map +1 -0
  153. package/dist/mcp/tools.d.ts.map +1 -1
  154. package/dist/mcp/tools.js +1258 -22
  155. package/dist/mcp/tools.js.map +1 -1
  156. package/dist/pack/delta.d.ts +8 -0
  157. package/dist/pack/delta.d.ts.map +1 -1
  158. package/dist/pack/delta.js +241 -30
  159. package/dist/pack/delta.js.map +1 -1
  160. package/dist/refs/branch.d.ts +38 -25
  161. package/dist/refs/branch.d.ts.map +1 -1
  162. package/dist/refs/branch.js +421 -94
  163. package/dist/refs/branch.js.map +1 -1
  164. package/dist/refs/storage.d.ts +77 -5
  165. package/dist/refs/storage.d.ts.map +1 -1
  166. package/dist/refs/storage.js +193 -43
  167. package/dist/refs/storage.js.map +1 -1
  168. package/dist/refs/tag.d.ts +44 -24
  169. package/dist/refs/tag.d.ts.map +1 -1
  170. package/dist/refs/tag.js +411 -70
  171. package/dist/refs/tag.js.map +1 -1
  172. package/dist/storage/backend.d.ts +425 -0
  173. package/dist/storage/backend.d.ts.map +1 -0
  174. package/dist/storage/backend.js +41 -0
  175. package/dist/storage/backend.js.map +1 -0
  176. package/dist/storage/fsx-adapter.d.ts +204 -0
  177. package/dist/storage/fsx-adapter.d.ts.map +1 -0
  178. package/dist/storage/fsx-adapter.js +518 -0
  179. package/dist/storage/fsx-adapter.js.map +1 -0
  180. package/dist/storage/r2-pack.d.ts.map +1 -1
  181. package/dist/storage/r2-pack.js +4 -1
  182. package/dist/storage/r2-pack.js.map +1 -1
  183. package/dist/tiered/cdc-pipeline.js +3 -3
  184. package/dist/tiered/cdc-pipeline.js.map +1 -1
  185. package/dist/tiered/migration.d.ts.map +1 -1
  186. package/dist/tiered/migration.js +4 -1
  187. package/dist/tiered/migration.js.map +1 -1
  188. package/dist/types/capability.d.ts +1385 -0
  189. package/dist/types/capability.d.ts.map +1 -0
  190. package/dist/types/capability.js +36 -0
  191. package/dist/types/capability.js.map +1 -0
  192. package/dist/types/index.d.ts +13 -0
  193. package/dist/types/index.d.ts.map +1 -0
  194. package/dist/types/index.js +18 -0
  195. package/dist/types/index.js.map +1 -0
  196. package/dist/types/interfaces.d.ts +673 -0
  197. package/dist/types/interfaces.d.ts.map +1 -0
  198. package/dist/types/interfaces.js +26 -0
  199. package/dist/types/interfaces.js.map +1 -0
  200. package/dist/types/objects.d.ts +182 -0
  201. package/dist/types/objects.d.ts.map +1 -1
  202. package/dist/types/objects.js +249 -4
  203. package/dist/types/objects.js.map +1 -1
  204. package/dist/types/storage.d.ts +114 -0
  205. package/dist/types/storage.d.ts.map +1 -1
  206. package/dist/types/storage.js +160 -1
  207. package/dist/types/storage.js.map +1 -1
  208. package/dist/types/worker-loader.d.ts +60 -0
  209. package/dist/types/worker-loader.d.ts.map +1 -0
  210. package/dist/types/worker-loader.js +62 -0
  211. package/dist/types/worker-loader.js.map +1 -0
  212. package/dist/utils/hash.d.ts +126 -80
  213. package/dist/utils/hash.d.ts.map +1 -1
  214. package/dist/utils/hash.js +191 -100
  215. package/dist/utils/hash.js.map +1 -1
  216. package/dist/utils/sha1.d.ts +206 -0
  217. package/dist/utils/sha1.d.ts.map +1 -1
  218. package/dist/utils/sha1.js +405 -0
  219. package/dist/utils/sha1.js.map +1 -1
  220. package/dist/wire/path-security.d.ts +157 -0
  221. package/dist/wire/path-security.d.ts.map +1 -0
  222. package/dist/wire/path-security.js +307 -0
  223. package/dist/wire/path-security.js.map +1 -0
  224. package/dist/wire/receive-pack.d.ts +7 -0
  225. package/dist/wire/receive-pack.d.ts.map +1 -1
  226. package/dist/wire/receive-pack.js +29 -1
  227. package/dist/wire/receive-pack.js.map +1 -1
  228. package/dist/wire/upload-pack.d.ts.map +1 -1
  229. package/dist/wire/upload-pack.js +4 -1
  230. package/dist/wire/upload-pack.js.map +1 -1
  231. package/package.json +10 -1
@@ -9,7 +9,9 @@
9
9
  * - Content-addressable storage using SHA-1 hashes
10
10
  * - Write-ahead logging (WAL) for durability
11
11
  * - Object index for tiered storage support
12
- * - Batch operations for efficiency
12
+ * - Batch operations for efficiency with transaction support
13
+ * - LRU caching for hot tier objects
14
+ * - Metrics and logging infrastructure
13
15
  * - Typed accessors for each Git object type
14
16
  *
15
17
  * @module durable-object/object-store
@@ -18,23 +20,39 @@
18
20
  * ```typescript
19
21
  * import { ObjectStore } from './durable-object/object-store'
20
22
  *
21
- * const store = new ObjectStore(durableObjectStorage)
23
+ * const store = new ObjectStore(durableObjectStorage, {
24
+ * cacheMaxCount: 1000,
25
+ * cacheMaxBytes: 50 * 1024 * 1024, // 50MB
26
+ * enableMetrics: true
27
+ * })
22
28
  *
23
29
  * // Store a blob
24
30
  * const content = new TextEncoder().encode('Hello, World!')
25
31
  * const sha = await store.putObject('blob', content)
26
32
  *
27
- * // Retrieve it
33
+ * // Retrieve it (cached on second access)
28
34
  * const obj = await store.getObject(sha)
29
35
  * console.log(obj?.type, obj?.size)
30
36
  *
31
37
  * // Get typed object
32
38
  * const blob = await store.getBlobObject(sha)
39
+ *
40
+ * // Get metrics
41
+ * const metrics = store.getMetrics()
42
+ * console.log(`Cache hit rate: ${metrics.cacheHitRate}%`)
33
43
  * ```
34
44
  */
45
+ import { LRUCache } from '../storage/lru-cache';
46
+ import { isValidMode, isValidSha } from '../types/objects';
47
+ // Reserved for future validation
48
+ import { validateTreeEntry as _validateTreeEntry } from '../types/objects';
49
+ void _validateTreeEntry;
35
50
  import { hashObject } from '../utils/hash';
36
51
  const encoder = new TextEncoder();
37
52
  const decoder = new TextDecoder();
53
+ // Default cache configuration
54
+ const DEFAULT_CACHE_MAX_COUNT = 500;
55
+ const DEFAULT_CACHE_MAX_BYTES = 25 * 1024 * 1024; // 25MB
38
56
  // ============================================================================
39
57
  // ObjectStore Class
40
58
  // ============================================================================
@@ -66,13 +84,72 @@ const decoder = new TextDecoder();
66
84
  */
67
85
  export class ObjectStore {
68
86
  storage;
87
+ cache;
88
+ options;
89
+ logger;
90
+ backend;
91
+ // Metrics tracking
92
+ _reads = 0;
93
+ _writes = 0;
94
+ _deletes = 0;
95
+ _bytesWritten = 0;
96
+ _bytesRead = 0;
97
+ _totalWriteLatency = 0;
98
+ _totalReadLatency = 0;
99
+ _batchOperations = 0;
100
+ _batchObjectsTotal = 0;
69
101
  /**
70
102
  * Create a new ObjectStore.
71
103
  *
72
104
  * @param storage - Durable Object storage interface with SQL support
105
+ * @param options - Configuration options for caching, metrics, logging, and backend
106
+ *
107
+ * @example
108
+ * ```typescript
109
+ * // Basic usage (SQLite backend)
110
+ * const store = new ObjectStore(storage)
111
+ *
112
+ * // With caching and metrics
113
+ * const store = new ObjectStore(storage, {
114
+ * cacheMaxCount: 1000,
115
+ * cacheMaxBytes: 50 * 1024 * 1024,
116
+ * enableMetrics: true,
117
+ * logger: console
118
+ * })
119
+ *
120
+ * // With StorageBackend abstraction
121
+ * const store = new ObjectStore(storage, {
122
+ * backend: fsBackend
123
+ * })
124
+ * ```
73
125
  */
74
- constructor(storage) {
126
+ constructor(storage, options) {
75
127
  this.storage = storage;
128
+ this.options = options ?? {};
129
+ this.logger = options?.logger;
130
+ this.backend = options?.backend ?? null;
131
+ // Initialize LRU cache for hot tier objects
132
+ this.cache = new LRUCache({
133
+ maxCount: options?.cacheMaxCount ?? DEFAULT_CACHE_MAX_COUNT,
134
+ maxBytes: options?.cacheMaxBytes ?? DEFAULT_CACHE_MAX_BYTES,
135
+ defaultTTL: options?.cacheTTL,
136
+ sizeCalculator: (obj) => obj.data.byteLength + 100, // 100 bytes overhead for metadata
137
+ onEvict: (key, _value, reason) => {
138
+ this.log('debug', `Cache eviction: ${key} (reason: ${reason})`);
139
+ }
140
+ });
141
+ }
142
+ /**
143
+ * Log a message if logger is configured.
144
+ * @internal
145
+ */
146
+ log(level, message, ...args) {
147
+ if (!this.logger)
148
+ return;
149
+ const logFn = this.logger[level];
150
+ if (logFn) {
151
+ logFn.call(this.logger, `[ObjectStore] ${message}`, ...args);
152
+ }
76
153
  }
77
154
  /**
78
155
  * Store a raw object and return its SHA.
@@ -81,6 +158,7 @@ export class ObjectStore {
81
158
  * Computes the SHA-1 hash of the object in Git format (type + size + content),
82
159
  * logs the operation to WAL, stores the object, and updates the object index.
83
160
  * If an object with the same SHA already exists, it is replaced (idempotent).
161
+ * The object is also added to the LRU cache for fast subsequent reads.
84
162
  *
85
163
  * @param type - Object type ('blob', 'tree', 'commit', 'tag')
86
164
  * @param data - Raw object content (without Git header)
@@ -94,14 +172,55 @@ export class ObjectStore {
94
172
  * ```
95
173
  */
96
174
  async putObject(type, data) {
175
+ const startTime = this.options.enableMetrics ? Date.now() : 0;
176
+ // Delegate to backend if available
177
+ if (this.backend) {
178
+ const sha = await this.backend.putObject(type, data);
179
+ // Add to cache for fast subsequent reads
180
+ const storedObject = {
181
+ sha,
182
+ type,
183
+ size: data.length,
184
+ data,
185
+ createdAt: Date.now()
186
+ };
187
+ this.cache.set(sha, storedObject);
188
+ // Update metrics
189
+ if (this.options.enableMetrics) {
190
+ this._writes++;
191
+ this._bytesWritten += data.length;
192
+ this._totalWriteLatency += Date.now() - startTime;
193
+ }
194
+ return sha;
195
+ }
196
+ // Existing SQLite implementation as fallback
97
197
  // Compute SHA-1 hash using git object format: "type size\0content"
98
198
  const sha = await hashObject(type, data);
199
+ this.log('debug', `Storing ${type} object: ${sha} (${data.length} bytes)`);
99
200
  // Log to WAL first
100
201
  await this.logToWAL('PUT', sha, type, data);
202
+ const now = Date.now();
101
203
  // Store the object
102
- this.storage.sql.exec('INSERT OR REPLACE INTO objects (sha, type, size, data, created_at) VALUES (?, ?, ?, ?, ?)', sha, type, data.length, data, Date.now());
204
+ this.storage.sql.exec('INSERT OR REPLACE INTO objects (sha, type, size, data, created_at) VALUES (?, ?, ?, ?, ?)', sha, type, data.length, data, now);
103
205
  // Update object index
104
- this.storage.sql.exec('INSERT OR REPLACE INTO object_index (sha, tier, location, size, type) VALUES (?, ?, ?, ?, ?)', sha, 'hot', 'local', data.length, type);
206
+ this.storage.sql.exec('INSERT OR REPLACE INTO object_index (sha, tier, pack_id, offset, size, type, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)', sha, 'hot', null, // pack_id is null for hot tier
207
+ null, // offset is null for hot tier
208
+ data.length, type, now);
209
+ // Add to cache for fast subsequent reads
210
+ const storedObject = {
211
+ sha,
212
+ type,
213
+ size: data.length,
214
+ data,
215
+ createdAt: now
216
+ };
217
+ this.cache.set(sha, storedObject);
218
+ // Update metrics
219
+ if (this.options.enableMetrics) {
220
+ this._writes++;
221
+ this._bytesWritten += data.length;
222
+ this._totalWriteLatency += Date.now() - startTime;
223
+ }
105
224
  return sha;
106
225
  }
107
226
  /**
@@ -124,11 +243,44 @@ export class ObjectStore {
124
243
  * ```
125
244
  */
126
245
  async putTreeObject(entries) {
127
- // Sort entries by name (directories get trailing / for sorting)
246
+ // Validate all entries first
247
+ const seenNames = new Set();
248
+ for (const entry of entries) {
249
+ // Check for invalid names: empty, '.', '..', contains '/' or null byte
250
+ if (!entry.name || entry.name === '.' || entry.name === '..') {
251
+ throw new Error(`Invalid entry name: "${entry.name}". Entry names cannot be empty, ".", or ".."`);
252
+ }
253
+ if (entry.name.includes('/')) {
254
+ throw new Error(`Invalid entry name: "${entry.name}". Entry names cannot contain path separators`);
255
+ }
256
+ if (entry.name.includes('\0')) {
257
+ throw new Error(`Invalid entry name: "${entry.name}". Entry names cannot contain null bytes`);
258
+ }
259
+ // Check for duplicate names
260
+ if (seenNames.has(entry.name)) {
261
+ throw new Error(`Duplicate entry name: "${entry.name}". Tree entries must have unique names`);
262
+ }
263
+ seenNames.add(entry.name);
264
+ // Validate mode
265
+ if (!isValidMode(entry.mode)) {
266
+ throw new Error(`Invalid mode: "${entry.mode}". Valid modes: 100644, 100755, 040000, 120000, 160000`);
267
+ }
268
+ // Validate SHA
269
+ if (!isValidSha(entry.sha)) {
270
+ throw new Error(`Invalid SHA: "${entry.sha}". Must be 40 lowercase hex characters`);
271
+ }
272
+ }
273
+ // Sort entries by name using ASCII byte-order comparison
274
+ // Git sorts directories as if they have trailing slashes for comparison
128
275
  const sortedEntries = [...entries].sort((a, b) => {
129
276
  const aName = a.mode === '040000' ? a.name + '/' : a.name;
130
277
  const bName = b.mode === '040000' ? b.name + '/' : b.name;
131
- return aName.localeCompare(bName);
278
+ // Use simple comparison for ASCII byte order
279
+ if (aName < bName)
280
+ return -1;
281
+ if (aName > bName)
282
+ return 1;
283
+ return 0;
132
284
  });
133
285
  // Build tree content (without header)
134
286
  const entryParts = [];
@@ -230,7 +382,9 @@ export class ObjectStore {
230
382
  lines.push(`object ${tag.object}`);
231
383
  lines.push(`type ${tag.objectType}`);
232
384
  lines.push(`tag ${tag.name}`);
233
- lines.push(`tagger ${tag.tagger.name} <${tag.tagger.email}> ${tag.tagger.timestamp} ${tag.tagger.timezone}`);
385
+ if (tag.tagger) {
386
+ lines.push(`tagger ${tag.tagger.name} <${tag.tagger.email}> ${tag.tagger.timestamp} ${tag.tagger.timezone}`);
387
+ }
234
388
  lines.push('');
235
389
  lines.push(tag.message);
236
390
  const content = encoder.encode(lines.join('\n'));
@@ -240,8 +394,8 @@ export class ObjectStore {
240
394
  * Retrieve an object by SHA.
241
395
  *
242
396
  * @description
243
- * Fetches an object from the database by its SHA-1 hash.
244
- * Returns null if the object doesn't exist or if the SHA is invalid.
397
+ * Fetches an object from the LRU cache first, falling back to the database
398
+ * if not cached. Returns null if the object doesn't exist or if the SHA is invalid.
245
399
  *
246
400
  * @param sha - 40-character SHA-1 hash
247
401
  * @returns The stored object or null if not found
@@ -255,21 +409,75 @@ export class ObjectStore {
255
409
  * ```
256
410
  */
257
411
  async getObject(sha) {
412
+ const startTime = this.options.enableMetrics ? Date.now() : 0;
258
413
  if (!sha || sha.length < 4) {
259
414
  return null;
260
415
  }
416
+ // Check cache first (fast path)
417
+ const cached = this.cache.get(sha);
418
+ if (cached) {
419
+ this.log('debug', `Cache hit for object: ${sha}`);
420
+ if (this.options.enableMetrics) {
421
+ this._reads++;
422
+ this._bytesRead += cached.size;
423
+ this._totalReadLatency += Date.now() - startTime;
424
+ }
425
+ return cached;
426
+ }
427
+ // Delegate to backend if available
428
+ if (this.backend) {
429
+ const result = await this.backend.getObject(sha);
430
+ if (!result) {
431
+ this.log('debug', `Object not found: ${sha}`);
432
+ if (this.options.enableMetrics) {
433
+ this._reads++;
434
+ this._totalReadLatency += Date.now() - startTime;
435
+ }
436
+ return null;
437
+ }
438
+ const obj = {
439
+ sha,
440
+ type: result.type,
441
+ size: result.content.length,
442
+ data: result.content,
443
+ createdAt: Date.now()
444
+ };
445
+ // Add to cache for subsequent reads
446
+ this.cache.set(sha, obj);
447
+ if (this.options.enableMetrics) {
448
+ this._reads++;
449
+ this._bytesRead += obj.size;
450
+ this._totalReadLatency += Date.now() - startTime;
451
+ }
452
+ return obj;
453
+ }
454
+ // Existing SQLite implementation as fallback
455
+ // Fall back to database
261
456
  const result = this.storage.sql.exec('SELECT sha, type, size, data, created_at as createdAt FROM objects WHERE sha = ?', sha);
262
457
  const rows = result.toArray();
263
458
  if (rows.length === 0) {
459
+ this.log('debug', `Object not found: ${sha}`);
460
+ if (this.options.enableMetrics) {
461
+ this._reads++;
462
+ this._totalReadLatency += Date.now() - startTime;
463
+ }
264
464
  return null;
265
465
  }
266
- return rows[0];
466
+ const obj = rows[0];
467
+ // Add to cache for subsequent reads
468
+ this.cache.set(sha, obj);
469
+ if (this.options.enableMetrics) {
470
+ this._reads++;
471
+ this._bytesRead += obj.size;
472
+ this._totalReadLatency += Date.now() - startTime;
473
+ }
474
+ return obj;
267
475
  }
268
476
  /**
269
477
  * Delete an object by SHA.
270
478
  *
271
479
  * @description
272
- * Removes an object from both the objects table and the object index.
480
+ * Removes an object from the cache, objects table, and the object index.
273
481
  * The operation is logged to WAL. Returns false if the object doesn't exist.
274
482
  *
275
483
  * **Warning**: Deleting objects that are still referenced by other objects
@@ -287,17 +495,42 @@ export class ObjectStore {
287
495
  * ```
288
496
  */
289
497
  async deleteObject(sha) {
498
+ // Delegate to backend if available
499
+ if (this.backend) {
500
+ // Check if object exists first via backend
501
+ const exists = await this.backend.hasObject(sha);
502
+ if (!exists) {
503
+ return false;
504
+ }
505
+ this.log('debug', `Deleting object via backend: ${sha}`);
506
+ await this.backend.deleteObject(sha);
507
+ // Remove from cache
508
+ this.cache.delete(sha);
509
+ // Update metrics
510
+ if (this.options.enableMetrics) {
511
+ this._deletes++;
512
+ }
513
+ return true;
514
+ }
515
+ // Existing SQLite implementation as fallback
290
516
  // Check if object exists first
291
517
  const exists = await this.hasObject(sha);
292
518
  if (!exists) {
293
519
  return false;
294
520
  }
521
+ this.log('debug', `Deleting object: ${sha}`);
295
522
  // Log to WAL
296
523
  await this.logToWAL('DELETE', sha, 'blob', new Uint8Array(0));
297
524
  // Delete from objects table
298
525
  this.storage.sql.exec('DELETE FROM objects WHERE sha = ?', sha);
299
526
  // Delete from object index
300
527
  this.storage.sql.exec('DELETE FROM object_index WHERE sha = ?', sha);
528
+ // Remove from cache
529
+ this.cache.delete(sha);
530
+ // Update metrics
531
+ if (this.options.enableMetrics) {
532
+ this._deletes++;
533
+ }
301
534
  return true;
302
535
  }
303
536
  /**
@@ -320,6 +553,15 @@ export class ObjectStore {
320
553
  if (!sha || sha.length < 4) {
321
554
  return false;
322
555
  }
556
+ // Check cache first (fast path)
557
+ if (this.cache.has(sha)) {
558
+ return true;
559
+ }
560
+ // Delegate to backend if available
561
+ if (this.backend) {
562
+ return this.backend.hasObject(sha);
563
+ }
564
+ // Existing SQLite implementation as fallback
323
565
  // Use getObject and check for null - this works better with the mock
324
566
  const obj = await this.getObject(sha);
325
567
  return obj !== null;
@@ -345,11 +587,14 @@ export class ObjectStore {
345
587
  * ```
346
588
  */
347
589
  async verifyObject(sha) {
348
- const obj = await this.getObject(sha);
349
- if (!obj) {
590
+ // Read directly from storage (bypass cache) to verify actual stored data
591
+ const result = this.storage.sql.exec('SELECT type, data FROM objects WHERE sha = ?', sha);
592
+ const rows = result.toArray();
593
+ if (rows.length === 0) {
350
594
  return false;
351
595
  }
352
- const computedSha = await hashObject(obj.type, obj.data);
596
+ const obj = rows[0];
597
+ const computedSha = await hashObject(obj.type, new Uint8Array(obj.data));
353
598
  return computedSha === sha;
354
599
  }
355
600
  /**
@@ -393,12 +638,14 @@ export class ObjectStore {
393
638
  return obj?.size ?? null;
394
639
  }
395
640
  /**
396
- * Store multiple objects in a batch.
641
+ * Store multiple objects in a batch using a single transaction.
397
642
  *
398
643
  * @description
399
- * Stores multiple objects sequentially. Each object is stored
400
- * individually with its own WAL entry. For atomic batch operations,
401
- * consider wrapping in a transaction.
644
+ * Stores multiple objects atomically within a single SQLite transaction.
645
+ * This is more efficient than individual puts for bulk operations as it:
646
+ * - Reduces the number of disk flushes
647
+ * - Ensures atomic writes (all-or-nothing)
648
+ * - Batches WAL entries for better performance
402
649
  *
403
650
  * @param objects - Array of objects to store
404
651
  * @returns Array of SHA-1 hashes in the same order as input
@@ -412,19 +659,83 @@ export class ObjectStore {
412
659
  * ```
413
660
  */
414
661
  async putObjects(objects) {
662
+ if (objects.length === 0) {
663
+ return [];
664
+ }
665
+ // For single objects, delegate to putObject
666
+ if (objects.length === 1) {
667
+ const sha = await this.putObject(objects[0].type, objects[0].data);
668
+ return [sha];
669
+ }
670
+ const startTime = this.options.enableMetrics ? Date.now() : 0;
415
671
  const shas = [];
672
+ const now = Date.now();
673
+ let totalBytes = 0;
674
+ this.log('info', `Starting batch write of ${objects.length} objects`);
675
+ // Pre-compute all SHA hashes (CPU-bound, before transaction)
676
+ const objectsWithSha = [];
416
677
  for (const obj of objects) {
417
- const sha = await this.putObject(obj.type, obj.data);
678
+ const sha = await hashObject(obj.type, obj.data);
679
+ objectsWithSha.push({ sha, type: obj.type, data: obj.data });
418
680
  shas.push(sha);
681
+ totalBytes += obj.data.length;
682
+ }
683
+ // Begin transaction for atomic batch write
684
+ this.storage.sql.exec('BEGIN TRANSACTION');
685
+ try {
686
+ for (const { sha, type, data } of objectsWithSha) {
687
+ // Log batch operation to WAL (single entry for the batch)
688
+ const payload = encoder.encode(JSON.stringify({
689
+ sha,
690
+ type,
691
+ timestamp: now,
692
+ batchSize: objects.length
693
+ }));
694
+ this.storage.sql.exec('INSERT INTO wal (operation, payload, created_at, flushed) VALUES (?, ?, ?, 0)', 'BATCH_PUT', payload, now);
695
+ // Store the object
696
+ this.storage.sql.exec('INSERT OR REPLACE INTO objects (sha, type, size, data, created_at) VALUES (?, ?, ?, ?, ?)', sha, type, data.length, data, now);
697
+ // Update object index
698
+ this.storage.sql.exec('INSERT OR REPLACE INTO object_index (sha, tier, pack_id, offset, size, type, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)', sha, 'hot', null, // pack_id is null for hot tier
699
+ null, // offset is null for hot tier
700
+ data.length, type, now);
701
+ // Add to cache
702
+ const storedObject = {
703
+ sha,
704
+ type,
705
+ size: data.length,
706
+ data,
707
+ createdAt: now
708
+ };
709
+ this.cache.set(sha, storedObject);
710
+ }
711
+ // Commit transaction
712
+ this.storage.sql.exec('COMMIT');
713
+ this.log('info', `Batch write completed: ${objects.length} objects, ${totalBytes} bytes`);
714
+ // Update metrics
715
+ if (this.options.enableMetrics) {
716
+ this._writes += objects.length;
717
+ this._bytesWritten += totalBytes;
718
+ this._totalWriteLatency += Date.now() - startTime;
719
+ this._batchOperations++;
720
+ this._batchObjectsTotal += objects.length;
721
+ }
722
+ return shas;
723
+ }
724
+ catch (error) {
725
+ // Rollback on error
726
+ this.storage.sql.exec('ROLLBACK');
727
+ this.log('error', `Batch write failed, rolled back`, error);
728
+ throw error;
419
729
  }
420
- return shas;
421
730
  }
422
731
  /**
423
- * Retrieve multiple objects by SHA.
732
+ * Retrieve multiple objects by SHA using optimized batch queries.
424
733
  *
425
734
  * @description
426
- * Fetches multiple objects by their SHAs. Missing objects
427
- * are returned as null in the result array.
735
+ * Fetches multiple objects efficiently by:
736
+ * 1. First checking the LRU cache for each SHA
737
+ * 2. Batching uncached SHAs into a single SQL query with IN clause
738
+ * 3. Returning results in the original order with null for missing objects
428
739
  *
429
740
  * @param shas - Array of 40-character SHA-1 hashes
430
741
  * @returns Array of objects (or null for missing) in the same order
@@ -440,10 +751,58 @@ export class ObjectStore {
440
751
  * ```
441
752
  */
442
753
  async getObjects(shas) {
443
- const results = [];
444
- for (const sha of shas) {
445
- const obj = await this.getObject(sha);
446
- results.push(obj);
754
+ if (shas.length === 0) {
755
+ return [];
756
+ }
757
+ const startTime = this.options.enableMetrics ? Date.now() : 0;
758
+ const results = new Array(shas.length).fill(null);
759
+ const uncachedIndices = [];
760
+ const uncachedShas = [];
761
+ let totalBytesRead = 0;
762
+ // First pass: check cache for each SHA
763
+ for (let i = 0; i < shas.length; i++) {
764
+ const sha = shas[i];
765
+ if (!sha || sha.length < 4) {
766
+ results[i] = null;
767
+ continue;
768
+ }
769
+ const cached = this.cache.get(sha);
770
+ if (cached) {
771
+ results[i] = cached;
772
+ totalBytesRead += cached.size;
773
+ }
774
+ else {
775
+ uncachedIndices.push(i);
776
+ uncachedShas.push(sha);
777
+ }
778
+ }
779
+ // Second pass: batch query for uncached objects
780
+ if (uncachedShas.length > 0) {
781
+ this.log('debug', `Batch fetching ${uncachedShas.length} uncached objects`);
782
+ // Build optimized IN query
783
+ const placeholders = uncachedShas.map(() => '?').join(', ');
784
+ const result = this.storage.sql.exec(`SELECT sha, type, size, data, created_at as createdAt FROM objects WHERE sha IN (${placeholders})`, ...uncachedShas);
785
+ const rows = result.toArray();
786
+ // Build lookup map for O(1) access
787
+ const rowMap = new Map();
788
+ for (const row of rows) {
789
+ rowMap.set(row.sha, row);
790
+ // Add to cache for future reads
791
+ this.cache.set(row.sha, row);
792
+ totalBytesRead += row.size;
793
+ }
794
+ // Fill in results at original indices
795
+ for (let i = 0; i < uncachedIndices.length; i++) {
796
+ const originalIndex = uncachedIndices[i];
797
+ const sha = uncachedShas[i];
798
+ results[originalIndex] = rowMap.get(sha) ?? null;
799
+ }
800
+ }
801
+ // Update metrics
802
+ if (this.options.enableMetrics) {
803
+ this._reads += shas.length;
804
+ this._bytesRead += totalBytesRead;
805
+ this._totalReadLatency += Date.now() - startTime;
447
806
  }
448
807
  return results;
449
808
  }
@@ -505,21 +864,42 @@ export class ObjectStore {
505
864
  const entries = [];
506
865
  let offset = 0;
507
866
  const data = obj.data;
508
- while (offset < data.length) {
509
- // Find the null byte after mode+name
510
- let nullIndex = offset;
511
- while (nullIndex < data.length && data[nullIndex] !== 0) {
512
- nullIndex++;
513
- }
514
- const modeNameStr = decoder.decode(data.slice(offset, nullIndex));
515
- const spaceIndex = modeNameStr.indexOf(' ');
516
- const mode = modeNameStr.slice(0, spaceIndex);
517
- const name = modeNameStr.slice(spaceIndex + 1);
518
- // Read 20-byte SHA
519
- const sha20 = data.slice(nullIndex + 1, nullIndex + 21);
520
- const entrySha = bytesToHex(sha20);
521
- entries.push({ mode, name, sha: entrySha });
522
- offset = nullIndex + 21;
867
+ try {
868
+ while (offset < data.length) {
869
+ // Find the null byte after mode+name
870
+ let nullIndex = offset;
871
+ while (nullIndex < data.length && data[nullIndex] !== 0) {
872
+ nullIndex++;
873
+ }
874
+ // Check if we found a null byte
875
+ if (nullIndex >= data.length) {
876
+ // No null byte found - malformed data, return empty entries
877
+ return { type: 'tree', data: obj.data, entries: [] };
878
+ }
879
+ const modeNameStr = decoder.decode(data.slice(offset, nullIndex));
880
+ const spaceIndex = modeNameStr.indexOf(' ');
881
+ // Check for valid mode+name format
882
+ if (spaceIndex === -1) {
883
+ // No space found - malformed entry, return empty entries
884
+ return { type: 'tree', data: obj.data, entries: [] };
885
+ }
886
+ const mode = modeNameStr.slice(0, spaceIndex);
887
+ const name = modeNameStr.slice(spaceIndex + 1);
888
+ // Check if we have enough bytes for the 20-byte SHA
889
+ if (nullIndex + 21 > data.length) {
890
+ // Not enough bytes for SHA - return what we have parsed so far as malformed
891
+ return { type: 'tree', data: obj.data, entries: [] };
892
+ }
893
+ // Read 20-byte SHA
894
+ const sha20 = data.slice(nullIndex + 1, nullIndex + 21);
895
+ const entrySha = bytesToHex(sha20);
896
+ entries.push({ mode, name, sha: entrySha });
897
+ offset = nullIndex + 21;
898
+ }
899
+ }
900
+ catch {
901
+ // Any parsing error - return null or empty entries
902
+ return { type: 'tree', data: obj.data, entries: [] };
523
903
  }
524
904
  return {
525
905
  type: 'tree',
@@ -622,7 +1002,7 @@ export class ObjectStore {
622
1002
  let object = '';
623
1003
  let objectType = 'commit';
624
1004
  let name = '';
625
- let tagger = null;
1005
+ let tagger = undefined;
626
1006
  let messageStartIndex = 0;
627
1007
  for (let i = 0; i < lines.length; i++) {
628
1008
  const line = lines[i];
@@ -640,10 +1020,18 @@ export class ObjectStore {
640
1020
  name = line.slice(4);
641
1021
  }
642
1022
  else if (line.startsWith('tagger ')) {
643
- tagger = parseAuthorLine(line);
1023
+ try {
1024
+ tagger = parseAuthorLine(line);
1025
+ }
1026
+ catch {
1027
+ // Malformed tagger line - leave tagger as undefined
1028
+ return null;
1029
+ }
644
1030
  }
645
1031
  }
646
- if (!tagger) {
1032
+ // Validate required fields - object and name must be present
1033
+ // tagger is optional (some older tags or special tags may not have it)
1034
+ if (!object || !name) {
647
1035
  return null;
648
1036
  }
649
1037
  const message = lines.slice(messageStartIndex).join('\n');