@git-stunts/git-warp 10.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 (143) hide show
  1. package/LICENSE +201 -0
  2. package/NOTICE +16 -0
  3. package/README.md +480 -0
  4. package/SECURITY.md +30 -0
  5. package/bin/git-warp +24 -0
  6. package/bin/warp-graph.js +1574 -0
  7. package/index.d.ts +2366 -0
  8. package/index.js +180 -0
  9. package/package.json +129 -0
  10. package/scripts/install-git-warp.sh +258 -0
  11. package/scripts/uninstall-git-warp.sh +139 -0
  12. package/src/domain/WarpGraph.js +3157 -0
  13. package/src/domain/crdt/Dot.js +160 -0
  14. package/src/domain/crdt/LWW.js +154 -0
  15. package/src/domain/crdt/ORSet.js +371 -0
  16. package/src/domain/crdt/VersionVector.js +222 -0
  17. package/src/domain/entities/GraphNode.js +60 -0
  18. package/src/domain/errors/EmptyMessageError.js +47 -0
  19. package/src/domain/errors/ForkError.js +30 -0
  20. package/src/domain/errors/IndexError.js +23 -0
  21. package/src/domain/errors/OperationAbortedError.js +22 -0
  22. package/src/domain/errors/QueryError.js +39 -0
  23. package/src/domain/errors/SchemaUnsupportedError.js +17 -0
  24. package/src/domain/errors/ShardCorruptionError.js +56 -0
  25. package/src/domain/errors/ShardLoadError.js +57 -0
  26. package/src/domain/errors/ShardValidationError.js +61 -0
  27. package/src/domain/errors/StorageError.js +57 -0
  28. package/src/domain/errors/SyncError.js +30 -0
  29. package/src/domain/errors/TraversalError.js +23 -0
  30. package/src/domain/errors/WarpError.js +31 -0
  31. package/src/domain/errors/WormholeError.js +28 -0
  32. package/src/domain/errors/WriterError.js +39 -0
  33. package/src/domain/errors/index.js +21 -0
  34. package/src/domain/services/AnchorMessageCodec.js +99 -0
  35. package/src/domain/services/BitmapIndexBuilder.js +225 -0
  36. package/src/domain/services/BitmapIndexReader.js +435 -0
  37. package/src/domain/services/BoundaryTransitionRecord.js +463 -0
  38. package/src/domain/services/CheckpointMessageCodec.js +147 -0
  39. package/src/domain/services/CheckpointSerializerV5.js +281 -0
  40. package/src/domain/services/CheckpointService.js +384 -0
  41. package/src/domain/services/CommitDagTraversalService.js +156 -0
  42. package/src/domain/services/DagPathFinding.js +712 -0
  43. package/src/domain/services/DagTopology.js +239 -0
  44. package/src/domain/services/DagTraversal.js +245 -0
  45. package/src/domain/services/Frontier.js +108 -0
  46. package/src/domain/services/GCMetrics.js +101 -0
  47. package/src/domain/services/GCPolicy.js +122 -0
  48. package/src/domain/services/GitLogParser.js +205 -0
  49. package/src/domain/services/HealthCheckService.js +246 -0
  50. package/src/domain/services/HookInstaller.js +326 -0
  51. package/src/domain/services/HttpSyncServer.js +262 -0
  52. package/src/domain/services/IndexRebuildService.js +426 -0
  53. package/src/domain/services/IndexStalenessChecker.js +103 -0
  54. package/src/domain/services/JoinReducer.js +582 -0
  55. package/src/domain/services/KeyCodec.js +113 -0
  56. package/src/domain/services/LegacyAnchorDetector.js +67 -0
  57. package/src/domain/services/LogicalTraversal.js +351 -0
  58. package/src/domain/services/MessageCodecInternal.js +132 -0
  59. package/src/domain/services/MessageSchemaDetector.js +145 -0
  60. package/src/domain/services/MigrationService.js +55 -0
  61. package/src/domain/services/ObserverView.js +265 -0
  62. package/src/domain/services/PatchBuilderV2.js +669 -0
  63. package/src/domain/services/PatchMessageCodec.js +140 -0
  64. package/src/domain/services/ProvenanceIndex.js +337 -0
  65. package/src/domain/services/ProvenancePayload.js +242 -0
  66. package/src/domain/services/QueryBuilder.js +835 -0
  67. package/src/domain/services/StateDiff.js +300 -0
  68. package/src/domain/services/StateSerializerV5.js +156 -0
  69. package/src/domain/services/StreamingBitmapIndexBuilder.js +709 -0
  70. package/src/domain/services/SyncProtocol.js +593 -0
  71. package/src/domain/services/TemporalQuery.js +201 -0
  72. package/src/domain/services/TranslationCost.js +221 -0
  73. package/src/domain/services/TraversalService.js +8 -0
  74. package/src/domain/services/WarpMessageCodec.js +29 -0
  75. package/src/domain/services/WarpStateIndexBuilder.js +127 -0
  76. package/src/domain/services/WormholeService.js +353 -0
  77. package/src/domain/types/TickReceipt.js +285 -0
  78. package/src/domain/types/WarpTypes.js +209 -0
  79. package/src/domain/types/WarpTypesV2.js +200 -0
  80. package/src/domain/utils/CachedValue.js +140 -0
  81. package/src/domain/utils/EventId.js +89 -0
  82. package/src/domain/utils/LRUCache.js +112 -0
  83. package/src/domain/utils/MinHeap.js +114 -0
  84. package/src/domain/utils/RefLayout.js +280 -0
  85. package/src/domain/utils/WriterId.js +205 -0
  86. package/src/domain/utils/cancellation.js +33 -0
  87. package/src/domain/utils/canonicalStringify.js +42 -0
  88. package/src/domain/utils/defaultClock.js +20 -0
  89. package/src/domain/utils/defaultCodec.js +51 -0
  90. package/src/domain/utils/nullLogger.js +21 -0
  91. package/src/domain/utils/roaring.js +181 -0
  92. package/src/domain/utils/shardVersion.js +9 -0
  93. package/src/domain/warp/PatchSession.js +217 -0
  94. package/src/domain/warp/Writer.js +181 -0
  95. package/src/hooks/post-merge.sh +60 -0
  96. package/src/infrastructure/adapters/BunHttpAdapter.js +225 -0
  97. package/src/infrastructure/adapters/ClockAdapter.js +57 -0
  98. package/src/infrastructure/adapters/ConsoleLogger.js +150 -0
  99. package/src/infrastructure/adapters/DenoHttpAdapter.js +230 -0
  100. package/src/infrastructure/adapters/GitGraphAdapter.js +787 -0
  101. package/src/infrastructure/adapters/GlobalClockAdapter.js +5 -0
  102. package/src/infrastructure/adapters/NoOpLogger.js +62 -0
  103. package/src/infrastructure/adapters/NodeCryptoAdapter.js +32 -0
  104. package/src/infrastructure/adapters/NodeHttpAdapter.js +98 -0
  105. package/src/infrastructure/adapters/PerformanceClockAdapter.js +5 -0
  106. package/src/infrastructure/adapters/WebCryptoAdapter.js +121 -0
  107. package/src/infrastructure/codecs/CborCodec.js +384 -0
  108. package/src/ports/BlobPort.js +30 -0
  109. package/src/ports/ClockPort.js +25 -0
  110. package/src/ports/CodecPort.js +25 -0
  111. package/src/ports/CommitPort.js +114 -0
  112. package/src/ports/ConfigPort.js +31 -0
  113. package/src/ports/CryptoPort.js +38 -0
  114. package/src/ports/GraphPersistencePort.js +57 -0
  115. package/src/ports/HttpServerPort.js +25 -0
  116. package/src/ports/IndexStoragePort.js +39 -0
  117. package/src/ports/LoggerPort.js +68 -0
  118. package/src/ports/RefPort.js +51 -0
  119. package/src/ports/TreePort.js +51 -0
  120. package/src/visualization/index.js +26 -0
  121. package/src/visualization/layouts/converters.js +75 -0
  122. package/src/visualization/layouts/elkAdapter.js +86 -0
  123. package/src/visualization/layouts/elkLayout.js +95 -0
  124. package/src/visualization/layouts/index.js +29 -0
  125. package/src/visualization/renderers/ascii/box.js +16 -0
  126. package/src/visualization/renderers/ascii/check.js +271 -0
  127. package/src/visualization/renderers/ascii/colors.js +13 -0
  128. package/src/visualization/renderers/ascii/formatters.js +73 -0
  129. package/src/visualization/renderers/ascii/graph.js +344 -0
  130. package/src/visualization/renderers/ascii/history.js +335 -0
  131. package/src/visualization/renderers/ascii/index.js +14 -0
  132. package/src/visualization/renderers/ascii/info.js +245 -0
  133. package/src/visualization/renderers/ascii/materialize.js +255 -0
  134. package/src/visualization/renderers/ascii/path.js +240 -0
  135. package/src/visualization/renderers/ascii/progress.js +32 -0
  136. package/src/visualization/renderers/ascii/symbols.js +33 -0
  137. package/src/visualization/renderers/ascii/table.js +19 -0
  138. package/src/visualization/renderers/browser/index.js +1 -0
  139. package/src/visualization/renderers/svg/index.js +159 -0
  140. package/src/visualization/utils/ansi.js +14 -0
  141. package/src/visualization/utils/time.js +40 -0
  142. package/src/visualization/utils/truncate.js +40 -0
  143. package/src/visualization/utils/unicode.js +52 -0
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Writer - WARP writer abstraction for safe graph mutations.
3
+ *
4
+ * A Writer is the only way to mutate a WarpGraph state. It owns a writerId
5
+ * and maintains a single-writer chain under refs/warp/<graph>/writers/<writerId>.
6
+ *
7
+ * Key guarantees:
8
+ * - Single-writer chain per writerId
9
+ * - Ref-safe identity
10
+ * - CAS-based updates to prevent concurrent forks
11
+ * - Schema:2 only (PatchV2 ops with OR-Set semantics)
12
+ *
13
+ * @module domain/warp/Writer
14
+ * @see WARP Writer Spec v1
15
+ */
16
+
17
+ import defaultCodec from '../utils/defaultCodec.js';
18
+ import { validateWriterId, buildWriterRef } from '../utils/RefLayout.js';
19
+ import { PatchSession } from './PatchSession.js';
20
+ import { PatchBuilderV2 } from '../services/PatchBuilderV2.js';
21
+ import { decodePatchMessage, detectMessageKind } from '../services/WarpMessageCodec.js';
22
+ import { vvClone } from '../crdt/VersionVector.js';
23
+ import WriterError from '../errors/WriterError.js';
24
+
25
+ // Re-export for backward compatibility — consumers importing from Writer.js
26
+ // should migrate to importing from '../errors/WriterError.js' directly.
27
+ export { WriterError };
28
+
29
+ /**
30
+ * Writer class for creating and committing patches to a WARP graph.
31
+ *
32
+ * @class Writer
33
+ */
34
+ export class Writer {
35
+ /**
36
+ * Creates a new Writer instance.
37
+ *
38
+ * @param {Object} options
39
+ * @param {import('../../ports/GraphPersistencePort.js').default} options.persistence - Git adapter
40
+ * @param {string} options.graphName - Graph namespace
41
+ * @param {string} options.writerId - This writer's ID
42
+ * @param {import('../crdt/VersionVector.js').VersionVector} options.versionVector - Current version vector
43
+ * @param {() => Promise<import('../services/JoinReducer.js').WarpStateV5>} options.getCurrentState - Async function returning the current materialized V5 state
44
+ * @param {(result: {patch: Object, sha: string}) => void | Promise<void>} [options.onCommitSuccess] - Callback invoked after successful commit with { patch, sha }
45
+ * @param {'reject'|'cascade'|'warn'} [options.onDeleteWithData='warn'] - Policy when deleting a node with attached data
46
+ * @param {import('../../ports/CodecPort.js').default} [options.codec] - Codec for CBOR serialization (defaults to domain-local codec)
47
+ */
48
+ constructor({ persistence, graphName, writerId, versionVector, getCurrentState, onCommitSuccess, onDeleteWithData = 'warn', codec }) {
49
+ validateWriterId(writerId);
50
+
51
+ /** @type {import('../../ports/GraphPersistencePort.js').default} */
52
+ this._persistence = persistence;
53
+
54
+ /** @type {string} */
55
+ this._graphName = graphName;
56
+
57
+ /** @type {string} */
58
+ this._writerId = writerId;
59
+
60
+ /** @type {import('../crdt/VersionVector.js').VersionVector} */
61
+ this._versionVector = versionVector;
62
+
63
+ /** @type {Function} */
64
+ this._getCurrentState = getCurrentState;
65
+
66
+ /** @type {Function|undefined} */
67
+ this._onCommitSuccess = onCommitSuccess;
68
+
69
+ /** @type {'reject'|'cascade'|'warn'} */
70
+ this._onDeleteWithData = onDeleteWithData;
71
+
72
+ /** @type {import('../../ports/CodecPort.js').default|undefined} */
73
+ this._codec = codec || defaultCodec;
74
+ }
75
+
76
+ /**
77
+ * Gets the writer ID.
78
+ * @returns {string}
79
+ */
80
+ get writerId() {
81
+ return this._writerId;
82
+ }
83
+
84
+ /**
85
+ * Gets the graph name.
86
+ * @returns {string}
87
+ */
88
+ get graphName() {
89
+ return this._graphName;
90
+ }
91
+
92
+ /**
93
+ * Gets the current writer head SHA.
94
+ *
95
+ * @returns {Promise<string|null>} The tip SHA or null if no commits yet
96
+ */
97
+ async head() {
98
+ const writerRef = buildWriterRef(this._graphName, this._writerId);
99
+ return await this._persistence.readRef(writerRef);
100
+ }
101
+
102
+ /**
103
+ * Begins a new patch session.
104
+ *
105
+ * Reads the current writer head and captures it as the expected parent
106
+ * for CAS-based commit.
107
+ *
108
+ * @returns {Promise<PatchSession>} A fluent patch session
109
+ *
110
+ * @example
111
+ * const writer = await graph.writer();
112
+ * const patch = await writer.beginPatch();
113
+ * patch.addNode('user:alice');
114
+ * patch.setProperty('user:alice', 'name', 'Alice');
115
+ * await patch.commit();
116
+ */
117
+ async beginPatch() {
118
+ // Read current writer head and capture for CAS
119
+ const writerRef = buildWriterRef(this._graphName, this._writerId);
120
+ const expectedOldHead = await this._persistence.readRef(writerRef);
121
+
122
+ // Calculate next lamport
123
+ let lamport = 1;
124
+ if (expectedOldHead) {
125
+ const commitMessage = await this._persistence.showNode(expectedOldHead);
126
+ const kind = detectMessageKind(commitMessage);
127
+ if (kind === 'patch') {
128
+ try {
129
+ const patchInfo = decodePatchMessage(commitMessage);
130
+ lamport = patchInfo.lamport + 1;
131
+ } catch {
132
+ // Malformed message, start at 1
133
+ }
134
+ }
135
+ }
136
+
137
+ // Create internal PatchBuilderV2
138
+ const builder = new PatchBuilderV2({
139
+ persistence: this._persistence,
140
+ graphName: this._graphName,
141
+ writerId: this._writerId,
142
+ lamport,
143
+ versionVector: vvClone(this._versionVector),
144
+ getCurrentState: this._getCurrentState,
145
+ expectedParentSha: expectedOldHead,
146
+ onCommitSuccess: this._onCommitSuccess,
147
+ onDeleteWithData: this._onDeleteWithData,
148
+ codec: this._codec,
149
+ });
150
+
151
+ // Return PatchSession wrapping the builder
152
+ return new PatchSession({
153
+ builder,
154
+ persistence: this._persistence,
155
+ graphName: this._graphName,
156
+ writerId: this._writerId,
157
+ expectedOldHead,
158
+ });
159
+ }
160
+
161
+ /**
162
+ * Convenience method to build and commit a patch in one call.
163
+ *
164
+ * @param {(p: PatchSession) => void | Promise<void>} build - Function to build the patch
165
+ * @returns {Promise<string>} The commit SHA of the new patch
166
+ * @throws {WriterError} EMPTY_PATCH if no operations were added
167
+ * @throws {WriterError} WRITER_REF_ADVANCED if CAS fails (ref moved since beginPatch)
168
+ * @throws {WriterError} PERSIST_WRITE_FAILED if git operations fail
169
+ *
170
+ * @example
171
+ * const sha = await writer.commitPatch(p => {
172
+ * p.addNode('user:alice');
173
+ * p.setProperty('user:alice', 'name', 'Alice');
174
+ * });
175
+ */
176
+ async commitPatch(build) {
177
+ const patch = await this.beginPatch();
178
+ await build(patch);
179
+ return await patch.commit();
180
+ }
181
+ }
@@ -0,0 +1,60 @@
1
+ #!/bin/sh
2
+ # --- @git-stunts/git-warp post-merge hook __WARP_HOOK_VERSION__ ---
3
+ # warp-hook-version: __WARP_HOOK_VERSION__
4
+ #
5
+ # Post-merge hook: notify (or auto-materialize) when warp refs changed.
6
+ #
7
+ # Compares refs/warp/ before and after merge by maintaining
8
+ # a snapshot file at .git/warp-ref-snapshot. If any warp writer refs
9
+ # changed and warp.autoMaterialize is true, runs `git warp materialize`.
10
+ # Otherwise prints an informational message advising re-materialization.
11
+ # Always exits 0 — never blocks a merge.
12
+
13
+ GIT_DIR=$(git rev-parse --git-dir 2>/dev/null) || exit 0
14
+ SNAPSHOT="${GIT_DIR}/warp-ref-snapshot"
15
+
16
+ # Capture current warp refs (sorted for stable comparison)
17
+ CURRENT=$(git for-each-ref --format='%(refname) %(objectname)' --sort=refname refs/warp/ 2>/dev/null) || true
18
+
19
+ if [ -z "$CURRENT" ]; then
20
+ # No warp refs exist — clean up any stale snapshot and exit
21
+ rm -f "$SNAPSHOT"
22
+ exit 0
23
+ fi
24
+
25
+ CHANGED=0
26
+
27
+ if [ -f "$SNAPSHOT" ]; then
28
+ PREVIOUS=$(cat "$SNAPSHOT")
29
+ if [ "$CURRENT" != "$PREVIOUS" ]; then
30
+ CHANGED=1
31
+ fi
32
+ else
33
+ # First encounter — refs exist but no snapshot yet
34
+ CHANGED=1
35
+ fi
36
+
37
+ # Save current state for next comparison
38
+ printf '%s\n' "$CURRENT" > "$SNAPSHOT"
39
+
40
+ if [ "$CHANGED" -eq 0 ]; then
41
+ exit 0
42
+ fi
43
+
44
+ AUTO_MAT=$(git config --bool warp.autoMaterialize 2>/dev/null) || true
45
+
46
+ if [ "$AUTO_MAT" = "true" ]; then
47
+ echo "[warp] Refs changed — auto-materializing..."
48
+ if command -v git-warp >/dev/null 2>&1; then
49
+ git-warp materialize || echo "[warp] Warning: auto-materialize failed."
50
+ elif command -v warp-graph >/dev/null 2>&1; then
51
+ warp-graph materialize || echo "[warp] Warning: auto-materialize failed."
52
+ else
53
+ echo "[warp] Warning: neither git-warp nor warp-graph found in PATH."
54
+ fi
55
+ else
56
+ echo "[warp] Writer refs changed during merge. Call materialize() to see updates."
57
+ fi
58
+
59
+ exit 0
60
+ # --- end @git-stunts/git-warp ---
@@ -0,0 +1,225 @@
1
+ import HttpServerPort from '../../ports/HttpServerPort.js';
2
+
3
+ const ERROR_BODY = 'Internal Server Error';
4
+ const ERROR_BODY_BYTES = new TextEncoder().encode(ERROR_BODY);
5
+ const ERROR_BODY_LENGTH = String(ERROR_BODY_BYTES.byteLength);
6
+
7
+ const PAYLOAD_TOO_LARGE = 'Payload Too Large';
8
+ const PAYLOAD_TOO_LARGE_LENGTH = String(new TextEncoder().encode(PAYLOAD_TOO_LARGE).byteLength);
9
+
10
+ /** Absolute streaming body limit (10 MB) — matches NodeHttpAdapter. */
11
+ const MAX_BODY_BYTES = 10 * 1024 * 1024;
12
+
13
+ /**
14
+ * Reads a ReadableStream body with a byte-count limit.
15
+ * Aborts immediately when the limit is exceeded, preventing full buffering.
16
+ *
17
+ * @param {ReadableStream} bodyStream
18
+ * @returns {Promise<Uint8Array|undefined>}
19
+ */
20
+ async function readStreamBody(bodyStream) {
21
+ const reader = bodyStream.getReader();
22
+ const chunks = [];
23
+ let total = 0;
24
+ for (;;) {
25
+ const { done, value } = await reader.read();
26
+ if (done) {
27
+ break;
28
+ }
29
+ total += value.byteLength;
30
+ if (total > MAX_BODY_BYTES) {
31
+ await reader.cancel();
32
+ throw Object.assign(new Error('Payload Too Large'), { status: 413 });
33
+ }
34
+ chunks.push(value);
35
+ }
36
+ if (total === 0) {
37
+ return undefined;
38
+ }
39
+ const body = new Uint8Array(total);
40
+ let offset = 0;
41
+ for (const chunk of chunks) {
42
+ body.set(chunk, offset);
43
+ offset += chunk.byteLength;
44
+ }
45
+ return body;
46
+ }
47
+
48
+ /**
49
+ * Converts a Bun Request into the plain-object format expected by
50
+ * HttpServerPort request handlers.
51
+ *
52
+ * @param {Request} request - Bun fetch Request
53
+ * @returns {Promise<{ method: string, url: string, headers: Object, body: Buffer|undefined }>}
54
+ */
55
+ async function toPortRequest(request) {
56
+ const headers = {};
57
+ request.headers.forEach((value, key) => {
58
+ headers[key] = value;
59
+ });
60
+
61
+ let body;
62
+ if (request.method !== 'GET' && request.method !== 'HEAD') {
63
+ const cl = headers['content-length'];
64
+ if (cl !== undefined && Number(cl) > MAX_BODY_BYTES) {
65
+ throw Object.assign(new Error('Payload Too Large'), { status: 413 });
66
+ }
67
+ if (request.body) {
68
+ body = await readStreamBody(request.body);
69
+ }
70
+ }
71
+
72
+ const parsedUrl = new URL(request.url);
73
+ return {
74
+ method: request.method,
75
+ url: parsedUrl.pathname + parsedUrl.search,
76
+ headers,
77
+ body,
78
+ };
79
+ }
80
+
81
+ /**
82
+ * Converts a plain-object port response into a Bun Response.
83
+ *
84
+ * @param {{ status?: number, headers?: Object, body?: string|Uint8Array }} portResponse
85
+ * @returns {Response}
86
+ */
87
+ function toResponse(portResponse) {
88
+ return new Response(portResponse.body ?? null, {
89
+ status: portResponse.status || 200,
90
+ headers: portResponse.headers || {},
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Creates the Bun fetch handler that bridges between Request/Response
96
+ * and the HttpServerPort plain-object contract.
97
+ *
98
+ * @param {Function} requestHandler - Port-style async handler
99
+ * @param {{ error: Function }} logger
100
+ * @returns {(request: Request) => Promise<Response>}
101
+ */
102
+ function createFetchHandler(requestHandler, logger) {
103
+ return async (request) => {
104
+ try {
105
+ const portReq = await toPortRequest(request);
106
+ const portRes = await requestHandler(portReq);
107
+ return toResponse(portRes);
108
+ } catch (err) {
109
+ if (err.status === 413) {
110
+ return new Response(PAYLOAD_TOO_LARGE, {
111
+ status: 413,
112
+ headers: { 'Content-Type': 'text/plain', 'Content-Length': PAYLOAD_TOO_LARGE_LENGTH },
113
+ });
114
+ }
115
+ logger.error('BunHttpAdapter dispatch error', err);
116
+ return new Response(ERROR_BODY, {
117
+ status: 500,
118
+ headers: {
119
+ 'Content-Type': 'text/plain',
120
+ 'Content-Length': ERROR_BODY_LENGTH,
121
+ },
122
+ });
123
+ }
124
+ };
125
+ }
126
+
127
+ /**
128
+ * Starts a Bun server and invokes the callback with (null) on success
129
+ * or (err) on failure.
130
+ *
131
+ * Note: Bun.serve() is synchronous, so cb fires on the same tick
132
+ * (unlike Node's server.listen which defers via the event loop).
133
+ *
134
+ * @param {{ port: number, hostname?: string, fetch: Function }} serveOptions
135
+ * @param {Function|undefined} cb - Node-style callback
136
+ * @returns {Object} The Bun server instance
137
+ */
138
+ function startServer(serveOptions, cb) {
139
+ const server = globalThis.Bun.serve(serveOptions);
140
+ if (cb) {
141
+ cb(null);
142
+ }
143
+ return server;
144
+ }
145
+
146
+ /**
147
+ * Safely stops a Bun server, forwarding errors to the callback.
148
+ *
149
+ * @param {{ server: Object|null }} state - Shared mutable state
150
+ * @param {Function} [callback]
151
+ */
152
+ function stopServer(state, callback) {
153
+ try {
154
+ if (state.server) {
155
+ state.server.stop();
156
+ state.server = null;
157
+ }
158
+ if (callback) {
159
+ callback();
160
+ }
161
+ } catch (err) {
162
+ if (callback) {
163
+ callback(err);
164
+ }
165
+ }
166
+ }
167
+
168
+ const noopLogger = { error() {} };
169
+
170
+ /**
171
+ * Bun HTTP adapter implementing HttpServerPort.
172
+ *
173
+ * Uses `globalThis.Bun.serve()` so the module can be imported on any
174
+ * runtime (it will throw at call-time if Bun is not available).
175
+ *
176
+ * @extends HttpServerPort
177
+ */
178
+ export default class BunHttpAdapter extends HttpServerPort {
179
+ /**
180
+ * @param {{ logger?: { error: Function } }} [options]
181
+ */
182
+ constructor({ logger } = {}) {
183
+ super();
184
+ this._logger = logger || noopLogger;
185
+ }
186
+
187
+ /** @inheritdoc */
188
+ createServer(requestHandler) {
189
+ const fetchHandler = createFetchHandler(requestHandler, this._logger);
190
+ const state = { server: null };
191
+
192
+ return {
193
+ listen(port, host, callback) {
194
+ const cb = typeof host === 'function' ? host : callback;
195
+ const bindHost = typeof host === 'string' ? host : undefined;
196
+ const serveOptions = { port, fetch: fetchHandler };
197
+
198
+ if (bindHost !== undefined) {
199
+ serveOptions.hostname = bindHost;
200
+ }
201
+
202
+ try {
203
+ state.server = startServer(serveOptions, cb);
204
+ } catch (err) {
205
+ if (cb) {
206
+ cb(err);
207
+ }
208
+ }
209
+ },
210
+
211
+ close: (callback) => stopServer(state, callback),
212
+
213
+ address() {
214
+ if (!state.server) {
215
+ return null;
216
+ }
217
+ return {
218
+ address: state.server.hostname,
219
+ port: state.server.port,
220
+ family: state.server.hostname.includes(':') ? 'IPv6' : 'IPv4',
221
+ };
222
+ },
223
+ };
224
+ }
225
+ }
@@ -0,0 +1,57 @@
1
+ import { performance as nodePerformance } from 'node:perf_hooks';
2
+ import ClockPort from '../../ports/ClockPort.js';
3
+
4
+ /**
5
+ * Unified clock adapter supporting both Node.js and global performance APIs.
6
+ *
7
+ * Accepts an optional `performanceImpl` in the constructor, defaulting to
8
+ * `globalThis.performance`. Use the static factory methods for common cases:
9
+ *
10
+ * - `ClockAdapter.node()` — Node.js `perf_hooks.performance`
11
+ * - `ClockAdapter.global()` — `globalThis.performance` (Bun/Deno/browsers)
12
+ *
13
+ * @extends ClockPort
14
+ */
15
+ export default class ClockAdapter extends ClockPort {
16
+ /**
17
+ * @param {object} [options]
18
+ * @param {Performance} [options.performanceImpl] - Performance API implementation.
19
+ * Defaults to `globalThis.performance`.
20
+ */
21
+ constructor({ performanceImpl } = {}) {
22
+ super();
23
+ this._performance = performanceImpl || globalThis.performance;
24
+ }
25
+
26
+ /**
27
+ * Creates a ClockAdapter using Node.js `perf_hooks.performance`.
28
+ * @returns {ClockAdapter}
29
+ */
30
+ static node() {
31
+ return new ClockAdapter({ performanceImpl: nodePerformance });
32
+ }
33
+
34
+ /**
35
+ * Creates a ClockAdapter using `globalThis.performance`.
36
+ * @returns {ClockAdapter}
37
+ */
38
+ static global() {
39
+ return new ClockAdapter({ performanceImpl: globalThis.performance });
40
+ }
41
+
42
+ /**
43
+ * Returns a high-resolution timestamp in milliseconds.
44
+ * @returns {number}
45
+ */
46
+ now() {
47
+ return this._performance.now();
48
+ }
49
+
50
+ /**
51
+ * Returns the current wall-clock time as an ISO string.
52
+ * @returns {string}
53
+ */
54
+ timestamp() {
55
+ return new Date().toISOString();
56
+ }
57
+ }
@@ -0,0 +1,150 @@
1
+ /* eslint-disable no-console */
2
+ import LoggerPort from '../../ports/LoggerPort.js';
3
+
4
+ /**
5
+ * Log levels in order of severity.
6
+ * @readonly
7
+ * @enum {number}
8
+ */
9
+ export const LogLevel = Object.freeze({
10
+ DEBUG: 0,
11
+ INFO: 1,
12
+ WARN: 2,
13
+ ERROR: 3,
14
+ SILENT: 4,
15
+ });
16
+
17
+ /**
18
+ * Map of level names to LogLevel values.
19
+ * @type {Record<string, number>}
20
+ */
21
+ const LEVEL_NAMES = Object.freeze({
22
+ debug: LogLevel.DEBUG,
23
+ info: LogLevel.INFO,
24
+ warn: LogLevel.WARN,
25
+ error: LogLevel.ERROR,
26
+ silent: LogLevel.SILENT,
27
+ });
28
+
29
+ /**
30
+ * Console logger adapter with structured JSON output.
31
+ *
32
+ * Provides a production-ready implementation of LoggerPort that outputs
33
+ * structured JSON logs to the console. Supports log level filtering,
34
+ * timestamps, and child loggers with inherited context.
35
+ *
36
+ * @example
37
+ * const logger = new ConsoleLogger({ level: LogLevel.INFO });
38
+ * logger.info('Server started', { port: 3000 });
39
+ * // Output: {"timestamp":"...","level":"INFO","message":"Server started","port":3000}
40
+ *
41
+ * @example
42
+ * const childLogger = logger.child({ requestId: 'abc-123' });
43
+ * childLogger.info('Request received');
44
+ * // Output: {"timestamp":"...","level":"INFO","message":"Request received","requestId":"abc-123"}
45
+ */
46
+ export default class ConsoleLogger extends LoggerPort {
47
+ /**
48
+ * Creates a new ConsoleLogger instance.
49
+ * @param {Object} [options] - Logger options
50
+ * @param {number} [options.level=LogLevel.INFO] - Minimum log level to output
51
+ * @param {Record<string, unknown>} [options.context={}] - Base context for all log entries
52
+ * @param {function(): string} [options.timestampFn] - Custom timestamp function (defaults to ISO string)
53
+ */
54
+ constructor({ level = LogLevel.INFO, context = {}, timestampFn } = {}) {
55
+ super();
56
+ this._level = typeof level === 'string' ? (LEVEL_NAMES[level] ?? LogLevel.INFO) : level;
57
+ this._context = Object.freeze({ ...context });
58
+ this._timestampFn = timestampFn || (() => new Date().toISOString());
59
+ }
60
+
61
+ /**
62
+ * Log a debug-level message.
63
+ * @param {string} message - The log message
64
+ * @param {Record<string, unknown>} [context] - Additional structured metadata
65
+ * @returns {void}
66
+ */
67
+ debug(message, context) {
68
+ this._log({ level: LogLevel.DEBUG, levelName: 'DEBUG', message, context });
69
+ }
70
+
71
+ /**
72
+ * Log an info-level message.
73
+ * @param {string} message - The log message
74
+ * @param {Record<string, unknown>} [context] - Additional structured metadata
75
+ * @returns {void}
76
+ */
77
+ info(message, context) {
78
+ this._log({ level: LogLevel.INFO, levelName: 'INFO', message, context });
79
+ }
80
+
81
+ /**
82
+ * Log a warning-level message.
83
+ * @param {string} message - The log message
84
+ * @param {Record<string, unknown>} [context] - Additional structured metadata
85
+ * @returns {void}
86
+ */
87
+ warn(message, context) {
88
+ this._log({ level: LogLevel.WARN, levelName: 'WARN', message, context });
89
+ }
90
+
91
+ /**
92
+ * Log an error-level message.
93
+ * @param {string} message - The log message
94
+ * @param {Record<string, unknown>} [context] - Additional structured metadata
95
+ * @returns {void}
96
+ */
97
+ error(message, context) {
98
+ this._log({ level: LogLevel.ERROR, levelName: 'ERROR', message, context });
99
+ }
100
+
101
+ /**
102
+ * Create a child logger with additional base context.
103
+ * Child loggers inherit parent context and merge with their own.
104
+ * @param {Record<string, unknown>} context - Additional base context for the child
105
+ * @returns {ConsoleLogger} A new logger instance with merged context
106
+ */
107
+ child(context) {
108
+ return new ConsoleLogger({
109
+ level: this._level,
110
+ context: { ...this._context, ...context },
111
+ timestampFn: this._timestampFn,
112
+ });
113
+ }
114
+
115
+ /**
116
+ * Internal logging implementation.
117
+ * @param {Object} opts - Log options
118
+ * @param {number} opts.level - Numeric log level
119
+ * @param {string} opts.levelName - String representation of level
120
+ * @param {string} opts.message - Log message
121
+ * @param {Record<string, unknown>} [opts.context] - Additional context
122
+ * @private
123
+ */
124
+ _log({ level, levelName, message, context }) {
125
+ if (level < this._level) {
126
+ return;
127
+ }
128
+
129
+ const entry = {
130
+ timestamp: this._timestampFn(),
131
+ level: levelName,
132
+ message,
133
+ ...this._context,
134
+ ...context,
135
+ };
136
+
137
+ const output = JSON.stringify(entry);
138
+
139
+ switch (level) {
140
+ case LogLevel.ERROR:
141
+ console.error(output);
142
+ break;
143
+ case LogLevel.WARN:
144
+ console.warn(output);
145
+ break;
146
+ default:
147
+ console.log(output);
148
+ }
149
+ }
150
+ }