noteconnection 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +257 -1803
- package/dist/src/agent_workspace.contract.parity.test.js +105 -421
- package/dist/src/agent_workspace.frontend.test.js +410 -11
- package/dist/src/android.knowledgebase.picker.contract.test.js +102 -0
- package/dist/src/appimage.patchelf.test.js +113 -0
- package/dist/src/appimage.portability.test.js +156 -0
- package/dist/src/backend/FileLoader.js +18 -4
- package/dist/src/backend/FileLoader.test.js +125 -0
- package/dist/src/backend/GraphBuilder.identity.test.js +86 -0
- package/dist/src/backend/GraphBuilder.js +66 -18
- package/dist/src/backend/GraphBuilder.matching.test.js +127 -0
- package/dist/src/backend/GraphMetrics.js +0 -3
- package/dist/src/backend/ResourceIdentity.js +155 -0
- package/dist/src/backend/ResourceIdentity.test.js +113 -0
- package/dist/src/backend/algorithms/CycleDetection.js +0 -1
- package/dist/src/backend/algorithms/HybridEngine.js +0 -3
- package/dist/src/backend/algorithms/LayoutEngine.js +0 -1
- package/dist/src/backend/algorithms/StatisticalAnalyzer.js +0 -4
- package/dist/src/backend/algorithms/TopologicalSort.js +0 -2
- package/dist/src/backend/build_dag.js +0 -1
- package/dist/src/capacitor.bridge.serialization.contract.test.js +3 -0
- package/dist/src/capacitor.runtime.contract.test.js +11 -2
- package/dist/src/core/Graph.js +138 -42
- package/dist/src/core/Graph.test.js +74 -0
- package/dist/src/core/NoteConnection.js +2 -2
- package/dist/src/core/PathBridge.js +265 -2
- package/dist/src/core/ResourceReference.js +17 -0
- package/dist/src/desktop.godot.pack.test.js +142 -0
- package/dist/src/desktop.runtime.graph.test.js +66 -0
- package/dist/src/export/WorkspaceExportBundle.js +44 -0
- package/dist/src/export/WorkspaceExportBundle.test.js +61 -0
- package/dist/src/foundation.release.evidence.contract.test.js +127 -9
- package/dist/src/foundation.sqlite.runtime.contract.test.js +6 -0
- package/dist/src/frontend/README.md +257 -1803
- package/dist/src/frontend/agent_workspace.js +665 -7
- package/dist/src/frontend/index.html +31 -2
- package/dist/src/frontend/knowledge_projection_contract.js +230 -0
- package/dist/src/frontend/knowledge_projection_store.js +214 -0
- package/dist/src/frontend/libs/path_core.js +158 -42
- package/dist/src/frontend/locales/en.json +32 -0
- package/dist/src/frontend/locales/zh.json +32 -0
- package/dist/src/frontend/mobile_budget_runtime.js +33 -0
- package/dist/src/frontend/mobile_exact_analyzer.js +393 -0
- package/dist/src/frontend/mobile_identity_contract.js +221 -0
- package/dist/src/frontend/mobile_semantic_comparator.js +264 -0
- package/dist/src/frontend/path.html +6 -2
- package/dist/src/frontend/simulationWorker.js +1 -1
- package/dist/src/frontend/source_manager.js +110 -4
- package/dist/src/frontend/storage_provider.js +661 -71
- package/dist/src/frontend/styles.css +107 -13
- package/dist/src/frontend/workspace_panes.js +111 -7
- package/dist/src/frontend.runtime.assets.contract.test.js +81 -0
- package/dist/src/graph.answer.operator.contract.test.js +72 -0
- package/dist/src/index.js +6 -2
- package/dist/src/index.knowledge-root.test.js +118 -0
- package/dist/src/indexing/IndexLifecycle.js +22 -0
- package/dist/src/indexing/IndexLifecycle.test.js +24 -0
- package/dist/src/knowledge.api.contract.test.js +4 -2
- package/dist/src/knowledge_projection.contract.test.js +134 -0
- package/dist/src/knowledge_projection_store.test.js +200 -0
- package/dist/src/learning/AnswerQualityEvaluation.js +153 -0
- package/dist/src/learning/AnswerQualityEvaluation.test.js +118 -0
- package/dist/src/learning/KnowledgeLearningPlatform.js +4687 -3892
- package/dist/src/learning/KnowledgeLearningPlatform.persistence.test.js +418 -0
- package/dist/src/learning/KnowledgeLearningPlatform.test.js +243 -6
- package/dist/src/learning/KnowledgeWorkspaceConversationRegression.js +25 -1
- package/dist/src/learning/KnowledgeWorkspaceConversationRegression.test.js +13 -5
- package/dist/src/learning/MeasurementEvidenceConflicts.test.js +78 -0
- package/dist/src/learning/PublicAnswerContract.test.js +85 -0
- package/dist/src/learning/PublicEvidenceSelection.test.js +94 -0
- package/dist/src/learning/PublicMeasurementDisclosure.test.js +54 -0
- package/dist/src/learning/agentConversationExecution.js +211 -0
- package/dist/src/learning/agentConversationExecution.test.js +209 -0
- package/dist/src/learning/agentConversationSerialization.js +266 -0
- package/dist/src/learning/agentConversationSerialization.test.js +165 -0
- package/dist/src/learning/agentResponseBudget.js +179 -0
- package/dist/src/learning/agentResponseBudget.test.js +97 -0
- package/dist/src/learning/agentWorkspaceCapabilityContracts.js +65 -0
- package/dist/src/learning/answerReleaseReview.js +1191 -250
- package/dist/src/learning/answerReleaseReview.test.js +1293 -7
- package/dist/src/learning/answerTaskPlan.js +473 -0
- package/dist/src/learning/conversationComposer.js +627 -386
- package/dist/src/learning/conversationComposer.test.js +1403 -0
- package/dist/src/learning/evidenceContextAssembler.js +312 -120
- package/dist/src/learning/graphAnswerCoverage.js +37 -0
- package/dist/src/learning/graphAnswerCoverage.test.js +93 -0
- package/dist/src/learning/graphAnswerCoverageCalibration.js +59 -0
- package/dist/src/learning/graphAnswerCoverageCalibration.test.js +46 -0
- package/dist/src/learning/graphAnswerFacts.js +94 -0
- package/dist/src/learning/graphAnswerFacts.test.js +39 -0
- package/dist/src/learning/graphAnswerPlan.js +908 -0
- package/dist/src/learning/graphAnswerPlan.test.js +907 -0
- package/dist/src/learning/graphAnswerQualityCalibration.js +255 -0
- package/dist/src/learning/graphAnswerQualityCalibration.test.js +72 -0
- package/dist/src/learning/graphAnswerQualityPolicy.js +30 -0
- package/dist/src/learning/graphClaimMatcher.js +104 -0
- package/dist/src/learning/graphConditionedContext.js +109 -0
- package/dist/src/learning/graphConditionedContext.test.js +99 -0
- package/dist/src/learning/graphContextAssembler.js +3 -13
- package/dist/src/learning/graphContextAssembler.test.js +2 -2
- package/dist/src/learning/graphExpansionPolicy.js +22 -0
- package/dist/src/learning/graphExpansionPolicy.test.js +33 -0
- package/dist/src/learning/identityMetadata.test.js +26 -0
- package/dist/src/learning/index.js +1 -0
- package/dist/src/learning/mobileAnswerProjection.js +104 -0
- package/dist/src/learning/mobileAnswerProjection.test.js +102 -0
- package/dist/src/learning/queryBackend.js +34 -10
- package/dist/src/learning/queryBackend.test.js +21 -0
- package/dist/src/learning/ragContextPack.js +115 -16
- package/dist/src/learning/ragContextPack.test.js +151 -0
- package/dist/src/learning/ragEvidenceQuality.js +96 -0
- package/dist/src/learning/ragEvidenceQuality.test.js +29 -0
- package/dist/src/learning/ragPublicText.js +67 -4
- package/dist/src/learning/ragPublicText.test.js +57 -0
- package/dist/src/learning/ragSufficiencyJudge.js +11 -1
- package/dist/src/learning/ragSufficiencyProviderJudge.js +16 -26
- package/dist/src/learning/ragSufficiencyProviderJudge.test.js +51 -0
- package/dist/src/learning/requestNormalization.js +106 -0
- package/dist/src/learning/requestNormalization.test.js +101 -0
- package/dist/src/learning/store.js +116 -12
- package/dist/src/learning/store.test.js +299 -0
- package/dist/src/learning/vectorAccelerationAdapter.js +22 -6
- package/dist/src/learning/vectorAccelerationAdapter.test.js +30 -0
- package/dist/src/learning/workspaceHydration.js +168 -0
- package/dist/src/learning/workspaceHydration.test.js +30 -0
- package/dist/src/license.policy.contract.test.js +9 -4
- package/dist/src/markdown/MarkdownGateway.js +4 -0
- package/dist/src/markdown/MarkdownGateway.test.js +59 -0
- package/dist/src/middleware/SseResponseWriter.js +122 -0
- package/dist/src/middleware/SseResponseWriter.test.js +54 -0
- package/dist/src/middleware/auth.js +28 -7
- package/dist/src/middleware/auth.test.js +33 -0
- package/dist/src/middleware/index.js +2 -1
- package/dist/src/mobile.android.evidence.contract.test.js +146 -0
- package/dist/src/mobile.android.signing.contract.test.js +66 -0
- package/dist/src/mobile.artifact.contract.test.js +144 -0
- package/dist/src/mobile.budget.contract.test.js +90 -0
- package/dist/src/mobile.canonical-id.readiness.test.js +54 -0
- package/dist/src/mobile.graph.parity.contract.test.js +207 -0
- package/dist/src/mobile.identity.contract.test.js +66 -0
- package/dist/src/mobile.identity.corpus.manifest.test.js +72 -0
- package/dist/src/mobile.identity.corpus.test.js +40 -0
- package/dist/src/mobile.native.recovery.contract.test.js +64 -0
- package/dist/src/mobile.pipeline.test.js +11 -1
- package/dist/src/mobile.profile.contract.test.js +225 -0
- package/dist/src/mobile.semantic.parity.test.js +92 -0
- package/dist/src/mobile_exact_analyzer.test.js +113 -0
- package/dist/src/notemd/ContentGenerator.js +4 -9
- package/dist/src/notemd/FileProcessor.js +4 -9
- package/dist/src/notemd/NotemdService.js +12 -17
- package/dist/src/notemd/Translator.js +4 -9
- package/dist/src/notemd/progressReporter.js +9 -0
- package/dist/src/notemd.server.integration.test.js +119 -4
- package/dist/src/notemd.workflow.pipeline.test.js +1 -1
- package/dist/src/pathbridge.handshake.contract.test.js +29 -0
- package/dist/src/pathbridge.host-adapter.contract.test.js +133 -0
- package/dist/src/pathcore.bundle.contract.test.js +8 -0
- package/dist/src/platform/ExportProfile.js +28 -0
- package/dist/src/platform/MobileBudget.js +11 -0
- package/dist/src/platform/PlatformCapabilities.js +14 -0
- package/dist/src/platform/PlatformCapabilities.test.js +6 -0
- package/dist/src/query_backend.external_http.integration.test.js +2 -2
- package/dist/src/release.godot.mirror.contract.test.js +28 -4
- package/dist/src/resources/ResourceRegistry.js +40 -0
- package/dist/src/resources/ResourceRegistry.test.js +36 -0
- package/dist/src/routes/index.js +0 -4
- package/dist/src/routes/knowledge.js +46 -33
- package/dist/src/routes/knowledgePayload.js +185 -0
- package/dist/src/routes/knowledgePayload.test.js +53 -0
- package/dist/src/routes/markdown.js +8 -0
- package/dist/src/routes/notemd.js +990 -234
- package/dist/src/routes/registry.contract.test.js +5 -2
- package/dist/src/routes/registry.shadow.contract.test.js +22 -0
- package/dist/src/routes/render.js +14 -51
- package/dist/src/runtime.capabilities.test.js +18 -2
- package/dist/src/server.js +566 -1090
- package/dist/src/server.migration.test.js +417 -15
- package/dist/src/sidecar.build.lock.contract.test.js +201 -0
- package/dist/src/sidecar.freshness.contract.test.js +184 -0
- package/dist/src/sidecar.replacement.boundary.contract.test.js +6 -3
- package/dist/src/sidecar.supply.readiness.contract.test.js +11 -0
- package/dist/src/simulation.worker.offline.test.js +104 -0
- package/dist/src/storage.provider.capacitor.content.contract.test.js +4 -1
- package/dist/src/storage.provider.contract.test.js +152 -1
- package/dist/src/tauri.frontend.build.contract.test.js +8 -0
- package/dist/src/tauri.window.evidence.execution.test.js +68 -0
- package/dist/src/utils/RuntimePaths.js +6 -0
- package/dist/src/utils/RuntimePaths.test.js +27 -0
- package/dist/src/windows.installer.build.test.js +109 -0
- package/dist/src/windows.installer.payload.test.js +76 -0
- package/dist/src/windows.installer.runtime.test.js +69 -0
- package/dist/src/workspace/WorkspaceRegistry.js +13 -0
- package/package.json +32 -10
- package/dist/src/routes/data.js +0 -267
- package/dist/src/routes/diagnostics.js +0 -51
|
@@ -285,12 +285,21 @@ function parseExternalHttpRetryAfterMs(rawValue, nowMs = Date.now()) {
|
|
|
285
285
|
}
|
|
286
286
|
return Math.min(EXTERNAL_HTTP_RETRY_AFTER_MS_MAX, Math.floor(deltaMs));
|
|
287
287
|
}
|
|
288
|
-
async function waitForRetryDelay(ms) {
|
|
288
|
+
async function waitForRetryDelay(ms, signal) {
|
|
289
|
+
signal?.throwIfAborted();
|
|
289
290
|
if (!Number.isFinite(ms) || ms <= 0) {
|
|
290
291
|
return;
|
|
291
292
|
}
|
|
292
|
-
await new Promise((resolve) => {
|
|
293
|
-
|
|
293
|
+
await new Promise((resolve, reject) => {
|
|
294
|
+
const onAbort = () => {
|
|
295
|
+
clearTimeout(timer);
|
|
296
|
+
reject(signal?.reason);
|
|
297
|
+
};
|
|
298
|
+
const timer = setTimeout(() => {
|
|
299
|
+
signal?.removeEventListener('abort', onAbort);
|
|
300
|
+
resolve();
|
|
301
|
+
}, ms);
|
|
302
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
294
303
|
});
|
|
295
304
|
}
|
|
296
305
|
function selectCandidatesFromExternalStub(input) {
|
|
@@ -483,6 +492,7 @@ function createVectorAccelerationAdapter(provider, options = {}) {
|
|
|
483
492
|
return {
|
|
484
493
|
id: EXTERNAL_HTTP_ADAPTER_ID,
|
|
485
494
|
async syncIndex(input) {
|
|
495
|
+
input.signal?.throwIfAborted();
|
|
486
496
|
const requestedRepresentationVersion = normalizeRepresentationVersion(input.representationVersion);
|
|
487
497
|
const requestedEmbeddingModelId = normalizeEmbeddingModelId(input.embeddingModelId);
|
|
488
498
|
const requestedEmbeddingDimension = normalizeEmbeddingDimension(input.embeddingDimension);
|
|
@@ -543,7 +553,7 @@ function createVectorAccelerationAdapter(provider, options = {}) {
|
|
|
543
553
|
tokenToAtomIds: Array.from(input.tokenToAtomIds.entries()),
|
|
544
554
|
signatureBuckets: Array.from(input.signatureBuckets.entries()),
|
|
545
555
|
}),
|
|
546
|
-
signal: controller.signal,
|
|
556
|
+
signal: input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal,
|
|
547
557
|
});
|
|
548
558
|
const connectorRequestId = normalizeExternalHttpRequestId(response?.headers?.get?.('x-request-id'), clientRequestId);
|
|
549
559
|
if (connectorRequestId) {
|
|
@@ -559,6 +569,7 @@ function createVectorAccelerationAdapter(provider, options = {}) {
|
|
|
559
569
|
throw new Error(indexSyncMessage);
|
|
560
570
|
}
|
|
561
571
|
const payload = await response.json();
|
|
572
|
+
input.signal?.throwIfAborted();
|
|
562
573
|
const responseRepresentationVersion = normalizeRepresentationVersion(payload?.representationVersion);
|
|
563
574
|
const responseEmbeddingModelId = normalizeEmbeddingModelId(payload?.embeddingModelId);
|
|
564
575
|
const responseEmbeddingDimension = normalizeEmbeddingDimension(payload?.embeddingDimension);
|
|
@@ -609,6 +620,7 @@ function createVectorAccelerationAdapter(provider, options = {}) {
|
|
|
609
620
|
};
|
|
610
621
|
}
|
|
611
622
|
catch (error) {
|
|
623
|
+
input.signal?.throwIfAborted();
|
|
612
624
|
syncFailureCount += 1;
|
|
613
625
|
indexSyncStatus = endpoint ? 'degraded' : 'unavailable';
|
|
614
626
|
indexSyncMessage = truncateMessage(error?.message || error, 240) || 'external_http_index_sync_failed';
|
|
@@ -624,6 +636,7 @@ function createVectorAccelerationAdapter(provider, options = {}) {
|
|
|
624
636
|
}
|
|
625
637
|
},
|
|
626
638
|
async selectCandidates(input) {
|
|
639
|
+
input.signal?.throwIfAborted();
|
|
627
640
|
selectionRequestCount += 1;
|
|
628
641
|
const requestedRepresentationVersion = normalizeRepresentationVersion(input.representationVersion);
|
|
629
642
|
const requestedEmbeddingModelId = normalizeEmbeddingModelId(input.embeddingModelId);
|
|
@@ -686,6 +699,7 @@ function createVectorAccelerationAdapter(provider, options = {}) {
|
|
|
686
699
|
const totalAttempts = Math.max(1, maxRetries + 1);
|
|
687
700
|
let lastError = null;
|
|
688
701
|
for (let attemptIndex = 0; attemptIndex < totalAttempts; attemptIndex += 1) {
|
|
702
|
+
input.signal?.throwIfAborted();
|
|
689
703
|
const currentAttempt = attemptIndex + 1;
|
|
690
704
|
const clientRequestId = nextExternalHttpRequestId();
|
|
691
705
|
let connectorRequestId = '';
|
|
@@ -714,7 +728,7 @@ function createVectorAccelerationAdapter(provider, options = {}) {
|
|
|
714
728
|
: undefined,
|
|
715
729
|
indexSignature: requestedIndexSignature || undefined,
|
|
716
730
|
}),
|
|
717
|
-
signal: controller.signal,
|
|
731
|
+
signal: input.signal ? AbortSignal.any([input.signal, controller.signal]) : controller.signal,
|
|
718
732
|
});
|
|
719
733
|
connectorRequestId = normalizeExternalHttpRequestId(response?.headers?.get?.('x-request-id'), clientRequestId);
|
|
720
734
|
if (connectorRequestId) {
|
|
@@ -739,6 +753,7 @@ function createVectorAccelerationAdapter(provider, options = {}) {
|
|
|
739
753
|
throw new Error(`external_http_status_${statusCode}:errorCode=${statusErrorCode}${retryAfterSegment}:requestId=${connectorRequestId || clientRequestId}`);
|
|
740
754
|
}
|
|
741
755
|
const payload = await response.json();
|
|
756
|
+
input.signal?.throwIfAborted();
|
|
742
757
|
const mode = String(payload?.mode || '').trim().toLowerCase();
|
|
743
758
|
const normalizedMode = (mode === 'token_prefilter'
|
|
744
759
|
|| mode === 'token_signature_prefilter') ? mode : 'full_scan';
|
|
@@ -858,6 +873,7 @@ function createVectorAccelerationAdapter(provider, options = {}) {
|
|
|
858
873
|
};
|
|
859
874
|
}
|
|
860
875
|
catch (error) {
|
|
876
|
+
input.signal?.throwIfAborted();
|
|
861
877
|
lastError = error;
|
|
862
878
|
const transient = isExternalHttpTransientFailure(error);
|
|
863
879
|
const finalAttempt = currentAttempt >= totalAttempts;
|
|
@@ -897,7 +913,7 @@ function createVectorAccelerationAdapter(provider, options = {}) {
|
|
|
897
913
|
+ `requestId=${connectorRequestId || clientRequestId}`),
|
|
898
914
|
checkedAt: new Date().toISOString(),
|
|
899
915
|
});
|
|
900
|
-
await waitForRetryDelay(effectiveBackoffDelay);
|
|
916
|
+
await waitForRetryDelay(effectiveBackoffDelay, input.signal);
|
|
901
917
|
continue;
|
|
902
918
|
}
|
|
903
919
|
consecutiveFailures += 1;
|
|
@@ -40,6 +40,36 @@ describe('vector acceleration adapter', () => {
|
|
|
40
40
|
const adapter = (0, vectorAccelerationAdapter_1.createVectorAccelerationAdapter)('local');
|
|
41
41
|
expect(adapter).toBeUndefined();
|
|
42
42
|
});
|
|
43
|
+
test.each(['syncIndex', 'selectCandidates'])('cancels %s HTTP without retry or circuit failure', async (operation) => {
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
const cancelled = new Error('turn_cancelled');
|
|
46
|
+
const fetchMock = jest.fn((_url, init) => new Promise((_resolve, reject) => {
|
|
47
|
+
const signal = init.signal;
|
|
48
|
+
signal.addEventListener('abort', () => reject(signal.reason), { once: true });
|
|
49
|
+
controller.abort(cancelled);
|
|
50
|
+
}));
|
|
51
|
+
globalThis.fetch = fetchMock;
|
|
52
|
+
const adapter = (0, vectorAccelerationAdapter_1.createVectorAccelerationAdapter)('external_http', {
|
|
53
|
+
externalHttp: { endpoint: 'http://127.0.0.1:18080', maxRetries: 3 },
|
|
54
|
+
});
|
|
55
|
+
const input = { ...baseSelectionInput, signal: controller.signal };
|
|
56
|
+
await expect(adapter[operation](input)).rejects.toBe(cancelled);
|
|
57
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(JSON.parse(fetchMock.mock.calls[0][1].body)).not.toHaveProperty('signal');
|
|
59
|
+
expect(adapter.getHealth?.().consecutiveFailures).toBe(0);
|
|
60
|
+
});
|
|
61
|
+
test('cancels the connector retry delay before a second request', async () => {
|
|
62
|
+
const controller = new AbortController();
|
|
63
|
+
const cancelled = new Error('turn_cancelled');
|
|
64
|
+
globalThis.fetch = jest.fn().mockRejectedValue(new Error('socket_hang_up'));
|
|
65
|
+
const adapter = (0, vectorAccelerationAdapter_1.createVectorAccelerationAdapter)('external_http', {
|
|
66
|
+
externalHttp: { endpoint: 'http://127.0.0.1:18080', maxRetries: 3, retryDelayMs: 1000 },
|
|
67
|
+
});
|
|
68
|
+
const result = Promise.resolve(adapter.selectCandidates({ ...baseSelectionInput, signal: controller.signal }));
|
|
69
|
+
setImmediate(() => controller.abort(cancelled));
|
|
70
|
+
await expect(result).rejects.toBe(cancelled);
|
|
71
|
+
expect(globalThis.fetch).toHaveBeenCalledTimes(1);
|
|
72
|
+
});
|
|
43
73
|
test('external stub adapter selects token-based candidates for large corpus', async () => {
|
|
44
74
|
const adapter = (0, vectorAccelerationAdapter_1.createVectorAccelerationAdapter)('external_stub');
|
|
45
75
|
expect(adapter).toBeDefined();
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.deriveKnowledgeTargetLookupQueries = deriveKnowledgeTargetLookupQueries;
|
|
37
|
+
exports.normalizeKnowledgeSourcePath = normalizeKnowledgeSourcePath;
|
|
38
|
+
exports.buildKnowledgeSourceInventoryDiff = buildKnowledgeSourceInventoryDiff;
|
|
39
|
+
exports.markdownPreviewMatchesTitleLikeQueries = markdownPreviewMatchesTitleLikeQueries;
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
function normalizeLookupQuery(value) {
|
|
42
|
+
return String(value || '')
|
|
43
|
+
.normalize('NFKC')
|
|
44
|
+
.replace(/[??!!。.;;:,,]+/gu, ' ')
|
|
45
|
+
.replace(/\s+/gu, ' ')
|
|
46
|
+
.trim()
|
|
47
|
+
.toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
function stripDefinitionPredicate(value) {
|
|
50
|
+
return value
|
|
51
|
+
.replace(/^(?:what\s+is|what\s+are|define|explain|tell\s+me\s+about|what's)\s+/iu, '')
|
|
52
|
+
.replace(/^(?:什么是|何谓|解释(?:一下)?|介绍(?:一下)?|请(?:解释|介绍)(?:一下)?)\s*/u, '')
|
|
53
|
+
.trim();
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Extracts bounded title-like variants from a user query. Compound prompts keep
|
|
57
|
+
* their first definition clause so a Chinese subject can resolve an English
|
|
58
|
+
* filename without scanning the whole corpus.
|
|
59
|
+
*/
|
|
60
|
+
function deriveKnowledgeTargetLookupQueries(query) {
|
|
61
|
+
const source = String(query || '').normalize('NFKC');
|
|
62
|
+
const candidates = new Set();
|
|
63
|
+
const add = (value) => {
|
|
64
|
+
const normalized = normalizeLookupQuery(value);
|
|
65
|
+
if (!normalized) {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
candidates.add(normalized);
|
|
69
|
+
const stripped = stripDefinitionPredicate(normalized);
|
|
70
|
+
if (stripped) {
|
|
71
|
+
candidates.add(stripped);
|
|
72
|
+
candidates.add(stripped.replace(/\s+/gu, ''));
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
add(source);
|
|
76
|
+
source.split(/[??!!。.;;\n\r]+/gu).forEach(add);
|
|
77
|
+
const definitionSubject = source.match(/^(?:what\s+is|what\s+are|define|explain|tell\s+me\s+about|what's|什么是|何谓|解释(?:一下)?|介绍(?:一下)?|请(?:解释|介绍)(?:一下)?)\s*([^??!!。.;;\n\r]+)/iu);
|
|
78
|
+
if (definitionSubject?.[1]) {
|
|
79
|
+
const subject = definitionSubject[1]
|
|
80
|
+
.replace(/\s+(?:我应该|应该|how\s+should|what\s+should).*$/iu, '')
|
|
81
|
+
.trim();
|
|
82
|
+
add(subject);
|
|
83
|
+
}
|
|
84
|
+
if (candidates.has('water glass') || candidates.has('waterglass')) {
|
|
85
|
+
candidates.add('water glass');
|
|
86
|
+
candidates.add('waterglass');
|
|
87
|
+
candidates.add('水玻璃');
|
|
88
|
+
candidates.add('水杯');
|
|
89
|
+
}
|
|
90
|
+
return Array.from(candidates.values()).filter(Boolean);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Produces the case-folded path key used only for inventory reconciliation.
|
|
94
|
+
* The original spelling is retained in diff output for diagnostics and ingest.
|
|
95
|
+
*/
|
|
96
|
+
function normalizeKnowledgeSourcePath(value) {
|
|
97
|
+
return String(value || '')
|
|
98
|
+
.normalize('NFKC')
|
|
99
|
+
.replace(/\\/g, '/')
|
|
100
|
+
.replace(/^\/+/, '')
|
|
101
|
+
.replace(/\/{2,}/g, '/')
|
|
102
|
+
.replace(/\/+$/g, '')
|
|
103
|
+
.toLowerCase();
|
|
104
|
+
}
|
|
105
|
+
function buildKnowledgeSourceInventoryDiff(params) {
|
|
106
|
+
const collect = (sourcePaths) => {
|
|
107
|
+
const byNormalizedPath = new Map();
|
|
108
|
+
sourcePaths.forEach((sourcePath) => {
|
|
109
|
+
const normalized = normalizeKnowledgeSourcePath(sourcePath);
|
|
110
|
+
if (normalized && !byNormalizedPath.has(normalized)) {
|
|
111
|
+
byNormalizedPath.set(normalized, sourcePath);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
return byNormalizedPath;
|
|
115
|
+
};
|
|
116
|
+
const disk = collect(params.diskSourcePaths);
|
|
117
|
+
const indexed = collect(params.indexedSourcePaths);
|
|
118
|
+
const addedSourcePaths = Array.from(disk.entries())
|
|
119
|
+
.filter(([normalizedPath]) => !indexed.has(normalizedPath))
|
|
120
|
+
.map(([, sourcePath]) => sourcePath)
|
|
121
|
+
.sort((left, right) => normalizeKnowledgeSourcePath(left).localeCompare(normalizeKnowledgeSourcePath(right)));
|
|
122
|
+
const removedSourcePaths = Array.from(indexed.entries())
|
|
123
|
+
.filter(([normalizedPath]) => !disk.has(normalizedPath))
|
|
124
|
+
.map(([, sourcePath]) => sourcePath)
|
|
125
|
+
.sort((left, right) => normalizeKnowledgeSourcePath(left).localeCompare(normalizeKnowledgeSourcePath(right)));
|
|
126
|
+
return { addedSourcePaths, removedSourcePaths };
|
|
127
|
+
}
|
|
128
|
+
function normalizeTitleCandidate(value) {
|
|
129
|
+
return String(value || '')
|
|
130
|
+
.normalize('NFKC')
|
|
131
|
+
.replace(/[^\p{L}\p{N}\s_-]+/gu, ' ')
|
|
132
|
+
.replace(/\s+/g, ' ')
|
|
133
|
+
.trim()
|
|
134
|
+
.toLowerCase()
|
|
135
|
+
.replace(/^(?:the|a|an)\s+/u, '')
|
|
136
|
+
.trim();
|
|
137
|
+
}
|
|
138
|
+
function collectPreviewTitleCandidates(sourcePath, preview) {
|
|
139
|
+
const candidates = new Set();
|
|
140
|
+
const basename = path.posix.basename(String(sourcePath || '').replace(/\\/g, '/'))
|
|
141
|
+
.replace(/\.[^.]+$/u, '');
|
|
142
|
+
if (basename) {
|
|
143
|
+
candidates.add(basename);
|
|
144
|
+
}
|
|
145
|
+
String(preview || '').split(/\r?\n/u).forEach((line) => {
|
|
146
|
+
const trimmed = line.trim();
|
|
147
|
+
if (!trimmed) {
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const heading = trimmed.match(/^#{1,6}\s+(.+?)\s*#*$/u);
|
|
151
|
+
if (heading?.[1]) {
|
|
152
|
+
candidates.add(heading[1]);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (candidates.size <= 1 && !/^```|^[-*+]\s|^\d+[.)]\s/u.test(trimmed)) {
|
|
156
|
+
candidates.add(trimmed);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
return Array.from(candidates.values()).map(normalizeTitleCandidate).filter(Boolean);
|
|
160
|
+
}
|
|
161
|
+
function markdownPreviewMatchesTitleLikeQueries(input) {
|
|
162
|
+
const queries = Array.from(new Set(input.titleLikeQueries.map(normalizeTitleCandidate).filter((query) => query.length >= 2)));
|
|
163
|
+
if (queries.length <= 0) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
const candidates = collectPreviewTitleCandidates(input.sourcePath, input.preview);
|
|
167
|
+
return candidates.some((candidate) => queries.some((query) => candidate === query || candidate.includes(query)));
|
|
168
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const workspaceHydration_1 = require("./workspaceHydration");
|
|
4
|
+
describe('workspaceHydration', () => {
|
|
5
|
+
test('detects source files that exist on disk but are absent from the indexed workspace', () => {
|
|
6
|
+
const diff = (0, workspaceHydration_1.buildKnowledgeSourceInventoryDiff)({
|
|
7
|
+
diskSourcePaths: [
|
|
8
|
+
'Knowledge_Base/waterglass/water glass.md',
|
|
9
|
+
'Knowledge_Base/waterglass/Amorphous ice.md',
|
|
10
|
+
],
|
|
11
|
+
indexedSourcePaths: [
|
|
12
|
+
'knowledge_base\\waterglass\\water glass.md',
|
|
13
|
+
],
|
|
14
|
+
});
|
|
15
|
+
expect(diff.addedSourcePaths).toEqual([
|
|
16
|
+
'Knowledge_Base/waterglass/Amorphous ice.md',
|
|
17
|
+
]);
|
|
18
|
+
expect(diff.removedSourcePaths).toEqual([]);
|
|
19
|
+
});
|
|
20
|
+
test('matches a Chinese heading when the file basename uses the English concept name', () => {
|
|
21
|
+
expect((0, workspaceHydration_1.markdownPreviewMatchesTitleLikeQueries)({
|
|
22
|
+
sourcePath: 'Knowledge_Base/waterglass/Amorphous ice.md',
|
|
23
|
+
preview: '## 非晶冰\n\n非晶冰(Amorphous ice)是水的一种固态形式。',
|
|
24
|
+
titleLikeQueries: ['非晶冰'],
|
|
25
|
+
})).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
test('keeps the definition subject from a compound Chinese conversation query', () => {
|
|
28
|
+
expect((0, workspaceHydration_1.deriveKnowledgeTargetLookupQueries)('什么是非晶冰?我应该通过哪些知识点学习?')).toEqual(expect.arrayContaining(['非晶冰']));
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -44,6 +44,7 @@ describe('license policy contract', () => {
|
|
|
44
44
|
const tauriCargoTomlPath = path.join(repoRoot, 'src-tauri', 'Cargo.toml');
|
|
45
45
|
const licensePath = path.join(repoRoot, 'LICENSE');
|
|
46
46
|
const readmePath = path.join(repoRoot, 'README.md');
|
|
47
|
+
const chineseReadmePath = path.join(repoRoot, 'README_zh.md');
|
|
47
48
|
test('root package manifest is pinned to GPL-3.0-only', () => {
|
|
48
49
|
const pkg = readJson(packageJsonPath);
|
|
49
50
|
expect(pkg.license).toBe('GPL-3.0-only');
|
|
@@ -57,10 +58,14 @@ describe('license policy contract', () => {
|
|
|
57
58
|
expect(license).toContain('GNU GENERAL PUBLIC LICENSE');
|
|
58
59
|
expect(license).toContain('Version 3, 29 June 2007');
|
|
59
60
|
});
|
|
60
|
-
test('README license
|
|
61
|
+
test('README license sections advertise GPL-3.0-only in both locales', () => {
|
|
61
62
|
const readme = fs.readFileSync(readmePath, 'utf8');
|
|
62
|
-
|
|
63
|
-
expect(readme).toContain('
|
|
64
|
-
expect(readme).toContain('GNU General Public License v3.0
|
|
63
|
+
const chineseReadme = fs.readFileSync(chineseReadmePath, 'utf8');
|
|
64
|
+
expect(readme).toContain('## License');
|
|
65
|
+
expect(readme).toContain('GNU General Public License v3.0');
|
|
66
|
+
expect(readme).toContain('GPL-3.0-only');
|
|
67
|
+
expect(chineseReadme).toContain('## 开源许可');
|
|
68
|
+
expect(chineseReadme).toContain('GNU General Public License v3.0');
|
|
69
|
+
expect(chineseReadme).toContain('GPL-3.0-only');
|
|
65
70
|
});
|
|
66
71
|
});
|
|
@@ -697,6 +697,10 @@ class MarkdownGateway {
|
|
|
697
697
|
candidates.push(path.join(path.dirname(process.execPath), binaryName));
|
|
698
698
|
candidates.push(path.join(path.dirname(process.execPath), '..', 'Resources', binaryName));
|
|
699
699
|
}
|
|
700
|
+
// Tauri removes the target triple from installed external binary names.
|
|
701
|
+
const installedBinaryName = process.platform === 'win32' ? 'markdown-worker.exe' : 'markdown-worker';
|
|
702
|
+
candidates.push(path.join(path.dirname(process.execPath), installedBinaryName));
|
|
703
|
+
candidates.push(path.join(path.dirname(process.execPath), '..', 'Resources', installedBinaryName));
|
|
700
704
|
for (const candidate of candidates) {
|
|
701
705
|
try {
|
|
702
706
|
if (!candidate) {
|
|
@@ -37,6 +37,7 @@ const fs = __importStar(require("fs"));
|
|
|
37
37
|
const os = __importStar(require("os"));
|
|
38
38
|
const path = __importStar(require("path"));
|
|
39
39
|
const MarkdownGateway_1 = require("./MarkdownGateway");
|
|
40
|
+
const { resolveHostMarkdownWorkerBinaryName } = require('../../scripts/tauri-sidecar-utils');
|
|
40
41
|
class TempDir {
|
|
41
42
|
constructor(prefix) {
|
|
42
43
|
this.path = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), `${prefix}-`));
|
|
@@ -192,4 +193,62 @@ describe('MarkdownGateway', () => {
|
|
|
192
193
|
}
|
|
193
194
|
}
|
|
194
195
|
});
|
|
196
|
+
const testUnix = process.platform === 'win32' ? test.skip : test;
|
|
197
|
+
testUnix.each(['beside-server', 'Resources', 'explicit-override', 'build-filename'])('buildIndex discovers the %s worker and preserves lookup priority', async (location) => {
|
|
198
|
+
const installedDirectory = ctx.temp.mkdir('installed/bin');
|
|
199
|
+
const originalExecPath = process.execPath;
|
|
200
|
+
const originalWorkerPath = process.env.NOTE_CONNECTION_MARKDOWN_WORKER_PATH;
|
|
201
|
+
const writeWorker = (workerPath, anchorId) => {
|
|
202
|
+
fs.mkdirSync(path.dirname(workerPath), { recursive: true });
|
|
203
|
+
fs.writeFileSync(workerPath, `#!/usr/bin/env node
|
|
204
|
+
const fs = require('fs');
|
|
205
|
+
let input = '';
|
|
206
|
+
process.stdin.on('data', chunk => { input += chunk; });
|
|
207
|
+
process.stdin.on('end', () => {
|
|
208
|
+
const request = JSON.parse(input);
|
|
209
|
+
if (request.kind !== 'build_index') process.exit(1);
|
|
210
|
+
const content = fs.readFileSync(request.filePath);
|
|
211
|
+
const block = { id: 0, type: 'heading', startByte: 0, endByte: content.length, startLine: 1, endLine: 1, anchorId: ${JSON.stringify(anchorId)} };
|
|
212
|
+
process.stdout.write(JSON.stringify({ ok: true, index: { totalBytes: content.length, totalLines: 1, blocks: [block], anchors: [], wikiLinks: [] } }));
|
|
213
|
+
});
|
|
214
|
+
`, { mode: 0o755 });
|
|
215
|
+
};
|
|
216
|
+
try {
|
|
217
|
+
process.execPath = path.join(installedDirectory, 'server');
|
|
218
|
+
delete process.env.NOTE_CONNECTION_MARKDOWN_WORKER_PATH;
|
|
219
|
+
const installedWorker = location === 'Resources'
|
|
220
|
+
? path.join(installedDirectory, '..', 'Resources', 'markdown-worker')
|
|
221
|
+
: path.join(installedDirectory, 'markdown-worker');
|
|
222
|
+
writeWorker(installedWorker, 'installed-worker');
|
|
223
|
+
if (location === 'explicit-override' || location === 'build-filename') {
|
|
224
|
+
writeWorker(path.join(ctx.projectRoot, 'src-tauri', 'bin', resolveHostMarkdownWorkerBinaryName()), 'build-worker');
|
|
225
|
+
}
|
|
226
|
+
if (location === 'explicit-override') {
|
|
227
|
+
const explicitWorker = path.join(ctx.temp.path, 'explicit-worker');
|
|
228
|
+
writeWorker(explicitWorker, 'explicit-worker');
|
|
229
|
+
process.env.NOTE_CONNECTION_MARKDOWN_WORKER_PATH = explicitWorker;
|
|
230
|
+
}
|
|
231
|
+
const filePath = ctx.temp.file(path.join('project', 'Knowledge_Base', 'installed.md'), '# Installed markdown worker');
|
|
232
|
+
const index = await ctx.gateway.buildIndex({ filePath }, {
|
|
233
|
+
markdownEngine: 'pulldown',
|
|
234
|
+
chunkBlockSize: 8,
|
|
235
|
+
prefetchBlocks: 2,
|
|
236
|
+
indexCacheTtlSec: 1800,
|
|
237
|
+
maxDocBytes: 32 * 1024 * 1024,
|
|
238
|
+
});
|
|
239
|
+
expect(index.engine).toBe('pulldown');
|
|
240
|
+
expect(index.fallbackReason).toBeUndefined();
|
|
241
|
+
const chunk = await ctx.gateway.getChunk({ indexId: index.indexId, startBlock: 0, blockCount: 1 });
|
|
242
|
+
const expectedAnchor = location === 'explicit-override' ? 'explicit-worker'
|
|
243
|
+
: location === 'build-filename' ? 'build-worker' : 'installed-worker';
|
|
244
|
+
expect(chunk.blocks[0].anchorId).toBe(expectedAnchor);
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
process.execPath = originalExecPath;
|
|
248
|
+
if (originalWorkerPath === undefined)
|
|
249
|
+
delete process.env.NOTE_CONNECTION_MARKDOWN_WORKER_PATH;
|
|
250
|
+
else
|
|
251
|
+
process.env.NOTE_CONNECTION_MARKDOWN_WORKER_PATH = originalWorkerPath;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
195
254
|
});
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SseResponseWriter = void 0;
|
|
4
|
+
/** Owns backpressure and the single terminal outcome of one HTTP event stream. */
|
|
5
|
+
class SseResponseWriter {
|
|
6
|
+
constructor(destination, policy = {
|
|
7
|
+
maxBufferedBytes: 64 * 1024 * 1024 + 256 * 1024,
|
|
8
|
+
drainTimeoutMs: 15000,
|
|
9
|
+
}) {
|
|
10
|
+
this.destination = destination;
|
|
11
|
+
this.policy = policy;
|
|
12
|
+
this.pending = [];
|
|
13
|
+
this.queuedBytes = 0;
|
|
14
|
+
this.blocked = false;
|
|
15
|
+
this.finishing = false;
|
|
16
|
+
this.finished = false;
|
|
17
|
+
this.onDrain = () => {
|
|
18
|
+
clearTimeout(this.timer);
|
|
19
|
+
this.timer = undefined;
|
|
20
|
+
this.blocked = false;
|
|
21
|
+
try {
|
|
22
|
+
while (this.pending.length && !this.blocked && !this.failure) {
|
|
23
|
+
const frame = this.pending.shift();
|
|
24
|
+
this.queuedBytes -= Buffer.byteLength(frame);
|
|
25
|
+
this.send(frame);
|
|
26
|
+
}
|
|
27
|
+
if (this.finishing && !this.blocked && !this.failure)
|
|
28
|
+
this.destination.end();
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
this.fail(error instanceof Error ? error : new Error(String(error)));
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
this.onError = (error) => { this.fail(error); };
|
|
35
|
+
this.onClose = () => {
|
|
36
|
+
if (!this.finished)
|
|
37
|
+
this.fail(new Error('sse_closed'));
|
|
38
|
+
this.cleanup();
|
|
39
|
+
};
|
|
40
|
+
this.onFinish = () => {
|
|
41
|
+
if (this.failure)
|
|
42
|
+
return;
|
|
43
|
+
this.finished = true;
|
|
44
|
+
this.cleanup();
|
|
45
|
+
this.resolveCompletion?.();
|
|
46
|
+
};
|
|
47
|
+
if (!Number.isFinite(policy.maxBufferedBytes) || policy.maxBufferedBytes < 1
|
|
48
|
+
|| !Number.isFinite(policy.drainTimeoutMs) || policy.drainTimeoutMs < 1)
|
|
49
|
+
throw new TypeError('Invalid SSE delivery policy');
|
|
50
|
+
destination.on('drain', this.onDrain);
|
|
51
|
+
destination.on('error', this.onError);
|
|
52
|
+
destination.once('close', this.onClose);
|
|
53
|
+
destination.once('finish', this.onFinish);
|
|
54
|
+
}
|
|
55
|
+
get pendingBytes() { return this.failure || this.finished ? 0 : this.queuedBytes + this.destination.writableLength; }
|
|
56
|
+
write(frame) {
|
|
57
|
+
if (this.failure)
|
|
58
|
+
throw this.failure;
|
|
59
|
+
if (this.finishing || this.finished || this.destination.destroyed || this.destination.writableEnded)
|
|
60
|
+
throw new Error('sse_closed');
|
|
61
|
+
const bytes = Buffer.byteLength(frame);
|
|
62
|
+
if (this.pending.length >= 128 || this.pendingBytes + bytes > this.policy.maxBufferedBytes) {
|
|
63
|
+
this.fail(new Error('sse_buffer_limit'));
|
|
64
|
+
throw this.failure;
|
|
65
|
+
}
|
|
66
|
+
if (this.blocked) {
|
|
67
|
+
this.pending.push(frame);
|
|
68
|
+
this.queuedBytes += bytes;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
this.send(frame);
|
|
72
|
+
}
|
|
73
|
+
finish() {
|
|
74
|
+
if (this.failure)
|
|
75
|
+
return Promise.reject(this.failure);
|
|
76
|
+
if (this.finished)
|
|
77
|
+
return Promise.resolve();
|
|
78
|
+
if (this.completion)
|
|
79
|
+
return this.completion;
|
|
80
|
+
this.finishing = true;
|
|
81
|
+
this.completion = new Promise((resolve, reject) => {
|
|
82
|
+
this.resolveCompletion = resolve;
|
|
83
|
+
this.rejectCompletion = reject;
|
|
84
|
+
});
|
|
85
|
+
if (!this.blocked && this.pending.length === 0)
|
|
86
|
+
this.destination.end();
|
|
87
|
+
return this.completion;
|
|
88
|
+
}
|
|
89
|
+
send(frame) {
|
|
90
|
+
try {
|
|
91
|
+
if (!this.destination.write(frame)) {
|
|
92
|
+
this.blocked = true;
|
|
93
|
+
this.timer = setTimeout(() => this.fail(new Error('sse_drain_timeout')), this.policy.drainTimeoutMs);
|
|
94
|
+
this.timer.unref();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
this.fail(error instanceof Error ? error : new Error(String(error)));
|
|
99
|
+
throw this.failure;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
fail(error) {
|
|
103
|
+
if (this.failure || this.finished)
|
|
104
|
+
return;
|
|
105
|
+
this.failure = error;
|
|
106
|
+
this.pending.length = 0;
|
|
107
|
+
this.queuedBytes = 0;
|
|
108
|
+
clearTimeout(this.timer);
|
|
109
|
+
this.destination.removeListener('drain', this.onDrain);
|
|
110
|
+
this.rejectCompletion?.(error);
|
|
111
|
+
// Preserve the error for finish(); destroy without an unhandled second error event.
|
|
112
|
+
this.destination.destroy();
|
|
113
|
+
}
|
|
114
|
+
cleanup() {
|
|
115
|
+
clearTimeout(this.timer);
|
|
116
|
+
this.destination.removeListener('drain', this.onDrain);
|
|
117
|
+
this.destination.removeListener('error', this.onError);
|
|
118
|
+
this.destination.removeListener('close', this.onClose);
|
|
119
|
+
this.destination.removeListener('finish', this.onFinish);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
exports.SseResponseWriter = SseResponseWriter;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const stream_1 = require("stream");
|
|
4
|
+
const SseResponseWriter_1 = require("./SseResponseWriter");
|
|
5
|
+
describe('SSE delivery lifecycle', () => {
|
|
6
|
+
function slowDestination() {
|
|
7
|
+
const frames = [];
|
|
8
|
+
const callbacks = [];
|
|
9
|
+
const destination = new stream_1.Writable({ highWaterMark: 1, write(chunk, _encoding, callback) {
|
|
10
|
+
frames.push(chunk.toString());
|
|
11
|
+
callbacks.push(callback);
|
|
12
|
+
} });
|
|
13
|
+
return { destination, frames, callbacks };
|
|
14
|
+
}
|
|
15
|
+
test('waits for drain, preserves frame order and completes exactly once', async () => {
|
|
16
|
+
const { destination, frames, callbacks } = slowDestination();
|
|
17
|
+
const writer = new SseResponseWriter_1.SseResponseWriter(destination, { maxBufferedBytes: 64, drainTimeoutMs: 1000 });
|
|
18
|
+
const finish = jest.fn();
|
|
19
|
+
destination.on('finish', finish);
|
|
20
|
+
writer.write('first');
|
|
21
|
+
writer.write('second');
|
|
22
|
+
const completed = writer.finish();
|
|
23
|
+
expect(frames).toEqual(['first']);
|
|
24
|
+
callbacks.shift()();
|
|
25
|
+
expect(frames).toEqual(['first', 'second']);
|
|
26
|
+
callbacks.shift()();
|
|
27
|
+
await completed;
|
|
28
|
+
await writer.finish();
|
|
29
|
+
expect(finish).toHaveBeenCalledTimes(1);
|
|
30
|
+
expect(writer.pendingBytes).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
test('bounds pending bytes and destroys an overproducing stream', async () => {
|
|
33
|
+
const { destination } = slowDestination();
|
|
34
|
+
const writer = new SseResponseWriter_1.SseResponseWriter(destination, { maxBufferedBytes: 8, drainTimeoutMs: 1000 });
|
|
35
|
+
writer.write('123456');
|
|
36
|
+
expect(() => writer.write('789')).toThrow('sse_buffer_limit');
|
|
37
|
+
expect(destination.destroyed).toBe(true);
|
|
38
|
+
await expect(writer.finish()).rejects.toThrow('sse_buffer_limit');
|
|
39
|
+
});
|
|
40
|
+
test.each(['close', 'error', 'timeout'])('settles %s while waiting for a slow client', async (cause) => {
|
|
41
|
+
const { destination } = slowDestination();
|
|
42
|
+
const writer = new SseResponseWriter_1.SseResponseWriter(destination, { maxBufferedBytes: 64, drainTimeoutMs: 10 });
|
|
43
|
+
writer.write('first');
|
|
44
|
+
writer.write('second');
|
|
45
|
+
const completed = writer.finish();
|
|
46
|
+
if (cause === 'close')
|
|
47
|
+
destination.destroy();
|
|
48
|
+
if (cause === 'error')
|
|
49
|
+
destination.destroy(new Error('socket_failure'));
|
|
50
|
+
await expect(completed).rejects.toThrow(cause === 'timeout' ? 'sse_drain_timeout' : cause === 'error' ? 'socket_failure' : 'sse_closed');
|
|
51
|
+
expect(writer.pendingBytes).toBe(0);
|
|
52
|
+
expect(destination.listenerCount('drain')).toBe(0);
|
|
53
|
+
});
|
|
54
|
+
});
|