@remnic/cli 1.0.23 → 1.0.25
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/dist/index.js +979 -241
- package/package.json +7 -7
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@ import path11 from "path";
|
|
|
5
5
|
import { createDecipheriv, createHash } from "crypto";
|
|
6
6
|
import * as childProcess2 from "child_process";
|
|
7
7
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
8
|
+
import { gzipSync } from "zlib";
|
|
8
9
|
import {
|
|
9
10
|
parseConfig,
|
|
10
11
|
isOpenaiApiKeyDisabled,
|
|
@@ -91,15 +92,19 @@ import {
|
|
|
91
92
|
OFFLINE_SYNC_APPLY_MAX_BODY_BYTES,
|
|
92
93
|
OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
|
|
93
94
|
OFFLINE_SYNC_FILE_CONTENT_TRANSFER_CHUNK_BYTES,
|
|
95
|
+
OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES,
|
|
96
|
+
applyOfflineSyncFileContentChunk,
|
|
94
97
|
applyOfflineSyncSnapshot,
|
|
95
|
-
|
|
96
|
-
|
|
98
|
+
buildOfflineSyncChangesetFromSnapshot,
|
|
99
|
+
buildOfflineSyncSnapshotFromBase,
|
|
97
100
|
defaultOfflineSyncStatePath,
|
|
98
101
|
normalizeOfflineSyncSnapshot,
|
|
99
102
|
offlineSyncStateFromSnapshot,
|
|
100
103
|
readOfflineSyncFileContentChunk,
|
|
101
104
|
readOfflineSyncState,
|
|
105
|
+
shouldPreferIncomingOfflineRuntimeFile,
|
|
102
106
|
summarizeOfflineSyncPendingChanges,
|
|
107
|
+
summarizeOfflineSyncPendingFiles,
|
|
103
108
|
writeOfflineSyncState,
|
|
104
109
|
buildActionConfidenceInputFromOptions,
|
|
105
110
|
evaluateActionConfidence,
|
|
@@ -6643,29 +6648,230 @@ function offlineEndpoint(remoteUrl, pathname, params = {}) {
|
|
|
6643
6648
|
}
|
|
6644
6649
|
return url.toString();
|
|
6645
6650
|
}
|
|
6651
|
+
var OFFLINE_SYNC_REQUEST_TIMEOUT_DEFAULT_MS = 15 * 6e4;
|
|
6652
|
+
var OFFLINE_SYNC_SNAPSHOT_BASE_POST_PREFERRED_MAX_BODY_BYTES = 16 * 1024 * 1024;
|
|
6653
|
+
function parseOfflineSyncRequestTimeoutMs(raw, fallback = OFFLINE_SYNC_REQUEST_TIMEOUT_DEFAULT_MS) {
|
|
6654
|
+
if (raw === void 0 || raw.trim().length === 0) return fallback;
|
|
6655
|
+
const parsed = Number(raw);
|
|
6656
|
+
if (!Number.isInteger(parsed) || parsed < 1e3) {
|
|
6657
|
+
throw new Error("REMNIC_OFFLINE_REQUEST_TIMEOUT_MS must be an integer >= 1000");
|
|
6658
|
+
}
|
|
6659
|
+
return parsed;
|
|
6660
|
+
}
|
|
6661
|
+
function formatOfflineRequestForError(url, init = {}) {
|
|
6662
|
+
const method = (init.method ?? "GET").toString().toUpperCase();
|
|
6663
|
+
try {
|
|
6664
|
+
const parsed = new URL(url);
|
|
6665
|
+
return `${method} ${parsed.pathname}${parsed.search}`;
|
|
6666
|
+
} catch {
|
|
6667
|
+
return `${method} ${url}`;
|
|
6668
|
+
}
|
|
6669
|
+
}
|
|
6670
|
+
function offlineRequestTimeoutMs() {
|
|
6671
|
+
return parseOfflineSyncRequestTimeoutMs(
|
|
6672
|
+
process.env.REMNIC_OFFLINE_REQUEST_TIMEOUT_MS ?? process.env.ENGRAM_OFFLINE_REQUEST_TIMEOUT_MS
|
|
6673
|
+
);
|
|
6674
|
+
}
|
|
6675
|
+
function offlineFetchHeaders(token, initHeaders, defaultContentType) {
|
|
6676
|
+
const headers = new Headers(initHeaders);
|
|
6677
|
+
headers.set("authorization", `Bearer ${token}`);
|
|
6678
|
+
if (defaultContentType && !headers.has("content-type")) {
|
|
6679
|
+
headers.set("content-type", defaultContentType);
|
|
6680
|
+
}
|
|
6681
|
+
return headers;
|
|
6682
|
+
}
|
|
6683
|
+
async function fetchOfflineWithResponse(url, token, init = {}, options = {}, consume) {
|
|
6684
|
+
const timeoutMs = offlineRequestTimeoutMs();
|
|
6685
|
+
const controller = new AbortController();
|
|
6686
|
+
const upstreamSignal = init.signal;
|
|
6687
|
+
const requestContext = formatOfflineRequestForError(url, init);
|
|
6688
|
+
let didTimeout = false;
|
|
6689
|
+
const timeout = setTimeout(() => {
|
|
6690
|
+
didTimeout = true;
|
|
6691
|
+
controller.abort();
|
|
6692
|
+
}, timeoutMs);
|
|
6693
|
+
const abortFromUpstream = () => controller.abort(upstreamSignal?.reason);
|
|
6694
|
+
if (upstreamSignal) {
|
|
6695
|
+
if (upstreamSignal.aborted) controller.abort(upstreamSignal.reason);
|
|
6696
|
+
else upstreamSignal.addEventListener("abort", abortFromUpstream, { once: true });
|
|
6697
|
+
}
|
|
6698
|
+
let response;
|
|
6699
|
+
try {
|
|
6700
|
+
response = await fetch(url, {
|
|
6701
|
+
...init,
|
|
6702
|
+
signal: controller.signal,
|
|
6703
|
+
headers: offlineFetchHeaders(token, init.headers, options.defaultContentType)
|
|
6704
|
+
});
|
|
6705
|
+
} catch (error) {
|
|
6706
|
+
clearTimeout(timeout);
|
|
6707
|
+
upstreamSignal?.removeEventListener("abort", abortFromUpstream);
|
|
6708
|
+
if (didTimeout) {
|
|
6709
|
+
throw new Error(`offline sync request timed out after ${timeoutMs}ms: ${requestContext}`);
|
|
6710
|
+
}
|
|
6711
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6712
|
+
throw new Error(`offline sync request failed before response: ${requestContext} - ${message}`);
|
|
6713
|
+
}
|
|
6714
|
+
try {
|
|
6715
|
+
return await consume(response);
|
|
6716
|
+
} catch (error) {
|
|
6717
|
+
if (didTimeout) {
|
|
6718
|
+
throw new Error(`offline sync request timed out after ${timeoutMs}ms: ${requestContext}`);
|
|
6719
|
+
}
|
|
6720
|
+
throw error;
|
|
6721
|
+
} finally {
|
|
6722
|
+
clearTimeout(timeout);
|
|
6723
|
+
upstreamSignal?.removeEventListener("abort", abortFromUpstream);
|
|
6724
|
+
}
|
|
6725
|
+
}
|
|
6726
|
+
async function throwOfflineResponseError(response, url, init, label = "offline sync request") {
|
|
6727
|
+
let detail = "";
|
|
6728
|
+
try {
|
|
6729
|
+
detail = await response.text();
|
|
6730
|
+
} catch {
|
|
6731
|
+
detail = "";
|
|
6732
|
+
}
|
|
6733
|
+
throw new Error(
|
|
6734
|
+
`${label} failed: ${formatOfflineRequestForError(url, init)} returned ${response.status} ${response.statusText}${detail ? ` - ${detail.slice(0, 500)}` : ""}`
|
|
6735
|
+
);
|
|
6736
|
+
}
|
|
6646
6737
|
async function fetchOfflineJson(url, token, init = {}) {
|
|
6647
|
-
|
|
6648
|
-
|
|
6649
|
-
|
|
6650
|
-
|
|
6651
|
-
|
|
6652
|
-
|
|
6738
|
+
return fetchOfflineWithResponse(
|
|
6739
|
+
url,
|
|
6740
|
+
token,
|
|
6741
|
+
init,
|
|
6742
|
+
{ defaultContentType: init.body !== void 0 ? "application/json" : void 0 },
|
|
6743
|
+
async (response) => {
|
|
6744
|
+
if (!response.ok) {
|
|
6745
|
+
await throwOfflineResponseError(response, url, init);
|
|
6746
|
+
}
|
|
6747
|
+
return await response.json();
|
|
6748
|
+
}
|
|
6749
|
+
);
|
|
6750
|
+
}
|
|
6751
|
+
async function parseOfflineSnapshotStreamResponse(response) {
|
|
6752
|
+
if (!response.body) {
|
|
6753
|
+
throw new Error("offline sync snapshot stream response omitted body");
|
|
6754
|
+
}
|
|
6755
|
+
const reader = response.body.getReader();
|
|
6756
|
+
const decoder = new TextDecoder();
|
|
6757
|
+
let buffered = "";
|
|
6758
|
+
let header = null;
|
|
6759
|
+
const files = [];
|
|
6760
|
+
const handleLine = (line) => {
|
|
6761
|
+
if (line.trim().length === 0) return;
|
|
6762
|
+
const parsed = JSON.parse(line);
|
|
6763
|
+
if (parsed.type === "snapshot") {
|
|
6764
|
+
if (header) throw new Error("offline sync snapshot stream repeated header");
|
|
6765
|
+
header = {
|
|
6766
|
+
...typeof parsed.namespace === "string" && parsed.namespace.length > 0 ? { namespace: parsed.namespace } : {},
|
|
6767
|
+
format: parsed.format,
|
|
6768
|
+
schemaVersion: parsed.schemaVersion,
|
|
6769
|
+
createdAt: parsed.createdAt,
|
|
6770
|
+
sourceId: parsed.sourceId,
|
|
6771
|
+
includeTranscripts: parsed.includeTranscripts
|
|
6772
|
+
};
|
|
6773
|
+
return;
|
|
6653
6774
|
}
|
|
6775
|
+
if (parsed.type === "file") {
|
|
6776
|
+
if (!header) throw new Error("offline sync snapshot stream file arrived before header");
|
|
6777
|
+
files.push(parsed.file);
|
|
6778
|
+
return;
|
|
6779
|
+
}
|
|
6780
|
+
throw new Error("offline sync snapshot stream contained unknown event");
|
|
6781
|
+
};
|
|
6782
|
+
for (; ; ) {
|
|
6783
|
+
const { value, done } = await reader.read();
|
|
6784
|
+
if (done) break;
|
|
6785
|
+
buffered += decoder.decode(value, { stream: true });
|
|
6786
|
+
for (; ; ) {
|
|
6787
|
+
const newline = buffered.indexOf("\n");
|
|
6788
|
+
if (newline < 0) break;
|
|
6789
|
+
const line = buffered.slice(0, newline);
|
|
6790
|
+
buffered = buffered.slice(newline + 1);
|
|
6791
|
+
handleLine(line);
|
|
6792
|
+
}
|
|
6793
|
+
}
|
|
6794
|
+
buffered += decoder.decode();
|
|
6795
|
+
handleLine(buffered);
|
|
6796
|
+
const finalHeader = header;
|
|
6797
|
+
if (!finalHeader) throw new Error("offline sync snapshot stream omitted header");
|
|
6798
|
+
const snapshot = normalizeOfflineSyncSnapshot({
|
|
6799
|
+
format: finalHeader.format,
|
|
6800
|
+
schemaVersion: finalHeader.schemaVersion,
|
|
6801
|
+
createdAt: finalHeader.createdAt,
|
|
6802
|
+
sourceId: finalHeader.sourceId,
|
|
6803
|
+
includeTranscripts: finalHeader.includeTranscripts,
|
|
6804
|
+
files
|
|
6654
6805
|
});
|
|
6655
|
-
|
|
6656
|
-
|
|
6657
|
-
|
|
6658
|
-
|
|
6659
|
-
|
|
6660
|
-
|
|
6806
|
+
return {
|
|
6807
|
+
...finalHeader.namespace ? { namespace: finalHeader.namespace } : {},
|
|
6808
|
+
...snapshot
|
|
6809
|
+
};
|
|
6810
|
+
}
|
|
6811
|
+
async function fetchOfflineSnapshotStream(args) {
|
|
6812
|
+
const url = offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/snapshot-stream", {
|
|
6813
|
+
namespace: args.namespace,
|
|
6814
|
+
include_transcripts: args.includeTranscripts ? "true" : "false",
|
|
6815
|
+
content: "false"
|
|
6816
|
+
});
|
|
6817
|
+
return fetchOfflineWithResponse(
|
|
6818
|
+
url,
|
|
6819
|
+
args.token,
|
|
6820
|
+
{},
|
|
6821
|
+
{},
|
|
6822
|
+
async (response) => {
|
|
6823
|
+
if (!response.ok) {
|
|
6824
|
+
await throwOfflineResponseError(response, url, {}, "offline sync snapshot-stream request");
|
|
6825
|
+
}
|
|
6826
|
+
return parseOfflineSnapshotStreamResponse(response);
|
|
6661
6827
|
}
|
|
6662
|
-
|
|
6663
|
-
`offline sync request failed: ${response.status} ${response.statusText}${detail ? ` - ${detail.slice(0, 500)}` : ""}`
|
|
6664
|
-
);
|
|
6665
|
-
}
|
|
6666
|
-
return await response.json();
|
|
6828
|
+
);
|
|
6667
6829
|
}
|
|
6668
6830
|
async function fetchOfflineSnapshot(args) {
|
|
6831
|
+
let tryStreamSnapshot = false;
|
|
6832
|
+
if (args.includeContent === false && args.baseFiles && args.baseFiles.length > 0) {
|
|
6833
|
+
if (args.baseFiles.length > OFFLINE_SYNC_SNAPSHOT_BASE_POST_MAX_FILES) {
|
|
6834
|
+
tryStreamSnapshot = true;
|
|
6835
|
+
} else {
|
|
6836
|
+
const postBody = offlineSnapshotBasePostBody({
|
|
6837
|
+
namespace: args.namespace,
|
|
6838
|
+
includeTranscripts: args.includeTranscripts,
|
|
6839
|
+
baseFiles: args.baseFiles,
|
|
6840
|
+
baseCapturedAt: args.baseCapturedAt
|
|
6841
|
+
});
|
|
6842
|
+
const postRequest = offlineSnapshotBasePostRequest(postBody);
|
|
6843
|
+
if (postRequest) {
|
|
6844
|
+
const postRequestUsesGzip = new Headers(postRequest.headers).get("content-encoding")?.toLowerCase() === "gzip";
|
|
6845
|
+
try {
|
|
6846
|
+
return await fetchOfflineJson(
|
|
6847
|
+
offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/snapshot"),
|
|
6848
|
+
args.token,
|
|
6849
|
+
{
|
|
6850
|
+
method: "POST",
|
|
6851
|
+
...postRequest
|
|
6852
|
+
}
|
|
6853
|
+
);
|
|
6854
|
+
} catch (error) {
|
|
6855
|
+
if (!isOfflineSnapshotPostFallbackError(error, { compressed: postRequestUsesGzip })) throw error;
|
|
6856
|
+
tryStreamSnapshot = true;
|
|
6857
|
+
}
|
|
6858
|
+
} else {
|
|
6859
|
+
tryStreamSnapshot = true;
|
|
6860
|
+
}
|
|
6861
|
+
}
|
|
6862
|
+
}
|
|
6863
|
+
if (tryStreamSnapshot) {
|
|
6864
|
+
try {
|
|
6865
|
+
return await fetchOfflineSnapshotStream({
|
|
6866
|
+
remoteUrl: args.remoteUrl,
|
|
6867
|
+
token: args.token,
|
|
6868
|
+
namespace: args.namespace,
|
|
6869
|
+
includeTranscripts: args.includeTranscripts
|
|
6870
|
+
});
|
|
6871
|
+
} catch (error) {
|
|
6872
|
+
if (!isOfflineSnapshotStreamFallbackError(error)) throw error;
|
|
6873
|
+
}
|
|
6874
|
+
}
|
|
6669
6875
|
return fetchOfflineJson(
|
|
6670
6876
|
offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/snapshot", {
|
|
6671
6877
|
namespace: args.namespace,
|
|
@@ -6675,6 +6881,47 @@ async function fetchOfflineSnapshot(args) {
|
|
|
6675
6881
|
args.token
|
|
6676
6882
|
);
|
|
6677
6883
|
}
|
|
6884
|
+
function offlineSnapshotBasePostBody(args) {
|
|
6885
|
+
return JSON.stringify({
|
|
6886
|
+
namespace: args.namespace,
|
|
6887
|
+
includeTranscripts: args.includeTranscripts,
|
|
6888
|
+
includeContent: false,
|
|
6889
|
+
baseFiles: args.baseFiles,
|
|
6890
|
+
...args.baseCapturedAt ? { baseCapturedAt: args.baseCapturedAt.toISOString() } : {}
|
|
6891
|
+
});
|
|
6892
|
+
}
|
|
6893
|
+
function offlineSnapshotBasePostBodyFits(body) {
|
|
6894
|
+
const bytes = Buffer.byteLength(body, "utf-8");
|
|
6895
|
+
return bytes <= OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES && bytes <= OFFLINE_SYNC_SNAPSHOT_BASE_POST_PREFERRED_MAX_BODY_BYTES;
|
|
6896
|
+
}
|
|
6897
|
+
var OFFLINE_SYNC_SNAPSHOT_BASE_POST_MAX_FILES = 5e4;
|
|
6898
|
+
function offlineSnapshotBasePostRequest(body) {
|
|
6899
|
+
const bytes = Buffer.byteLength(body, "utf-8");
|
|
6900
|
+
if (bytes > OFFLINE_SYNC_SNAPSHOT_BASE_MAX_BODY_BYTES) return null;
|
|
6901
|
+
if (bytes <= OFFLINE_SYNC_SNAPSHOT_BASE_POST_PREFERRED_MAX_BODY_BYTES) {
|
|
6902
|
+
return { body };
|
|
6903
|
+
}
|
|
6904
|
+
const compressed = gzipSync(body);
|
|
6905
|
+
if (compressed.byteLength > OFFLINE_SYNC_SNAPSHOT_BASE_POST_PREFERRED_MAX_BODY_BYTES) {
|
|
6906
|
+
return null;
|
|
6907
|
+
}
|
|
6908
|
+
return {
|
|
6909
|
+
body: compressed,
|
|
6910
|
+
headers: {
|
|
6911
|
+
"content-encoding": "gzip"
|
|
6912
|
+
}
|
|
6913
|
+
};
|
|
6914
|
+
}
|
|
6915
|
+
function isOfflineSnapshotPostFallbackError(error, options = {}) {
|
|
6916
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6917
|
+
if (/offline-sync\/snapshot\b.* returned (404|405|413)\b/.test(message)) return true;
|
|
6918
|
+
if (!options.compressed) return false;
|
|
6919
|
+
return /offline-sync\/snapshot\b.* returned (400|415)\b/.test(message) && /\b(unsupported_content_encoding|invalid_gzip_body|invalid_json)\b/.test(message);
|
|
6920
|
+
}
|
|
6921
|
+
function isOfflineSnapshotStreamFallbackError(error) {
|
|
6922
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6923
|
+
return /offline-sync\/snapshot-stream\b.* returned (404|405)\b/.test(message);
|
|
6924
|
+
}
|
|
6678
6925
|
async function fetchOfflineFiles(args) {
|
|
6679
6926
|
return fetchOfflineJson(
|
|
6680
6927
|
offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/files"),
|
|
@@ -6702,6 +6949,28 @@ var OFFLINE_SYNC_DIRECT_PUSH_MIN_BYTES = Math.min(
|
|
|
6702
6949
|
OFFLINE_SYNC_INLINE_CONTENT_MAX_BYTES
|
|
6703
6950
|
);
|
|
6704
6951
|
var OFFLINE_SYNC_DIRECT_HYDRATE_MIN_BYTES = OFFLINE_SYNC_DIRECT_PUSH_MIN_BYTES;
|
|
6952
|
+
var OFFLINE_SYNC_CHANGESET_RETRY_MAX = 1024;
|
|
6953
|
+
var OfflineRemoteFileChangedError = class extends Error {
|
|
6954
|
+
path;
|
|
6955
|
+
constructor(path12) {
|
|
6956
|
+
super(`remote file changed while fetching offline content: ${path12}`);
|
|
6957
|
+
this.name = "OfflineRemoteFileChangedError";
|
|
6958
|
+
this.path = path12;
|
|
6959
|
+
}
|
|
6960
|
+
};
|
|
6961
|
+
function isOfflineRemoteFileChangedError(error) {
|
|
6962
|
+
return error instanceof OfflineRemoteFileChangedError || error instanceof Error && error.message.startsWith("remote file changed while fetching offline content: ");
|
|
6963
|
+
}
|
|
6964
|
+
function isOfflineLocalFileChangedError(error) {
|
|
6965
|
+
return error instanceof Error && error.message.startsWith("local file changed while pushing offline content: ");
|
|
6966
|
+
}
|
|
6967
|
+
function offlineChangesetFileChangedPath(error) {
|
|
6968
|
+
if (!(error instanceof Error)) return null;
|
|
6969
|
+
const prefix = "offline sync file changed while building changeset: ";
|
|
6970
|
+
if (!error.message.startsWith(prefix)) return null;
|
|
6971
|
+
const relPath = error.message.slice(prefix.length).trim();
|
|
6972
|
+
return relPath.length > 0 ? relPath : null;
|
|
6973
|
+
}
|
|
6705
6974
|
function parseOfflineHeaderNumber(headers, name) {
|
|
6706
6975
|
const raw = headers.get(name);
|
|
6707
6976
|
if (raw === null) throw new Error(`offline file content response omitted ${name}`);
|
|
@@ -6712,86 +6981,77 @@ function parseOfflineHeaderNumber(headers, name) {
|
|
|
6712
6981
|
return parsed;
|
|
6713
6982
|
}
|
|
6714
6983
|
async function fetchOfflineFileContentChunk(args) {
|
|
6715
|
-
const
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
|
|
6729
|
-
|
|
6984
|
+
const url = offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/file-content");
|
|
6985
|
+
const init = {
|
|
6986
|
+
method: "POST",
|
|
6987
|
+
body: JSON.stringify({
|
|
6988
|
+
namespace: args.namespace,
|
|
6989
|
+
includeTranscripts: args.includeTranscripts,
|
|
6990
|
+
path: args.path,
|
|
6991
|
+
offset: args.offset,
|
|
6992
|
+
length: args.length
|
|
6993
|
+
})
|
|
6994
|
+
};
|
|
6995
|
+
return fetchOfflineWithResponse(
|
|
6996
|
+
url,
|
|
6997
|
+
args.token,
|
|
6998
|
+
init,
|
|
6999
|
+
{ defaultContentType: "application/json" },
|
|
7000
|
+
async (response) => {
|
|
7001
|
+
if (!response.ok) {
|
|
7002
|
+
await throwOfflineResponseError(response, url, init, "offline sync file-content request");
|
|
7003
|
+
}
|
|
7004
|
+
const encodedPath = response.headers.get("x-remnic-file-path");
|
|
7005
|
+
const relPath = encodedPath ? decodeURIComponent(encodedPath) : args.path;
|
|
7006
|
+
const content = Buffer.from(await response.arrayBuffer());
|
|
7007
|
+
const chunkBytes = parseOfflineHeaderNumber(response.headers, "x-remnic-chunk-bytes");
|
|
7008
|
+
const sha256 = response.headers.get("x-remnic-file-sha256") ?? void 0;
|
|
7009
|
+
if (content.length !== chunkBytes) {
|
|
7010
|
+
throw new Error(`offline file content response length mismatch for ${relPath}`);
|
|
7011
|
+
}
|
|
7012
|
+
return {
|
|
7013
|
+
path: relPath,
|
|
7014
|
+
...sha256 ? { sha256 } : {},
|
|
7015
|
+
bytes: parseOfflineHeaderNumber(response.headers, "x-remnic-file-bytes"),
|
|
7016
|
+
mtimeMs: parseOfflineHeaderNumber(response.headers, "x-remnic-file-mtime-ms"),
|
|
7017
|
+
offset: parseOfflineHeaderNumber(response.headers, "x-remnic-chunk-offset"),
|
|
7018
|
+
chunkBytes,
|
|
7019
|
+
content
|
|
7020
|
+
};
|
|
6730
7021
|
}
|
|
6731
7022
|
);
|
|
6732
|
-
if (!response.ok) {
|
|
6733
|
-
let detail = "";
|
|
6734
|
-
try {
|
|
6735
|
-
detail = await response.text();
|
|
6736
|
-
} catch {
|
|
6737
|
-
detail = "";
|
|
6738
|
-
}
|
|
6739
|
-
throw new Error(
|
|
6740
|
-
`offline sync file-content request failed: ${response.status} ${response.statusText}${detail ? ` - ${detail.slice(0, 500)}` : ""}`
|
|
6741
|
-
);
|
|
6742
|
-
}
|
|
6743
|
-
const encodedPath = response.headers.get("x-remnic-file-path");
|
|
6744
|
-
const relPath = encodedPath ? decodeURIComponent(encodedPath) : args.path;
|
|
6745
|
-
const content = Buffer.from(await response.arrayBuffer());
|
|
6746
|
-
const chunkBytes = parseOfflineHeaderNumber(response.headers, "x-remnic-chunk-bytes");
|
|
6747
|
-
const sha256 = response.headers.get("x-remnic-file-sha256") ?? void 0;
|
|
6748
|
-
if (content.length !== chunkBytes) {
|
|
6749
|
-
throw new Error(`offline file content response length mismatch for ${relPath}`);
|
|
6750
|
-
}
|
|
6751
|
-
return {
|
|
6752
|
-
path: relPath,
|
|
6753
|
-
...sha256 ? { sha256 } : {},
|
|
6754
|
-
bytes: parseOfflineHeaderNumber(response.headers, "x-remnic-file-bytes"),
|
|
6755
|
-
mtimeMs: parseOfflineHeaderNumber(response.headers, "x-remnic-file-mtime-ms"),
|
|
6756
|
-
offset: parseOfflineHeaderNumber(response.headers, "x-remnic-chunk-offset"),
|
|
6757
|
-
chunkBytes,
|
|
6758
|
-
content
|
|
6759
|
-
};
|
|
6760
7023
|
}
|
|
6761
7024
|
async function postOfflineFileContentChunk(args) {
|
|
6762
|
-
const
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
|
|
6769
|
-
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
7025
|
+
const url = offlineEndpoint(args.remoteUrl, "/remnic/v1/offline-sync/apply-file-content", {
|
|
7026
|
+
namespace: args.namespace
|
|
7027
|
+
});
|
|
7028
|
+
const init = {
|
|
7029
|
+
method: "POST",
|
|
7030
|
+
headers: {
|
|
7031
|
+
"content-type": "application/octet-stream",
|
|
7032
|
+
"x-remnic-include-transcripts": args.includeTranscripts ? "true" : "false",
|
|
7033
|
+
"x-remnic-source-id": encodeURIComponent(args.sourceId),
|
|
7034
|
+
"x-remnic-file-path": encodeURIComponent(args.file.path),
|
|
7035
|
+
"x-remnic-file-sha256": args.file.sha256,
|
|
7036
|
+
"x-remnic-file-bytes": String(args.file.bytes),
|
|
7037
|
+
"x-remnic-file-mtime-ms": String(args.file.mtimeMs),
|
|
7038
|
+
"x-remnic-chunk-offset": String(args.offset),
|
|
7039
|
+
...args.baseSha256 ? { "x-remnic-base-sha256": args.baseSha256 } : {}
|
|
7040
|
+
},
|
|
7041
|
+
body: new Blob([new Uint8Array(args.content)])
|
|
7042
|
+
};
|
|
7043
|
+
return fetchOfflineWithResponse(
|
|
7044
|
+
url,
|
|
7045
|
+
args.token,
|
|
7046
|
+
init,
|
|
7047
|
+
{},
|
|
7048
|
+
async (response) => {
|
|
7049
|
+
if (!response.ok) {
|
|
7050
|
+
await throwOfflineResponseError(response, url, init, "offline sync apply-file-content request");
|
|
7051
|
+
}
|
|
7052
|
+
return await response.json();
|
|
6781
7053
|
}
|
|
6782
7054
|
);
|
|
6783
|
-
if (!response.ok) {
|
|
6784
|
-
let detail = "";
|
|
6785
|
-
try {
|
|
6786
|
-
detail = await response.text();
|
|
6787
|
-
} catch {
|
|
6788
|
-
detail = "";
|
|
6789
|
-
}
|
|
6790
|
-
throw new Error(
|
|
6791
|
-
`offline sync apply-file-content request failed: ${response.status} ${response.statusText}${detail ? ` - ${detail.slice(0, 500)}` : ""}`
|
|
6792
|
-
);
|
|
6793
|
-
}
|
|
6794
|
-
return await response.json();
|
|
6795
7055
|
}
|
|
6796
7056
|
function resolvedOfflineSnapshotNamespace(snapshot, requestedNamespace) {
|
|
6797
7057
|
const resolved = typeof snapshot.namespace === "string" && snapshot.namespace.trim().length > 0 ? snapshot.namespace.trim() : void 0;
|
|
@@ -6831,10 +7091,29 @@ function offlineFileStateMap(files) {
|
|
|
6831
7091
|
function offlineSnapshotContentFilesForApply(options) {
|
|
6832
7092
|
const base = offlineFileStateMap(options.baseFiles);
|
|
6833
7093
|
const current = options.currentFiles ? offlineFileStateMap(options.currentFiles) : null;
|
|
7094
|
+
const conflictContentMaxBytes = options.conflictContentMaxBytes ?? Number.POSITIVE_INFINITY;
|
|
7095
|
+
const deferredPaths = new Set(options.deferredPaths ?? []);
|
|
6834
7096
|
const files = [];
|
|
6835
7097
|
for (const incoming of options.snapshot.files) {
|
|
6836
|
-
if (
|
|
6837
|
-
|
|
7098
|
+
if (deferredPaths.has(incoming.path)) continue;
|
|
7099
|
+
const baseEntry = base.get(incoming.path);
|
|
7100
|
+
const currentEntry = current?.get(incoming.path);
|
|
7101
|
+
if (currentEntry?.sha256 === incoming.sha256) continue;
|
|
7102
|
+
if (currentEntry && baseEntry && incoming.sha256 === baseEntry.sha256) continue;
|
|
7103
|
+
if (shouldPreferIncomingOfflineRuntimeFile(incoming.path)) {
|
|
7104
|
+
files.push(incoming);
|
|
7105
|
+
continue;
|
|
7106
|
+
}
|
|
7107
|
+
if (!currentEntry && baseEntry && incoming.sha256 === baseEntry.sha256) continue;
|
|
7108
|
+
if (!currentEntry && !baseEntry) {
|
|
7109
|
+
files.push(incoming);
|
|
7110
|
+
continue;
|
|
7111
|
+
}
|
|
7112
|
+
if (baseEntry && currentEntry && currentEntry.sha256 === baseEntry.sha256) {
|
|
7113
|
+
files.push(incoming);
|
|
7114
|
+
continue;
|
|
7115
|
+
}
|
|
7116
|
+
if (incoming.bytes > conflictContentMaxBytes) continue;
|
|
6838
7117
|
files.push(incoming);
|
|
6839
7118
|
}
|
|
6840
7119
|
return files.sort((left, right) => left.path.localeCompare(right.path));
|
|
@@ -6842,15 +7121,24 @@ function offlineSnapshotContentFilesForApply(options) {
|
|
|
6842
7121
|
function shouldDirectHydrateOfflineFile(options) {
|
|
6843
7122
|
if (options.incoming.bytes < OFFLINE_SYNC_DIRECT_HYDRATE_MIN_BYTES) return false;
|
|
6844
7123
|
if (options.current?.sha256 === options.incoming.sha256) return false;
|
|
7124
|
+
if (shouldPreferIncomingOfflineRuntimeFile(options.incoming.path)) return true;
|
|
6845
7125
|
if (options.current && options.base && options.current.sha256 === options.base.sha256) {
|
|
6846
7126
|
return true;
|
|
6847
7127
|
}
|
|
6848
7128
|
return !options.current && !options.base;
|
|
6849
7129
|
}
|
|
7130
|
+
function offlinePartialHydrationForPaths(options) {
|
|
7131
|
+
const hydratedPaths = new Set(options.hydratedPaths);
|
|
7132
|
+
return {
|
|
7133
|
+
hydratedFiles: options.files.filter((file) => hydratedPaths.has(file.path)),
|
|
7134
|
+
remoteDeferredPaths: [...options.deferredPaths]
|
|
7135
|
+
};
|
|
7136
|
+
}
|
|
6850
7137
|
function offlineDirectPushFiles(options) {
|
|
6851
7138
|
const base = offlineFileStateMap(options.baseFiles);
|
|
6852
7139
|
return options.currentFiles.filter((current) => {
|
|
6853
7140
|
if (current.bytes < OFFLINE_SYNC_DIRECT_PUSH_MIN_BYTES) return false;
|
|
7141
|
+
if (shouldPreferIncomingOfflineRuntimeFile(current.path)) return false;
|
|
6854
7142
|
return current.sha256 !== base.get(current.path)?.sha256;
|
|
6855
7143
|
}).sort((left, right) => right.bytes - left.bytes || left.path.localeCompare(right.path));
|
|
6856
7144
|
}
|
|
@@ -6870,6 +7158,9 @@ async function pushOfflineFileContent(args) {
|
|
|
6870
7158
|
}
|
|
6871
7159
|
let offset = 0;
|
|
6872
7160
|
let finalResult = null;
|
|
7161
|
+
let remoteSatisfiedResult = null;
|
|
7162
|
+
const hash = createHash("sha256");
|
|
7163
|
+
let bytes = 0;
|
|
6873
7164
|
while (offset < args.file.bytes || args.file.bytes === 0 && offset === 0) {
|
|
6874
7165
|
const chunk = await readOfflineSyncFileContentChunk({
|
|
6875
7166
|
root: args.memoryDir,
|
|
@@ -6888,23 +7179,36 @@ async function pushOfflineFileContent(args) {
|
|
|
6888
7179
|
if (chunk.chunkBytes === 0 && args.file.bytes > 0) {
|
|
6889
7180
|
throw new Error(`local offline content chunk was empty before EOF: ${args.file.path}`);
|
|
6890
7181
|
}
|
|
6891
|
-
|
|
6892
|
-
|
|
6893
|
-
|
|
6894
|
-
|
|
6895
|
-
|
|
6896
|
-
|
|
6897
|
-
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
7182
|
+
hash.update(chunk.content);
|
|
7183
|
+
bytes += chunk.chunkBytes;
|
|
7184
|
+
if (!remoteSatisfiedResult) {
|
|
7185
|
+
finalResult = await postOfflineFileContentChunk({
|
|
7186
|
+
remoteUrl: args.remoteUrl,
|
|
7187
|
+
token: args.token,
|
|
7188
|
+
namespace: args.namespace,
|
|
7189
|
+
includeTranscripts: args.includeTranscripts,
|
|
7190
|
+
sourceId: args.sourceId,
|
|
7191
|
+
file: args.file,
|
|
7192
|
+
baseSha256: args.baseSha256,
|
|
7193
|
+
offset,
|
|
7194
|
+
content: chunk.content
|
|
7195
|
+
});
|
|
7196
|
+
if (finalResult.conflict) {
|
|
7197
|
+
return finalResult;
|
|
7198
|
+
}
|
|
7199
|
+
if (finalResult.done && finalResult.skipped) {
|
|
7200
|
+
remoteSatisfiedResult = finalResult;
|
|
7201
|
+
}
|
|
6904
7202
|
}
|
|
6905
7203
|
offset += chunk.chunkBytes;
|
|
6906
7204
|
if (args.file.bytes === 0) break;
|
|
6907
7205
|
}
|
|
7206
|
+
if (hash.digest("hex") !== args.file.sha256 || bytes !== args.file.bytes) {
|
|
7207
|
+
throw new Error(`local file changed while pushing offline content: ${args.file.path}`);
|
|
7208
|
+
}
|
|
7209
|
+
if (remoteSatisfiedResult) {
|
|
7210
|
+
return remoteSatisfiedResult;
|
|
7211
|
+
}
|
|
6908
7212
|
if (!finalResult?.done) {
|
|
6909
7213
|
throw new Error(`offline sync large-file push did not finish for ${args.file.path}`);
|
|
6910
7214
|
}
|
|
@@ -6926,6 +7230,7 @@ async function pushOfflineFileContentFromChunkReader(args) {
|
|
|
6926
7230
|
let offset = 0;
|
|
6927
7231
|
let pending = null;
|
|
6928
7232
|
let finalResult = null;
|
|
7233
|
+
let remoteSatisfiedResult = null;
|
|
6929
7234
|
for await (const rawChunk of chunks) {
|
|
6930
7235
|
const chunk = Buffer.from(rawChunk);
|
|
6931
7236
|
if (chunk.length === 0) continue;
|
|
@@ -6934,19 +7239,24 @@ async function pushOfflineFileContentFromChunkReader(args) {
|
|
|
6934
7239
|
}
|
|
6935
7240
|
if (pending) {
|
|
6936
7241
|
hash.update(pending);
|
|
6937
|
-
|
|
6938
|
-
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
|
|
7242
|
+
if (!remoteSatisfiedResult) {
|
|
7243
|
+
finalResult = await postOfflineFileContentChunk({
|
|
7244
|
+
remoteUrl: args.remoteUrl,
|
|
7245
|
+
token: args.token,
|
|
7246
|
+
namespace: args.namespace,
|
|
7247
|
+
includeTranscripts: args.includeTranscripts,
|
|
7248
|
+
sourceId: args.sourceId,
|
|
7249
|
+
file: args.file,
|
|
7250
|
+
baseSha256: args.baseSha256,
|
|
7251
|
+
offset,
|
|
7252
|
+
content: pending
|
|
7253
|
+
});
|
|
7254
|
+
if (finalResult.conflict) {
|
|
7255
|
+
return finalResult;
|
|
7256
|
+
}
|
|
7257
|
+
if (finalResult.done && finalResult.skipped) {
|
|
7258
|
+
remoteSatisfiedResult = finalResult;
|
|
7259
|
+
}
|
|
6950
7260
|
}
|
|
6951
7261
|
offset += pending.length;
|
|
6952
7262
|
}
|
|
@@ -6958,6 +7268,9 @@ async function pushOfflineFileContentFromChunkReader(args) {
|
|
|
6958
7268
|
if (digest !== args.file.sha256 || finalBytes !== args.file.bytes) {
|
|
6959
7269
|
throw new Error(`local file changed while pushing offline content: ${args.file.path}`);
|
|
6960
7270
|
}
|
|
7271
|
+
if (remoteSatisfiedResult) {
|
|
7272
|
+
return remoteSatisfiedResult;
|
|
7273
|
+
}
|
|
6961
7274
|
if (pending) {
|
|
6962
7275
|
finalResult = await postOfflineFileContentChunk({
|
|
6963
7276
|
remoteUrl: args.remoteUrl,
|
|
@@ -6973,6 +7286,9 @@ async function pushOfflineFileContentFromChunkReader(args) {
|
|
|
6973
7286
|
if (finalResult.conflict) {
|
|
6974
7287
|
return finalResult;
|
|
6975
7288
|
}
|
|
7289
|
+
if (finalResult.done && finalResult.skipped) {
|
|
7290
|
+
return finalResult;
|
|
7291
|
+
}
|
|
6976
7292
|
} else if (args.file.bytes === 0) {
|
|
6977
7293
|
finalResult = await postOfflineFileContentChunk({
|
|
6978
7294
|
remoteUrl: args.remoteUrl,
|
|
@@ -6991,11 +7307,10 @@ async function pushOfflineFileContentFromChunkReader(args) {
|
|
|
6991
7307
|
}
|
|
6992
7308
|
return finalResult;
|
|
6993
7309
|
}
|
|
6994
|
-
async function
|
|
6995
|
-
const chunks = [];
|
|
6996
|
-
const hash = createHash("sha256");
|
|
7310
|
+
async function hydrateOfflineFileContent(args) {
|
|
6997
7311
|
let offset = 0;
|
|
6998
|
-
|
|
7312
|
+
let finalResult = null;
|
|
7313
|
+
while (offset < args.expected.bytes || args.expected.bytes === 0 && offset === 0) {
|
|
6999
7314
|
const chunk = await fetchOfflineFileContentChunk({
|
|
7000
7315
|
remoteUrl: args.remoteUrl,
|
|
7001
7316
|
token: args.token,
|
|
@@ -7005,54 +7320,92 @@ async function fetchOfflineFileContent(args) {
|
|
|
7005
7320
|
offset,
|
|
7006
7321
|
length: Math.min(
|
|
7007
7322
|
OFFLINE_SYNC_FILE_CONTENT_MAX_CHUNK_BYTES,
|
|
7008
|
-
args.expected.bytes - offset
|
|
7323
|
+
Math.max(1, args.expected.bytes - offset)
|
|
7009
7324
|
)
|
|
7010
7325
|
});
|
|
7011
7326
|
if (chunk.path !== args.expected.path || chunk.sha256 !== void 0 && chunk.sha256 !== args.expected.sha256 || chunk.bytes !== args.expected.bytes || chunk.mtimeMs !== args.expected.mtimeMs || chunk.offset !== offset || chunk.chunkBytes !== chunk.content.length) {
|
|
7012
|
-
throw new
|
|
7327
|
+
throw new OfflineRemoteFileChangedError(args.expected.path);
|
|
7013
7328
|
}
|
|
7014
|
-
if (chunk.chunkBytes === 0) {
|
|
7329
|
+
if (chunk.chunkBytes === 0 && args.expected.bytes > 0) {
|
|
7015
7330
|
throw new Error(`remote offline content chunk was empty before EOF: ${args.expected.path}`);
|
|
7016
7331
|
}
|
|
7017
|
-
|
|
7018
|
-
|
|
7332
|
+
finalResult = await applyOfflineSyncFileContentChunk({
|
|
7333
|
+
root: args.memoryDir,
|
|
7334
|
+
sourceId: args.sourceId,
|
|
7335
|
+
path: args.expected.path,
|
|
7336
|
+
sha256: args.expected.sha256,
|
|
7337
|
+
bytes: args.expected.bytes,
|
|
7338
|
+
mtimeMs: args.expected.mtimeMs,
|
|
7339
|
+
offset,
|
|
7340
|
+
content: chunk.content,
|
|
7341
|
+
...args.baseSha256 ? { baseSha256: args.baseSha256 } : {},
|
|
7342
|
+
includeTranscripts: args.includeTranscripts,
|
|
7343
|
+
readFile: args.readFile,
|
|
7344
|
+
readFileDigest: args.readFileDigest,
|
|
7345
|
+
writeFile: args.writeFile,
|
|
7346
|
+
writeStagingFile: args.writeStagingFile,
|
|
7347
|
+
writeFileChunks: args.writeFileChunks
|
|
7348
|
+
});
|
|
7349
|
+
if (finalResult.conflict) {
|
|
7350
|
+
return finalResult;
|
|
7351
|
+
}
|
|
7352
|
+
if (finalResult.done && finalResult.skipped) {
|
|
7353
|
+
return finalResult;
|
|
7354
|
+
}
|
|
7019
7355
|
offset += chunk.chunkBytes;
|
|
7356
|
+
if (args.expected.bytes === 0) break;
|
|
7020
7357
|
}
|
|
7021
|
-
|
|
7022
|
-
|
|
7023
|
-
if (digest !== args.expected.sha256 || content.length !== args.expected.bytes) {
|
|
7024
|
-
throw new Error(`remote offline content checksum mismatch for ${args.expected.path}`);
|
|
7358
|
+
if (!finalResult?.done) {
|
|
7359
|
+
throw new Error(`offline sync large-file hydrate did not finish for ${args.expected.path}`);
|
|
7025
7360
|
}
|
|
7026
|
-
return
|
|
7361
|
+
return finalResult;
|
|
7027
7362
|
}
|
|
7028
7363
|
async function directHydrateLargeOfflineFiles(args) {
|
|
7029
|
-
if (!args.
|
|
7364
|
+
if (!args.readFile || !args.writeFile || !args.writeStagingFile || !args.writeFileChunks) {
|
|
7365
|
+
return { hydratedPaths: /* @__PURE__ */ new Set(), deferredPaths: /* @__PURE__ */ new Set() };
|
|
7366
|
+
}
|
|
7030
7367
|
const snapshot = normalizeOfflineSyncSnapshot(args.snapshot);
|
|
7031
7368
|
const base = offlineFileStateMap(args.baseFiles);
|
|
7032
7369
|
const current = offlineFileStateMap(args.currentFiles);
|
|
7033
|
-
const
|
|
7370
|
+
const hydratedPaths = args.hydrationProgress?.hydratedPaths ?? /* @__PURE__ */ new Set();
|
|
7371
|
+
const deferredPaths = args.hydrationProgress?.deferredPaths ?? /* @__PURE__ */ new Set();
|
|
7034
7372
|
const candidates = snapshot.files.filter((incoming) => shouldDirectHydrateOfflineFile({
|
|
7035
7373
|
incoming,
|
|
7036
7374
|
base: base.get(incoming.path),
|
|
7037
7375
|
current: current.get(incoming.path)
|
|
7038
7376
|
})).sort((left, right) => right.bytes - left.bytes || left.path.localeCompare(right.path));
|
|
7039
7377
|
for (const incoming of candidates) {
|
|
7040
|
-
|
|
7041
|
-
|
|
7042
|
-
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
|
|
7046
|
-
|
|
7047
|
-
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
|
|
7053
|
-
|
|
7378
|
+
let result;
|
|
7379
|
+
try {
|
|
7380
|
+
result = await hydrateOfflineFileContent({
|
|
7381
|
+
remoteUrl: args.remoteUrl,
|
|
7382
|
+
token: args.token,
|
|
7383
|
+
namespace: args.namespace,
|
|
7384
|
+
includeTranscripts: args.includeTranscripts,
|
|
7385
|
+
memoryDir: args.memoryDir,
|
|
7386
|
+
sourceId: "remote",
|
|
7387
|
+
expected: incoming,
|
|
7388
|
+
baseSha256: base.get(incoming.path)?.sha256,
|
|
7389
|
+
readFile: args.readFile,
|
|
7390
|
+
readFileDigest: args.readFileDigest,
|
|
7391
|
+
writeFile: args.writeFile,
|
|
7392
|
+
writeStagingFile: args.writeStagingFile,
|
|
7393
|
+
writeFileChunks: args.writeFileChunks
|
|
7394
|
+
});
|
|
7395
|
+
} catch (error) {
|
|
7396
|
+
if (!isOfflineRemoteFileChangedError(error)) throw error;
|
|
7397
|
+
deferredPaths.add(incoming.path);
|
|
7398
|
+
continue;
|
|
7399
|
+
}
|
|
7400
|
+
if (result.conflict) {
|
|
7401
|
+
deferredPaths.add(result.conflict.path);
|
|
7402
|
+
continue;
|
|
7403
|
+
}
|
|
7404
|
+
if (result.applied || result.skipped) {
|
|
7405
|
+
hydratedPaths.add(incoming.path);
|
|
7406
|
+
}
|
|
7054
7407
|
}
|
|
7055
|
-
return
|
|
7408
|
+
return { hydratedPaths, deferredPaths };
|
|
7056
7409
|
}
|
|
7057
7410
|
function chunkOfflineFileContentBatches(files) {
|
|
7058
7411
|
const chunks = [];
|
|
@@ -7076,7 +7429,7 @@ function chunkOfflineFileContentBatches(files) {
|
|
|
7076
7429
|
}
|
|
7077
7430
|
function isOfflineFilesUnsupportedError(error) {
|
|
7078
7431
|
const message = error instanceof Error ? error.message : String(error);
|
|
7079
|
-
return /offline sync request failed: 404\b/.test(message);
|
|
7432
|
+
return /offline sync request failed: .* returned 404\b/.test(message);
|
|
7080
7433
|
}
|
|
7081
7434
|
function isMissingOfflineContentError(error) {
|
|
7082
7435
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -7087,7 +7440,9 @@ async function hydrateOfflineSnapshotContent(args) {
|
|
|
7087
7440
|
const neededFiles = offlineSnapshotContentFilesForApply({
|
|
7088
7441
|
snapshot,
|
|
7089
7442
|
baseFiles: args.baseFiles,
|
|
7090
|
-
currentFiles: args.currentFiles
|
|
7443
|
+
currentFiles: args.currentFiles,
|
|
7444
|
+
conflictContentMaxBytes: OFFLINE_SYNC_FILES_CONTENT_MAX_BATCH_BYTES,
|
|
7445
|
+
deferredPaths: args.deferredPaths
|
|
7091
7446
|
});
|
|
7092
7447
|
if (neededFiles.length === 0) return { ...args.snapshot, files: snapshot.files };
|
|
7093
7448
|
const expectedByPath = new Map(snapshot.files.map((file) => [file.path, file]));
|
|
@@ -7180,7 +7535,8 @@ async function postOfflineChangesBatch(args) {
|
|
|
7180
7535
|
method: "POST",
|
|
7181
7536
|
body: JSON.stringify({
|
|
7182
7537
|
namespace: args.namespace,
|
|
7183
|
-
changeset: args.changeset
|
|
7538
|
+
changeset: args.changeset,
|
|
7539
|
+
returnCurrentFiles: false
|
|
7184
7540
|
})
|
|
7185
7541
|
}
|
|
7186
7542
|
);
|
|
@@ -7201,6 +7557,7 @@ async function pushOfflineChanges(args) {
|
|
|
7201
7557
|
appliedDeletes += result.appliedDeletes;
|
|
7202
7558
|
skipped += result.skipped;
|
|
7203
7559
|
conflicts.push(...result.conflicts);
|
|
7560
|
+
args.onBatchApplied?.({ changeset, result });
|
|
7204
7561
|
}
|
|
7205
7562
|
return {
|
|
7206
7563
|
namespace,
|
|
@@ -7246,6 +7603,24 @@ async function createOfflineStorageIo(memoryDir) {
|
|
|
7246
7603
|
}
|
|
7247
7604
|
return {
|
|
7248
7605
|
readFile: async ({ filePath }) => storage.readOfflineSyncFile(filePath),
|
|
7606
|
+
readFileDigest: async ({ filePath }) => {
|
|
7607
|
+
const hash = createHash("sha256");
|
|
7608
|
+
let bytes = 0;
|
|
7609
|
+
for await (const rawChunk of readOfflineSyncFileChunks({
|
|
7610
|
+
filePath,
|
|
7611
|
+
memoryDir,
|
|
7612
|
+
secureStoreKey,
|
|
7613
|
+
chunkSize: OFFLINE_SYNC_FILE_CONTENT_UPLOAD_CHUNK_BYTES
|
|
7614
|
+
})) {
|
|
7615
|
+
const chunk = Buffer.isBuffer(rawChunk) ? rawChunk : Buffer.from(rawChunk);
|
|
7616
|
+
hash.update(chunk);
|
|
7617
|
+
bytes += chunk.length;
|
|
7618
|
+
}
|
|
7619
|
+
return {
|
|
7620
|
+
sha256: hash.digest("hex"),
|
|
7621
|
+
bytes
|
|
7622
|
+
};
|
|
7623
|
+
},
|
|
7249
7624
|
readFileChunks: ({ filePath, chunkSize }) => readOfflineSyncFileChunks({
|
|
7250
7625
|
filePath,
|
|
7251
7626
|
memoryDir,
|
|
@@ -7253,6 +7628,8 @@ async function createOfflineStorageIo(memoryDir) {
|
|
|
7253
7628
|
chunkSize
|
|
7254
7629
|
}),
|
|
7255
7630
|
writeFile: async ({ filePath, content }) => storage.writeOfflineSyncFile(filePath, content),
|
|
7631
|
+
writeStagingFile: async ({ filePath, content }) => storage.writeOfflineSyncStagingFile(filePath, content),
|
|
7632
|
+
writeFileChunks: async ({ filePath, chunks }) => storage.writeOfflineSyncFileChunks(filePath, chunks),
|
|
7256
7633
|
deleteFile: async ({ filePath }) => storage.deleteOfflineSyncFile(filePath)
|
|
7257
7634
|
};
|
|
7258
7635
|
}
|
|
@@ -7366,6 +7743,39 @@ var OfflineLargeFilePushError = class extends Error {
|
|
|
7366
7743
|
this.failures = failures;
|
|
7367
7744
|
}
|
|
7368
7745
|
};
|
|
7746
|
+
function advanceOfflineBaseFilesForSuccessfulPush(options) {
|
|
7747
|
+
const next = offlineFileStateMap(options.baseFiles);
|
|
7748
|
+
const current = offlineFileStateMap(options.currentFiles);
|
|
7749
|
+
const conflictPaths = new Set((options.conflicts ?? []).map((conflict) => conflict.path));
|
|
7750
|
+
for (const relPath of options.directPushedPaths ?? []) {
|
|
7751
|
+
if (conflictPaths.has(relPath)) continue;
|
|
7752
|
+
const file = current.get(relPath);
|
|
7753
|
+
if (file) next.set(relPath, file);
|
|
7754
|
+
}
|
|
7755
|
+
for (const change of options.changeset.changes) {
|
|
7756
|
+
if (conflictPaths.has(change.path)) continue;
|
|
7757
|
+
if (change.type === "delete") {
|
|
7758
|
+
next.delete(change.path);
|
|
7759
|
+
} else {
|
|
7760
|
+
next.set(change.path, {
|
|
7761
|
+
path: change.file.path,
|
|
7762
|
+
sha256: change.file.sha256,
|
|
7763
|
+
bytes: change.file.bytes,
|
|
7764
|
+
mtimeMs: change.file.mtimeMs
|
|
7765
|
+
});
|
|
7766
|
+
}
|
|
7767
|
+
}
|
|
7768
|
+
for (const file of options.hydratedFiles ?? []) {
|
|
7769
|
+
if (conflictPaths.has(file.path)) continue;
|
|
7770
|
+
next.set(file.path, {
|
|
7771
|
+
path: file.path,
|
|
7772
|
+
sha256: file.sha256,
|
|
7773
|
+
bytes: file.bytes,
|
|
7774
|
+
mtimeMs: file.mtimeMs
|
|
7775
|
+
});
|
|
7776
|
+
}
|
|
7777
|
+
return [...next.values()].sort((left, right) => left.path.localeCompare(right.path));
|
|
7778
|
+
}
|
|
7369
7779
|
async function runOfflineSyncOnce(options) {
|
|
7370
7780
|
fs7.mkdirSync(options.memoryDir, { recursive: true });
|
|
7371
7781
|
let activeStatePath = options.statePath;
|
|
@@ -7404,21 +7814,23 @@ async function runOfflineSyncOnce(options) {
|
|
|
7404
7814
|
});
|
|
7405
7815
|
}
|
|
7406
7816
|
const baseFiles = priorState?.baseFiles ?? [];
|
|
7817
|
+
const baseCapturedAt = priorState ? new Date(priorState.lastSyncedAt) : void 0;
|
|
7407
7818
|
const storageIo = await createOfflineStorageIo(options.memoryDir);
|
|
7408
7819
|
const localSourceId = localOfflineSourceId(options.memoryDir);
|
|
7409
|
-
const
|
|
7820
|
+
const currentSnapshotForPush = await buildOfflineSyncSnapshotFromBase({
|
|
7410
7821
|
root: options.memoryDir,
|
|
7411
7822
|
sourceId: localSourceId,
|
|
7412
7823
|
baseFiles,
|
|
7413
|
-
|
|
7414
|
-
readFile: storageIo.readFile
|
|
7415
|
-
});
|
|
7416
|
-
const currentSnapshotForPush = await buildOfflineSyncSnapshot({
|
|
7417
|
-
root: options.memoryDir,
|
|
7418
|
-
sourceId: localSourceId,
|
|
7824
|
+
baseCapturedAt,
|
|
7419
7825
|
includeContent: false,
|
|
7420
7826
|
includeTranscripts: options.includeTranscripts,
|
|
7421
|
-
readFile: storageIo.readFile
|
|
7827
|
+
readFile: storageIo.readFile,
|
|
7828
|
+
readFileDigest: storageIo.readFileDigest
|
|
7829
|
+
});
|
|
7830
|
+
const pendingSummary = summarizeOfflineSyncPendingFiles({
|
|
7831
|
+
baseFiles,
|
|
7832
|
+
currentFiles: currentSnapshotForPush.files,
|
|
7833
|
+
includeTranscripts: options.includeTranscripts
|
|
7422
7834
|
});
|
|
7423
7835
|
const baseByPath = offlineFileStateMap(baseFiles);
|
|
7424
7836
|
let directPushAppliedUpserts = 0;
|
|
@@ -7426,6 +7838,7 @@ async function runOfflineSyncOnce(options) {
|
|
|
7426
7838
|
let directPushNamespace;
|
|
7427
7839
|
const directPushConflicts = [];
|
|
7428
7840
|
const directPushedPaths = /* @__PURE__ */ new Set();
|
|
7841
|
+
const directPushDeferredPaths = /* @__PURE__ */ new Set();
|
|
7429
7842
|
const directPushFailures = [];
|
|
7430
7843
|
for (const file of offlineDirectPushFiles({
|
|
7431
7844
|
currentFiles: currentSnapshotForPush.files,
|
|
@@ -7446,6 +7859,10 @@ async function runOfflineSyncOnce(options) {
|
|
|
7446
7859
|
readFileChunks: storageIo.readFileChunks
|
|
7447
7860
|
});
|
|
7448
7861
|
} catch (error) {
|
|
7862
|
+
if (isOfflineLocalFileChangedError(error)) {
|
|
7863
|
+
directPushDeferredPaths.add(file.path);
|
|
7864
|
+
continue;
|
|
7865
|
+
}
|
|
7449
7866
|
directPushFailures.push({
|
|
7450
7867
|
path: file.path,
|
|
7451
7868
|
error: error instanceof Error ? error.message : String(error)
|
|
@@ -7465,100 +7882,343 @@ async function runOfflineSyncOnce(options) {
|
|
|
7465
7882
|
if (result.skipped) directPushSkipped += 1;
|
|
7466
7883
|
}
|
|
7467
7884
|
}
|
|
7468
|
-
|
|
7469
|
-
|
|
7470
|
-
|
|
7471
|
-
|
|
7472
|
-
root: options.memoryDir,
|
|
7885
|
+
let changeset = {
|
|
7886
|
+
format: "remnic.offline-sync.changeset.v1",
|
|
7887
|
+
schemaVersion: 1,
|
|
7888
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7473
7889
|
sourceId: localSourceId,
|
|
7474
|
-
baseFiles,
|
|
7475
|
-
excludePaths: [...directPushedPaths],
|
|
7476
7890
|
includeTranscripts: options.includeTranscripts,
|
|
7477
|
-
|
|
7478
|
-
}
|
|
7479
|
-
|
|
7480
|
-
|
|
7481
|
-
|
|
7482
|
-
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
namespace: pushedInline?.namespace ?? directPushNamespace ?? syncNamespace ?? "",
|
|
7487
|
-
appliedUpserts: (pushedInline?.appliedUpserts ?? 0) + directPushAppliedUpserts,
|
|
7488
|
-
appliedDeletes: pushedInline?.appliedDeletes ?? 0,
|
|
7489
|
-
skipped: (pushedInline?.skipped ?? 0) + directPushSkipped,
|
|
7490
|
-
conflicts: [...directPushConflicts, ...pushedInline?.conflicts ?? []]
|
|
7891
|
+
changes: []
|
|
7892
|
+
};
|
|
7893
|
+
let pushed = null;
|
|
7894
|
+
const buildPushedSummary = (pushedInline2) => directPushedPaths.size > 0 || pushedInline2 ? {
|
|
7895
|
+
namespace: pushedInline2?.namespace ?? directPushNamespace ?? syncNamespace ?? "",
|
|
7896
|
+
appliedUpserts: (pushedInline2?.appliedUpserts ?? 0) + directPushAppliedUpserts,
|
|
7897
|
+
appliedDeletes: pushedInline2?.appliedDeletes ?? 0,
|
|
7898
|
+
skipped: (pushedInline2?.skipped ?? 0) + directPushSkipped,
|
|
7899
|
+
conflicts: [...directPushConflicts, ...pushedInline2?.conflicts ?? []]
|
|
7491
7900
|
} : null;
|
|
7492
|
-
const
|
|
7493
|
-
|
|
7494
|
-
|
|
7495
|
-
|
|
7496
|
-
|
|
7497
|
-
|
|
7498
|
-
});
|
|
7499
|
-
const currentSnapshot = await buildOfflineSyncSnapshot({
|
|
7500
|
-
root: options.memoryDir,
|
|
7501
|
-
sourceId: localSourceId,
|
|
7502
|
-
includeContent: false,
|
|
7503
|
-
includeTranscripts: options.includeTranscripts,
|
|
7504
|
-
readFile: storageIo.readFile
|
|
7901
|
+
const mergeInlinePushSummary = (prior, result) => ({
|
|
7902
|
+
namespace: result.namespace || prior?.namespace || syncNamespace || "",
|
|
7903
|
+
appliedUpserts: (prior?.appliedUpserts ?? 0) + result.appliedUpserts,
|
|
7904
|
+
appliedDeletes: (prior?.appliedDeletes ?? 0) + result.appliedDeletes,
|
|
7905
|
+
skipped: (prior?.skipped ?? 0) + result.skipped,
|
|
7906
|
+
conflicts: [...prior?.conflicts ?? [], ...result.conflicts]
|
|
7505
7907
|
});
|
|
7506
|
-
|
|
7507
|
-
|
|
7508
|
-
token: options.token,
|
|
7509
|
-
namespace: syncNamespace,
|
|
7510
|
-
includeTranscripts: options.includeTranscripts,
|
|
7511
|
-
snapshot: remoteSnapshotMetadata,
|
|
7512
|
-
baseFiles,
|
|
7513
|
-
currentFiles: currentSnapshot.files,
|
|
7908
|
+
pushed = buildPushedSummary(null);
|
|
7909
|
+
const stateWritePathsFor = (resolvedNamespace2) => offlineStatePathsForNamespace({
|
|
7514
7910
|
memoryDir: options.memoryDir,
|
|
7515
|
-
|
|
7911
|
+
remoteUrl: options.remoteUrl,
|
|
7912
|
+
requestedNamespace: options.namespace,
|
|
7913
|
+
resolvedNamespace: resolvedNamespace2,
|
|
7914
|
+
explicitStatePath: options.statePathExplicit ? activeStatePath : void 0
|
|
7516
7915
|
});
|
|
7517
|
-
const
|
|
7916
|
+
const writePartialPushState = async (error, partial, checkpointChangeset = changeset) => {
|
|
7917
|
+
const resolvedNamespace2 = partial?.resolvedNamespace ?? resolvedOfflineSnapshotNamespace({ namespace: pushed?.namespace ?? "" }, syncNamespace);
|
|
7918
|
+
const stateWritePaths2 = stateWritePathsFor(resolvedNamespace2);
|
|
7919
|
+
const nextBaseFiles = advanceOfflineBaseFilesForSuccessfulPush({
|
|
7920
|
+
baseFiles,
|
|
7921
|
+
currentFiles: currentSnapshotForPush.files,
|
|
7922
|
+
directPushedPaths: [...directPushedPaths],
|
|
7923
|
+
hydratedFiles: partial?.hydratedFiles,
|
|
7924
|
+
changeset: checkpointChangeset,
|
|
7925
|
+
conflicts: pushed?.conflicts ?? directPushConflicts
|
|
7926
|
+
});
|
|
7927
|
+
const state2 = {
|
|
7928
|
+
version: 1,
|
|
7929
|
+
remoteId: options.remoteUrl,
|
|
7930
|
+
...resolvedNamespace2 ? { namespace: resolvedNamespace2 } : {},
|
|
7931
|
+
includeTranscripts: options.includeTranscripts,
|
|
7932
|
+
// Partial checkpoints do not recapture conflicted/deferred paths, so keep
|
|
7933
|
+
// the original capture time for safe fast-base reuse on the next run.
|
|
7934
|
+
lastSyncedAt: priorState?.lastSyncedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
7935
|
+
baseFiles: nextBaseFiles
|
|
7936
|
+
};
|
|
7937
|
+
for (const statePath of stateWritePaths2) {
|
|
7938
|
+
await writeOfflineSyncState(statePath, state2);
|
|
7939
|
+
}
|
|
7940
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7941
|
+
return {
|
|
7942
|
+
statePath: stateWritePaths2[0] ?? activeStatePath,
|
|
7943
|
+
namespace: resolvedNamespace2,
|
|
7944
|
+
prepared: priorState === null,
|
|
7945
|
+
pushed,
|
|
7946
|
+
pull: null,
|
|
7947
|
+
pullError: message,
|
|
7948
|
+
partial: true,
|
|
7949
|
+
pendingSummary,
|
|
7950
|
+
remoteFileCount: partial?.remoteFileCount ?? null,
|
|
7951
|
+
deferred: {
|
|
7952
|
+
localChangedDuringPush: [...directPushDeferredPaths].sort(),
|
|
7953
|
+
remoteChangedDuringHydrate: [...partial?.remoteDeferredPaths ?? []].sort(),
|
|
7954
|
+
total: directPushDeferredPaths.size + (partial?.remoteDeferredPaths?.length ?? 0)
|
|
7955
|
+
}
|
|
7956
|
+
};
|
|
7957
|
+
};
|
|
7958
|
+
if (directPushFailures.length > 0) {
|
|
7959
|
+
const error = new OfflineLargeFilePushError(directPushFailures);
|
|
7960
|
+
if (pushed) return writePartialPushState(error);
|
|
7961
|
+
throw error;
|
|
7962
|
+
}
|
|
7963
|
+
let currentSnapshotForChangeset = directPushedPaths.size > 0 ? await buildOfflineSyncSnapshotFromBase({
|
|
7518
7964
|
root: options.memoryDir,
|
|
7519
7965
|
sourceId: localSourceId,
|
|
7966
|
+
baseFiles,
|
|
7967
|
+
baseCapturedAt,
|
|
7520
7968
|
includeContent: false,
|
|
7521
7969
|
includeTranscripts: options.includeTranscripts,
|
|
7522
|
-
readFile: storageIo.readFile
|
|
7523
|
-
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
7527
|
-
|
|
7528
|
-
|
|
7529
|
-
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7970
|
+
readFile: storageIo.readFile,
|
|
7971
|
+
readFileDigest: storageIo.readFileDigest
|
|
7972
|
+
}) : currentSnapshotForPush;
|
|
7973
|
+
let changesetRetryCount = 0;
|
|
7974
|
+
for (; ; ) {
|
|
7975
|
+
try {
|
|
7976
|
+
changeset = await buildOfflineSyncChangesetFromSnapshot({
|
|
7977
|
+
root: options.memoryDir,
|
|
7978
|
+
sourceId: localSourceId,
|
|
7979
|
+
currentFiles: currentSnapshotForChangeset.files,
|
|
7980
|
+
baseFiles,
|
|
7981
|
+
excludePaths: [...directPushedPaths, ...directPushDeferredPaths],
|
|
7982
|
+
includeTranscripts: options.includeTranscripts,
|
|
7983
|
+
readFile: storageIo.readFile
|
|
7984
|
+
});
|
|
7985
|
+
break;
|
|
7986
|
+
} catch (error) {
|
|
7987
|
+
const changedPath = offlineChangesetFileChangedPath(error);
|
|
7988
|
+
if (!changedPath) {
|
|
7989
|
+
if (pushed) return writePartialPushState(error);
|
|
7990
|
+
throw error;
|
|
7991
|
+
}
|
|
7992
|
+
if (directPushDeferredPaths.has(changedPath)) {
|
|
7993
|
+
const stalledError = new Error(`offline sync changeset retry stalled on already-deferred path: ${changedPath}`);
|
|
7994
|
+
if (pushed) return writePartialPushState(stalledError);
|
|
7995
|
+
throw stalledError;
|
|
7996
|
+
}
|
|
7997
|
+
if (changesetRetryCount >= OFFLINE_SYNC_CHANGESET_RETRY_MAX) {
|
|
7998
|
+
const retryError = new Error(
|
|
7999
|
+
`offline sync changeset retry limit exceeded after ${OFFLINE_SYNC_CHANGESET_RETRY_MAX} volatile files; last changed path: ${changedPath}`
|
|
8000
|
+
);
|
|
8001
|
+
if (pushed) return writePartialPushState(retryError);
|
|
8002
|
+
throw retryError;
|
|
8003
|
+
}
|
|
8004
|
+
changesetRetryCount += 1;
|
|
8005
|
+
directPushDeferredPaths.add(changedPath);
|
|
8006
|
+
currentSnapshotForChangeset = await buildOfflineSyncSnapshotFromBase({
|
|
8007
|
+
root: options.memoryDir,
|
|
8008
|
+
sourceId: localSourceId,
|
|
8009
|
+
baseFiles,
|
|
8010
|
+
baseCapturedAt,
|
|
8011
|
+
includeContent: false,
|
|
8012
|
+
includeTranscripts: options.includeTranscripts,
|
|
8013
|
+
readFile: storageIo.readFile,
|
|
8014
|
+
readFileDigest: storageIo.readFileDigest
|
|
8015
|
+
});
|
|
8016
|
+
}
|
|
8017
|
+
}
|
|
8018
|
+
const inlineAppliedChanges = [];
|
|
8019
|
+
let pushedInlineProgress = null;
|
|
8020
|
+
let pushedInline = null;
|
|
7535
8021
|
try {
|
|
7536
|
-
|
|
8022
|
+
pushedInline = changeset.changes.length > 0 ? await pushOfflineChanges({
|
|
8023
|
+
remoteUrl: options.remoteUrl,
|
|
8024
|
+
token: options.token,
|
|
8025
|
+
namespace: syncNamespace,
|
|
8026
|
+
changeset,
|
|
8027
|
+
onBatchApplied: (batch) => {
|
|
8028
|
+
inlineAppliedChanges.push(...batch.changeset.changes);
|
|
8029
|
+
pushedInlineProgress = mergeInlinePushSummary(pushedInlineProgress, batch.result);
|
|
8030
|
+
pushed = buildPushedSummary(pushedInlineProgress);
|
|
8031
|
+
}
|
|
8032
|
+
}) : null;
|
|
8033
|
+
} catch (error) {
|
|
8034
|
+
if (pushed || inlineAppliedChanges.length > 0) {
|
|
8035
|
+
return writePartialPushState(error, void 0, {
|
|
8036
|
+
...changeset,
|
|
8037
|
+
changes: inlineAppliedChanges
|
|
8038
|
+
});
|
|
8039
|
+
}
|
|
8040
|
+
throw error;
|
|
8041
|
+
}
|
|
8042
|
+
pushed = buildPushedSummary(pushedInline);
|
|
8043
|
+
let remoteSnapshotMetadata;
|
|
8044
|
+
try {
|
|
8045
|
+
remoteSnapshotMetadata = await fetchOfflineSnapshot({
|
|
8046
|
+
remoteUrl: options.remoteUrl,
|
|
8047
|
+
token: options.token,
|
|
8048
|
+
namespace: syncNamespace,
|
|
8049
|
+
includeTranscripts: options.includeTranscripts,
|
|
8050
|
+
includeContent: false,
|
|
8051
|
+
baseFiles,
|
|
8052
|
+
baseCapturedAt
|
|
8053
|
+
});
|
|
8054
|
+
} catch (error) {
|
|
8055
|
+
if (pushed) return writePartialPushState(error);
|
|
8056
|
+
throw error;
|
|
8057
|
+
}
|
|
8058
|
+
let currentSnapshot;
|
|
8059
|
+
try {
|
|
8060
|
+
currentSnapshot = await buildOfflineSyncSnapshotFromBase({
|
|
7537
8061
|
root: options.memoryDir,
|
|
7538
|
-
|
|
8062
|
+
sourceId: localSourceId,
|
|
8063
|
+
baseFiles,
|
|
8064
|
+
baseCapturedAt,
|
|
8065
|
+
includeContent: false,
|
|
8066
|
+
includeTranscripts: options.includeTranscripts,
|
|
8067
|
+
readFile: storageIo.readFile,
|
|
8068
|
+
readFileDigest: storageIo.readFileDigest
|
|
8069
|
+
});
|
|
8070
|
+
} catch (error) {
|
|
8071
|
+
if (pushed) return writePartialPushState(error);
|
|
8072
|
+
throw error;
|
|
8073
|
+
}
|
|
8074
|
+
let directHydration;
|
|
8075
|
+
const directHydrationProgress = {
|
|
8076
|
+
hydratedPaths: /* @__PURE__ */ new Set(),
|
|
8077
|
+
deferredPaths: /* @__PURE__ */ new Set()
|
|
8078
|
+
};
|
|
8079
|
+
try {
|
|
8080
|
+
directHydration = await directHydrateLargeOfflineFiles({
|
|
8081
|
+
remoteUrl: options.remoteUrl,
|
|
8082
|
+
token: options.token,
|
|
8083
|
+
namespace: syncNamespace,
|
|
8084
|
+
includeTranscripts: options.includeTranscripts,
|
|
8085
|
+
snapshot: remoteSnapshotMetadata,
|
|
7539
8086
|
baseFiles,
|
|
8087
|
+
currentFiles: currentSnapshot.files,
|
|
8088
|
+
memoryDir: options.memoryDir,
|
|
7540
8089
|
readFile: storageIo.readFile,
|
|
8090
|
+
readFileDigest: storageIo.readFileDigest,
|
|
7541
8091
|
writeFile: storageIo.writeFile,
|
|
7542
|
-
|
|
8092
|
+
writeStagingFile: storageIo.writeStagingFile,
|
|
8093
|
+
writeFileChunks: storageIo.writeFileChunks,
|
|
8094
|
+
hydrationProgress: directHydrationProgress
|
|
7543
8095
|
});
|
|
7544
8096
|
} catch (error) {
|
|
7545
|
-
|
|
7546
|
-
|
|
8097
|
+
const partial = offlinePartialHydrationForPaths({
|
|
8098
|
+
files: remoteSnapshotMetadata.files,
|
|
8099
|
+
hydratedPaths: directHydrationProgress.hydratedPaths,
|
|
8100
|
+
deferredPaths: directHydrationProgress.deferredPaths
|
|
8101
|
+
});
|
|
8102
|
+
if (pushed || partial.hydratedFiles.length > 0) {
|
|
8103
|
+
return writePartialPushState(error, {
|
|
8104
|
+
...partial,
|
|
8105
|
+
resolvedNamespace: resolvedOfflineSnapshotNamespace(remoteSnapshotMetadata, syncNamespace),
|
|
8106
|
+
remoteFileCount: remoteSnapshotMetadata.files.length
|
|
8107
|
+
});
|
|
8108
|
+
}
|
|
8109
|
+
throw error;
|
|
8110
|
+
}
|
|
8111
|
+
const directHydratedPaths = directHydration.hydratedPaths;
|
|
8112
|
+
const remoteDeferredPaths = directHydration.deferredPaths;
|
|
8113
|
+
const partialHydration = offlinePartialHydrationForPaths({
|
|
8114
|
+
files: remoteSnapshotMetadata.files,
|
|
8115
|
+
hydratedPaths: directHydratedPaths,
|
|
8116
|
+
deferredPaths: remoteDeferredPaths
|
|
8117
|
+
});
|
|
8118
|
+
const partialHydrationWithContext = {
|
|
8119
|
+
...partialHydration,
|
|
8120
|
+
resolvedNamespace: resolvedOfflineSnapshotNamespace(remoteSnapshotMetadata, syncNamespace),
|
|
8121
|
+
remoteFileCount: remoteSnapshotMetadata.files.length
|
|
8122
|
+
};
|
|
8123
|
+
const buildCurrentSnapshotForApply = async () => buildOfflineSyncSnapshotFromBase({
|
|
8124
|
+
root: options.memoryDir,
|
|
8125
|
+
sourceId: localSourceId,
|
|
8126
|
+
baseFiles,
|
|
8127
|
+
baseCapturedAt,
|
|
8128
|
+
includeContent: false,
|
|
8129
|
+
includeTranscripts: options.includeTranscripts,
|
|
8130
|
+
readFile: storageIo.readFile,
|
|
8131
|
+
readFileDigest: storageIo.readFileDigest
|
|
8132
|
+
});
|
|
8133
|
+
const applyCurrentSnapshot = directHydratedPaths.size > 0 ? await buildCurrentSnapshotForApply() : currentSnapshot;
|
|
8134
|
+
let remoteSnapshot;
|
|
8135
|
+
try {
|
|
8136
|
+
remoteSnapshot = await hydrateOfflineSnapshotContent({
|
|
7547
8137
|
remoteUrl: options.remoteUrl,
|
|
7548
8138
|
token: options.token,
|
|
7549
8139
|
namespace: syncNamespace,
|
|
7550
8140
|
includeTranscripts: options.includeTranscripts,
|
|
7551
8141
|
snapshot: remoteSnapshotMetadata,
|
|
7552
|
-
baseFiles
|
|
8142
|
+
baseFiles,
|
|
8143
|
+
currentFiles: applyCurrentSnapshot.files,
|
|
8144
|
+
deferredPaths: [...remoteDeferredPaths]
|
|
7553
8145
|
});
|
|
8146
|
+
} catch (error) {
|
|
8147
|
+
if (pushed || partialHydration.hydratedFiles.length > 0) {
|
|
8148
|
+
return writePartialPushState(error, partialHydrationWithContext);
|
|
8149
|
+
}
|
|
8150
|
+
throw error;
|
|
8151
|
+
}
|
|
8152
|
+
const resolvedNamespace = resolvedOfflineSnapshotNamespace(remoteSnapshot, syncNamespace);
|
|
8153
|
+
let pull;
|
|
8154
|
+
try {
|
|
8155
|
+
const latestApplySnapshot = await buildCurrentSnapshotForApply();
|
|
7554
8156
|
pull = await applyOfflineSyncSnapshot({
|
|
7555
8157
|
root: options.memoryDir,
|
|
7556
|
-
snapshot:
|
|
8158
|
+
snapshot: remoteSnapshot,
|
|
7557
8159
|
baseFiles,
|
|
8160
|
+
currentFiles: latestApplySnapshot.files,
|
|
8161
|
+
deferredPaths: [...remoteDeferredPaths],
|
|
8162
|
+
allowMissingConflictContent: true,
|
|
7558
8163
|
readFile: storageIo.readFile,
|
|
8164
|
+
readFileDigest: storageIo.readFileDigest,
|
|
7559
8165
|
writeFile: storageIo.writeFile,
|
|
7560
8166
|
deleteFile: storageIo.deleteFile
|
|
7561
8167
|
});
|
|
8168
|
+
} catch (error) {
|
|
8169
|
+
if (!isMissingOfflineContentError(error)) {
|
|
8170
|
+
if (pushed || partialHydration.hydratedFiles.length > 0) {
|
|
8171
|
+
return writePartialPushState(error, {
|
|
8172
|
+
...partialHydrationWithContext,
|
|
8173
|
+
resolvedNamespace
|
|
8174
|
+
});
|
|
8175
|
+
}
|
|
8176
|
+
throw error;
|
|
8177
|
+
}
|
|
8178
|
+
let retrySnapshot;
|
|
8179
|
+
try {
|
|
8180
|
+
retrySnapshot = await hydrateOfflineSnapshotContent({
|
|
8181
|
+
remoteUrl: options.remoteUrl,
|
|
8182
|
+
token: options.token,
|
|
8183
|
+
namespace: syncNamespace,
|
|
8184
|
+
includeTranscripts: options.includeTranscripts,
|
|
8185
|
+
snapshot: remoteSnapshotMetadata,
|
|
8186
|
+
baseFiles,
|
|
8187
|
+
currentFiles: applyCurrentSnapshot.files,
|
|
8188
|
+
deferredPaths: [...remoteDeferredPaths]
|
|
8189
|
+
});
|
|
8190
|
+
} catch (retryError) {
|
|
8191
|
+
if (pushed || partialHydration.hydratedFiles.length > 0) {
|
|
8192
|
+
return writePartialPushState(retryError, {
|
|
8193
|
+
...partialHydrationWithContext,
|
|
8194
|
+
resolvedNamespace
|
|
8195
|
+
});
|
|
8196
|
+
}
|
|
8197
|
+
throw retryError;
|
|
8198
|
+
}
|
|
8199
|
+
try {
|
|
8200
|
+
const latestRetryApplySnapshot = await buildCurrentSnapshotForApply();
|
|
8201
|
+
pull = await applyOfflineSyncSnapshot({
|
|
8202
|
+
root: options.memoryDir,
|
|
8203
|
+
snapshot: retrySnapshot,
|
|
8204
|
+
baseFiles,
|
|
8205
|
+
currentFiles: latestRetryApplySnapshot.files,
|
|
8206
|
+
deferredPaths: [...remoteDeferredPaths],
|
|
8207
|
+
allowMissingConflictContent: true,
|
|
8208
|
+
readFile: storageIo.readFile,
|
|
8209
|
+
readFileDigest: storageIo.readFileDigest,
|
|
8210
|
+
writeFile: storageIo.writeFile,
|
|
8211
|
+
deleteFile: storageIo.deleteFile
|
|
8212
|
+
});
|
|
8213
|
+
} catch (retryApplyError) {
|
|
8214
|
+
if (pushed || partialHydration.hydratedFiles.length > 0) {
|
|
8215
|
+
return writePartialPushState(retryApplyError, {
|
|
8216
|
+
...partialHydrationWithContext,
|
|
8217
|
+
resolvedNamespace
|
|
8218
|
+
});
|
|
8219
|
+
}
|
|
8220
|
+
throw retryApplyError;
|
|
8221
|
+
}
|
|
7562
8222
|
}
|
|
7563
8223
|
const state = offlineSyncStateFromSnapshot({
|
|
7564
8224
|
remoteId: options.remoteUrl,
|
|
@@ -7566,13 +8226,7 @@ async function runOfflineSyncOnce(options) {
|
|
|
7566
8226
|
snapshot: remoteSnapshot,
|
|
7567
8227
|
baseFiles: pull.nextBaseFiles
|
|
7568
8228
|
});
|
|
7569
|
-
const stateWritePaths =
|
|
7570
|
-
memoryDir: options.memoryDir,
|
|
7571
|
-
remoteUrl: options.remoteUrl,
|
|
7572
|
-
requestedNamespace: options.namespace,
|
|
7573
|
-
resolvedNamespace,
|
|
7574
|
-
explicitStatePath: options.statePathExplicit ? activeStatePath : void 0
|
|
7575
|
-
});
|
|
8229
|
+
const stateWritePaths = stateWritePathsFor(resolvedNamespace);
|
|
7576
8230
|
for (const statePath of stateWritePaths) {
|
|
7577
8231
|
await writeOfflineSyncState(statePath, state);
|
|
7578
8232
|
}
|
|
@@ -7582,8 +8236,53 @@ async function runOfflineSyncOnce(options) {
|
|
|
7582
8236
|
prepared: priorState === null,
|
|
7583
8237
|
pushed,
|
|
7584
8238
|
pull,
|
|
8239
|
+
partial: false,
|
|
7585
8240
|
pendingSummary,
|
|
7586
|
-
remoteFileCount: remoteSnapshot.files.length
|
|
8241
|
+
remoteFileCount: remoteSnapshot.files.length,
|
|
8242
|
+
deferred: {
|
|
8243
|
+
localChangedDuringPush: [...directPushDeferredPaths].sort(),
|
|
8244
|
+
remoteChangedDuringHydrate: [...remoteDeferredPaths].sort(),
|
|
8245
|
+
total: directPushDeferredPaths.size + remoteDeferredPaths.size
|
|
8246
|
+
}
|
|
8247
|
+
};
|
|
8248
|
+
}
|
|
8249
|
+
function sumOfflineFileBytes(files) {
|
|
8250
|
+
return files.reduce((total, file) => total + file.bytes, 0);
|
|
8251
|
+
}
|
|
8252
|
+
function offlineStateJsonSummary(state) {
|
|
8253
|
+
if (!state) return null;
|
|
8254
|
+
return {
|
|
8255
|
+
remoteId: state.remoteId,
|
|
8256
|
+
namespace: state.namespace ?? null,
|
|
8257
|
+
includeTranscripts: state.includeTranscripts,
|
|
8258
|
+
lastSyncedAt: state.lastSyncedAt,
|
|
8259
|
+
baseFileCount: state.baseFiles.length,
|
|
8260
|
+
baseBytes: sumOfflineFileBytes(state.baseFiles)
|
|
8261
|
+
};
|
|
8262
|
+
}
|
|
8263
|
+
function offlinePullJsonSummary(pull) {
|
|
8264
|
+
return {
|
|
8265
|
+
upserted: pull.upserted,
|
|
8266
|
+
deleted: pull.deleted,
|
|
8267
|
+
skipped: pull.skipped,
|
|
8268
|
+
pendingLocal: pull.pendingLocal,
|
|
8269
|
+
conflicts: pull.conflicts,
|
|
8270
|
+
nextBaseFileCount: pull.nextBaseFiles.length,
|
|
8271
|
+
nextBaseBytes: sumOfflineFileBytes(pull.nextBaseFiles)
|
|
8272
|
+
};
|
|
8273
|
+
}
|
|
8274
|
+
function offlineSyncResultJsonSummary(result) {
|
|
8275
|
+
return {
|
|
8276
|
+
statePath: result.statePath,
|
|
8277
|
+
namespace: result.namespace ?? null,
|
|
8278
|
+
prepared: result.prepared,
|
|
8279
|
+
partial: result.partial,
|
|
8280
|
+
pushed: result.pushed,
|
|
8281
|
+
pull: result.pull ? offlinePullJsonSummary(result.pull) : null,
|
|
8282
|
+
pullError: result.pullError ?? null,
|
|
8283
|
+
pendingSummary: result.pendingSummary,
|
|
8284
|
+
remoteFileCount: result.remoteFileCount,
|
|
8285
|
+
deferred: result.deferred
|
|
7587
8286
|
};
|
|
7588
8287
|
}
|
|
7589
8288
|
function assertOfflineStateMatches(options) {
|
|
@@ -7664,6 +8363,7 @@ Environment fallbacks:
|
|
|
7664
8363
|
snapshot: remoteSnapshot,
|
|
7665
8364
|
baseFiles: existingState?.state.baseFiles ?? [],
|
|
7666
8365
|
readFile: storageIo.readFile,
|
|
8366
|
+
readFileDigest: storageIo.readFileDigest,
|
|
7667
8367
|
writeFile: storageIo.writeFile,
|
|
7668
8368
|
deleteFile: storageIo.deleteFile
|
|
7669
8369
|
});
|
|
@@ -7677,7 +8377,12 @@ Environment fallbacks:
|
|
|
7677
8377
|
await writeOfflineSyncState(pathToWrite, state);
|
|
7678
8378
|
}
|
|
7679
8379
|
if (json) {
|
|
7680
|
-
console.log(JSON.stringify({
|
|
8380
|
+
console.log(JSON.stringify({
|
|
8381
|
+
statePath: activeStatePath,
|
|
8382
|
+
namespace: resolvedNamespace,
|
|
8383
|
+
remoteFiles: remoteSnapshot.files.length,
|
|
8384
|
+
pull: offlinePullJsonSummary(pull)
|
|
8385
|
+
}, null, 2));
|
|
7681
8386
|
} else {
|
|
7682
8387
|
console.log(`Offline cache prepared: ${memoryDir}`);
|
|
7683
8388
|
console.log(`Namespace: ${resolvedNamespace ?? "(default)"}`);
|
|
@@ -7699,12 +8404,19 @@ Environment fallbacks:
|
|
|
7699
8404
|
statePathExplicit
|
|
7700
8405
|
});
|
|
7701
8406
|
if (json) {
|
|
7702
|
-
console.log(JSON.stringify(result, null, 2));
|
|
8407
|
+
console.log(JSON.stringify(offlineSyncResultJsonSummary(result), null, 2));
|
|
7703
8408
|
} else {
|
|
7704
8409
|
console.log(`Offline sync complete${result.prepared ? " (initialized state)" : ""}.`);
|
|
7705
8410
|
console.log(`Pushed: ${result.pushed ? `${result.pushed.appliedUpserts} upserts, ${result.pushed.appliedDeletes} deletes, ${result.pushed.conflicts.length} conflicts` : "nothing pending"}`);
|
|
7706
|
-
|
|
8411
|
+
if (result.pull) {
|
|
8412
|
+
console.log(`Pulled: ${result.pull.upserted} upserts, ${result.pull.deleted} deletes, ${result.pull.conflicts.length} conflicts`);
|
|
8413
|
+
} else {
|
|
8414
|
+
console.log(`Pulled: deferred (${result.pullError ?? "pull unavailable"})`);
|
|
8415
|
+
}
|
|
7707
8416
|
console.log(`Pending local before push: ${result.pendingSummary.total}`);
|
|
8417
|
+
if (result.deferred.total > 0) {
|
|
8418
|
+
console.log(`Deferred volatile files: ${result.deferred.total}`);
|
|
8419
|
+
}
|
|
7708
8420
|
console.log(`Namespace: ${result.namespace ?? "(default)"}`);
|
|
7709
8421
|
console.log(`State: ${result.statePath}`);
|
|
7710
8422
|
}
|
|
@@ -7727,11 +8439,17 @@ Environment fallbacks:
|
|
|
7727
8439
|
root: memoryDir,
|
|
7728
8440
|
sourceId: localOfflineSourceId(memoryDir),
|
|
7729
8441
|
baseFiles: state?.baseFiles ?? [],
|
|
8442
|
+
baseCapturedAt: state ? new Date(state.lastSyncedAt) : void 0,
|
|
7730
8443
|
includeTranscripts,
|
|
7731
|
-
readFile: storageIo.readFile
|
|
8444
|
+
readFile: storageIo.readFile,
|
|
8445
|
+
readFileDigest: storageIo.readFileDigest
|
|
7732
8446
|
});
|
|
7733
8447
|
if (json) {
|
|
7734
|
-
console.log(JSON.stringify({
|
|
8448
|
+
console.log(JSON.stringify({
|
|
8449
|
+
statePath: statePath ?? null,
|
|
8450
|
+
state: offlineStateJsonSummary(state),
|
|
8451
|
+
pending: summary
|
|
8452
|
+
}, null, 2));
|
|
7735
8453
|
} else {
|
|
7736
8454
|
console.log(`Offline state: ${state ? "ready" : "not prepared"}`);
|
|
7737
8455
|
console.log(`State: ${statePath ?? "(not selected; pass --state or --remote-url to inspect a prepared remote state)"}`);
|
|
@@ -7762,8 +8480,10 @@ Environment fallbacks:
|
|
|
7762
8480
|
statePath,
|
|
7763
8481
|
statePathExplicit
|
|
7764
8482
|
});
|
|
8483
|
+
const pulled = result.pull ? result.pull.upserted + result.pull.deleted : 0;
|
|
8484
|
+
const conflicts = (result.pushed?.conflicts.length ?? 0) + (result.pull?.conflicts.length ?? 0);
|
|
7765
8485
|
console.log(
|
|
7766
|
-
`[${(/* @__PURE__ */ new Date()).toISOString()}] sync ok: pushed=${result.pushed ? result.pushed.appliedUpserts + result.pushed.appliedDeletes : 0}, pulled=${
|
|
8486
|
+
`[${(/* @__PURE__ */ new Date()).toISOString()}] sync ${result.partial ? "partial" : "ok"}: pushed=${result.pushed ? result.pushed.appliedUpserts + result.pushed.appliedDeletes : 0}, pulled=${pulled}, conflicts=${conflicts}, deferred=${result.deferred.total}${result.pullError ? `, pullError=${result.pullError}` : ""}`
|
|
7767
8487
|
);
|
|
7768
8488
|
} catch (error) {
|
|
7769
8489
|
console.log(`[${(/* @__PURE__ */ new Date()).toISOString()}] sync waiting: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -10676,32 +11396,50 @@ if (argv1Base.endsWith("remnic.ts") || argv1Base.endsWith("remnic.js") || argv1B
|
|
|
10676
11396
|
export {
|
|
10677
11397
|
BENCHMARK_CATALOG,
|
|
10678
11398
|
OFFLINE_SYNC_APPLY_MAX_REQUEST_BYTES,
|
|
11399
|
+
OFFLINE_SYNC_CHANGESET_RETRY_MAX,
|
|
10679
11400
|
OFFLINE_SYNC_DIRECT_HYDRATE_MIN_BYTES,
|
|
10680
11401
|
OFFLINE_SYNC_DIRECT_PUSH_MIN_BYTES,
|
|
10681
11402
|
OFFLINE_SYNC_FILE_CONTENT_UPLOAD_CHUNK_BYTES,
|
|
11403
|
+
OFFLINE_SYNC_REQUEST_TIMEOUT_DEFAULT_MS,
|
|
11404
|
+
OFFLINE_SYNC_SNAPSHOT_BASE_POST_MAX_FILES,
|
|
11405
|
+
OFFLINE_SYNC_SNAPSHOT_BASE_POST_PREFERRED_MAX_BODY_BYTES,
|
|
10682
11406
|
TAXONOMY_RESOLVE_BOOLEAN_FLAGS,
|
|
10683
11407
|
TAXONOMY_RESOLVE_VALUE_FLAGS,
|
|
10684
11408
|
__benchDatasetTestHooks,
|
|
11409
|
+
advanceOfflineBaseFilesForSuccessfulPush,
|
|
10685
11410
|
buildBenchRuntimeProfileRequest,
|
|
10686
11411
|
buildPackageBenchExecutionPlans,
|
|
10687
11412
|
buildQueryRecallRequest,
|
|
10688
11413
|
chunkOfflineChangesetApplyBatches,
|
|
10689
11414
|
chunkOfflineFileContentBatches,
|
|
11415
|
+
directHydrateLargeOfflineFiles,
|
|
10690
11416
|
extractXrayRawArgs,
|
|
11417
|
+
fetchOfflineSnapshot,
|
|
10691
11418
|
formatOfflineLargeFilePushFailureMessage,
|
|
11419
|
+
formatOfflineRequestForError,
|
|
10692
11420
|
getBenchUsageText,
|
|
10693
11421
|
hasFlag,
|
|
11422
|
+
isOfflineSnapshotPostFallbackError,
|
|
10694
11423
|
main,
|
|
11424
|
+
offlinePartialHydrationForPaths,
|
|
11425
|
+
offlineSnapshotBasePostBody,
|
|
11426
|
+
offlineSnapshotBasePostBodyFits,
|
|
11427
|
+
offlineSnapshotBasePostRequest,
|
|
11428
|
+
offlineSnapshotContentFilesForApply,
|
|
10695
11429
|
parseBenchArgs,
|
|
10696
11430
|
parseCapsuleForkArgs,
|
|
10697
11431
|
parseConnectorConfig,
|
|
11432
|
+
parseOfflineSyncRequestTimeoutMs,
|
|
10698
11433
|
parseTaxonomyResolveArgs,
|
|
10699
11434
|
parseTrainingExportArgs,
|
|
11435
|
+
pushOfflineFileContent,
|
|
11436
|
+
pushOfflineFileContentFromChunkReader,
|
|
10700
11437
|
renderQueryTextLines,
|
|
10701
11438
|
resolveConfigPath,
|
|
10702
11439
|
resolveFlag,
|
|
10703
11440
|
resolveMemoryDir,
|
|
10704
11441
|
resolveSyncSourceDir,
|
|
11442
|
+
runOfflineSyncOnce,
|
|
10705
11443
|
runTrainingExport,
|
|
10706
11444
|
runXrayCommand,
|
|
10707
11445
|
shouldDirectHydrateOfflineFile,
|