@machinemetrics/mm-erp-sdk 0.1.5-beta.0 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{config-2l5vnNkA.js → config-qat9zgOl.js} +6 -6
- package/dist/{config-2l5vnNkA.js.map → config-qat9zgOl.js.map} +1 -1
- package/dist/{connector-factory-CQ8e7Tae.js → connector-factory-C2czCs9v.js} +12 -3
- package/dist/connector-factory-C2czCs9v.js.map +1 -0
- package/dist/{hashed-cache-manager-Ci59eC75.js → hashed-cache-manager-CzyFSt2B.js} +5 -4
- package/dist/{hashed-cache-manager-Ci59eC75.js.map → hashed-cache-manager-CzyFSt2B.js.map} +1 -1
- package/dist/{index-CXbOvFyf.js → index-B9wo8pld.js} +7 -7
- package/dist/{index-CXbOvFyf.js.map → index-B9wo8pld.js.map} +1 -1
- package/dist/index.d.ts +14 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/{logger-QG73MndU.js → logger-Db8CkwR6.js} +929 -971
- package/dist/logger-Db8CkwR6.js.map +1 -0
- package/dist/mm-erp-sdk.js +417 -10
- package/dist/mm-erp-sdk.js.map +1 -1
- package/dist/services/data-sync-service/index.d.ts +1 -1
- package/dist/services/data-sync-service/index.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/clean-up-expired-cache.d.ts +2 -0
- package/dist/services/data-sync-service/jobs/clean-up-expired-cache.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js +42 -41
- package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js.map +1 -1
- package/dist/services/data-sync-service/jobs/from-erp.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/from-erp.js +11 -5
- package/dist/services/data-sync-service/jobs/from-erp.js.map +1 -1
- package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.d.ts +2 -0
- package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js +39 -40
- package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js.map +1 -1
- package/dist/services/data-sync-service/jobs/run-migrations.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/run-migrations.js +4 -3
- package/dist/services/data-sync-service/jobs/run-migrations.js.map +1 -1
- package/dist/services/data-sync-service/jobs/to-erp.d.ts.map +1 -1
- package/dist/services/data-sync-service/jobs/to-erp.js +15 -5
- package/dist/services/data-sync-service/jobs/to-erp.js.map +1 -1
- package/dist/services/erp-api-services/index.d.ts +5 -1
- package/dist/services/erp-api-services/index.d.ts.map +1 -1
- package/dist/services/mm-api-service/index.d.ts +3 -2
- package/dist/services/mm-api-service/index.d.ts.map +1 -1
- package/dist/services/mm-api-service/mm-api-service.d.ts +20 -0
- package/dist/services/mm-api-service/mm-api-service.d.ts.map +1 -1
- package/dist/types/erp-types.d.ts +1 -2
- package/dist/types/erp-types.d.ts.map +1 -1
- package/dist/utils/connector-factory.d.ts.map +1 -1
- package/dist/utils/connector-log/log-deduper.d.ts +56 -0
- package/dist/utils/connector-log/log-deduper.d.ts.map +1 -0
- package/dist/utils/connector-log/mm-connector-logger-example.d.ts +1 -0
- package/dist/utils/connector-log/mm-connector-logger-example.d.ts.map +1 -0
- package/dist/utils/connector-log/mm-connector-logger.d.ts +74 -0
- package/dist/utils/connector-log/mm-connector-logger.d.ts.map +1 -0
- package/dist/utils/error-utils.d.ts +2 -0
- package/dist/utils/error-utils.d.ts.map +1 -0
- package/dist/utils/index.d.ts +11 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/standard-process-drivers/index.d.ts +2 -1
- package/dist/utils/standard-process-drivers/index.d.ts.map +1 -1
- package/dist/utils/timezone.d.ts +7 -0
- package/dist/utils/timezone.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +19 -5
- package/src/services/data-sync-service/index.ts +1 -4
- package/src/services/data-sync-service/jobs/clean-up-expired-cache.ts +19 -7
- package/src/services/data-sync-service/jobs/from-erp.ts +12 -5
- package/src/services/data-sync-service/jobs/retry-failed-labor-tickets.ts +15 -5
- package/src/services/data-sync-service/jobs/run-migrations.ts +5 -2
- package/src/services/data-sync-service/jobs/to-erp.ts +17 -5
- package/src/services/erp-api-services/index.ts +9 -1
- package/src/services/mm-api-service/index.ts +1 -1
- package/src/services/mm-api-service/mm-api-service.ts +28 -0
- package/src/types/erp-types.ts +0 -1
- package/src/utils/application-initializer.ts +1 -1
- package/src/utils/connector-factory.ts +14 -3
- package/src/utils/connector-log/log-deduper.ts +284 -0
- package/src/utils/connector-log/mm-connector-logger-example.ts +97 -0
- package/src/utils/connector-log/mm-connector-logger.ts +177 -0
- package/src/utils/error-utils.ts +18 -0
- package/src/utils/index.ts +12 -5
- package/src/utils/mm-labor-ticket-helpers.ts +2 -2
- package/src/utils/standard-process-drivers/index.ts +2 -4
- package/src/utils/timezone.ts +28 -0
- package/dist/connector-factory-CQ8e7Tae.js.map +0 -1
- package/dist/logger-QG73MndU.js.map +0 -1
package/dist/mm-erp-sdk.js
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import { C as CoreConfiguration, H as HashedCacheManager } from "./hashed-cache-manager-
|
|
2
|
-
import { g, a } from "./hashed-cache-manager-
|
|
3
|
-
import { l as logger } from "./logger-
|
|
4
|
-
import { g as getCachedMMToken, s as setCachedMMToken, a as setTimezoneOffsetInCache, b as getCachedTimezoneOffset, S as SQLiteCoordinator } from "./index-
|
|
5
|
-
import { c, d } from "./index-
|
|
1
|
+
import { C as CoreConfiguration, H as HashedCacheManager } from "./hashed-cache-manager-CzyFSt2B.js";
|
|
2
|
+
import { E, g, a } from "./hashed-cache-manager-CzyFSt2B.js";
|
|
3
|
+
import { l as logger } from "./logger-Db8CkwR6.js";
|
|
4
|
+
import { g as getCachedMMToken, s as setCachedMMToken, a as setTimezoneOffsetInCache, b as getCachedTimezoneOffset, S as SQLiteCoordinator } from "./index-B9wo8pld.js";
|
|
5
|
+
import { c, d } from "./index-B9wo8pld.js";
|
|
6
6
|
import axios, { AxiosError } from "axios";
|
|
7
7
|
import knex from "knex";
|
|
8
8
|
import { c as config } from "./knexfile-1qKKIORB.js";
|
|
9
|
-
import "
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import "./connector-factory-C2czCs9v.js";
|
|
10
12
|
import Bree from "bree";
|
|
11
13
|
import Graceful from "@ladjs/graceful";
|
|
12
|
-
import path from "path";
|
|
13
14
|
import { fileURLToPath } from "url";
|
|
14
15
|
import sql from "mssql";
|
|
15
16
|
import { z } from "zod";
|
|
@@ -21,7 +22,6 @@ var ERPType = /* @__PURE__ */ ((ERPType2) => {
|
|
|
21
22
|
ERPType2["PROSHOP"] = "PROSHOP";
|
|
22
23
|
ERPType2["SYTELINE"] = "SYTELINE";
|
|
23
24
|
ERPType2["TEMPLATE"] = "TEMPLATE";
|
|
24
|
-
ERPType2["GLOBALSHOP"] = "GLOBALSHOP";
|
|
25
25
|
return ERPType2;
|
|
26
26
|
})(ERPType || {});
|
|
27
27
|
var ERPObjType = /* @__PURE__ */ ((ERPObjType2) => {
|
|
@@ -929,6 +929,38 @@ class MMApiClient {
|
|
|
929
929
|
(ticket) => new MMReceiveLaborTicket(ticket)
|
|
930
930
|
);
|
|
931
931
|
}
|
|
932
|
+
/**
|
|
933
|
+
* Send connector logs to the MM API
|
|
934
|
+
* @param logEntry Single log entry to send
|
|
935
|
+
* @returns Promise with the API response
|
|
936
|
+
*/
|
|
937
|
+
async sendConnectorLog(logEntry) {
|
|
938
|
+
return await this.postData(
|
|
939
|
+
"/connector/logs",
|
|
940
|
+
logEntry,
|
|
941
|
+
{},
|
|
942
|
+
{
|
|
943
|
+
baseUrl: "ApiBase"
|
|
944
|
+
/* ApiBase */
|
|
945
|
+
}
|
|
946
|
+
);
|
|
947
|
+
}
|
|
948
|
+
/**
|
|
949
|
+
* Send bulk connector logs to the MM API
|
|
950
|
+
* @param logs Array of log entries to send
|
|
951
|
+
* @returns Promise with the API response
|
|
952
|
+
*/
|
|
953
|
+
async sendBulkConnectorLogs(logs) {
|
|
954
|
+
return await this.postData(
|
|
955
|
+
"/connector/logs",
|
|
956
|
+
{ logs },
|
|
957
|
+
{},
|
|
958
|
+
{
|
|
959
|
+
baseUrl: "ApiBase"
|
|
960
|
+
/* ApiBase */
|
|
961
|
+
}
|
|
962
|
+
);
|
|
963
|
+
}
|
|
932
964
|
async deleteFailedLaborTicketIds(system, laborTicketRefs) {
|
|
933
965
|
return await this.postData(
|
|
934
966
|
`${this.resourceURLs[ERPObjType.LABOR_TICKETS]}/failed/remove`,
|
|
@@ -1429,6 +1461,23 @@ const formatDateWithTZOffset = (date, timezoneOffset) => {
|
|
|
1429
1461
|
const minutes = Math.floor(absOffset % 1 * 60).toString().padStart(2, "0");
|
|
1430
1462
|
return `${isoDate}${sign}${hours}:${minutes}`;
|
|
1431
1463
|
};
|
|
1464
|
+
const toISOWithOffset = (date, timezoneOffset) => {
|
|
1465
|
+
const sign = timezoneOffset >= 0 ? "+" : "-";
|
|
1466
|
+
const abs = Math.abs(timezoneOffset);
|
|
1467
|
+
const hours = Math.floor(abs);
|
|
1468
|
+
const minutes = Math.round((abs - hours) * 60);
|
|
1469
|
+
const pad2 = (n) => n.toString().padStart(2, "0");
|
|
1470
|
+
const pad3 = (n) => n.toString().padStart(3, "0");
|
|
1471
|
+
const yyyy = date.getUTCFullYear();
|
|
1472
|
+
const MM = pad2(date.getUTCMonth() + 1);
|
|
1473
|
+
const dd = pad2(date.getUTCDate());
|
|
1474
|
+
const HH = pad2(date.getUTCHours());
|
|
1475
|
+
const mm = pad2(date.getUTCMinutes());
|
|
1476
|
+
const ss = pad2(date.getUTCSeconds());
|
|
1477
|
+
const SSS = pad3(date.getUTCMilliseconds());
|
|
1478
|
+
const off = `${sign}${pad2(hours)}:${pad2(minutes)}`;
|
|
1479
|
+
return `${yyyy}-${MM}-${dd}T${HH}:${mm}:${ss}.${SSS}${off}`;
|
|
1480
|
+
};
|
|
1432
1481
|
function calculateTimeDifferenceInHours(startTime, endTime, timezoneOffset) {
|
|
1433
1482
|
if (!startTime || !endTime) return 0;
|
|
1434
1483
|
const localStartTime = convertToLocalTime(startTime, timezoneOffset);
|
|
@@ -1611,7 +1660,7 @@ function convertLaborTicketToLocalTimezone(laborTicket, timezoneOffset) {
|
|
|
1611
1660
|
];
|
|
1612
1661
|
timeFields.forEach((field) => {
|
|
1613
1662
|
const localTime = convertToLocalTime(laborTicket[field], timezoneOffset);
|
|
1614
|
-
laborTicket[field] = localTime
|
|
1663
|
+
laborTicket[field] = localTime ? toISOWithOffset(localTime, timezoneOffset) : null;
|
|
1615
1664
|
});
|
|
1616
1665
|
return laborTicket;
|
|
1617
1666
|
}
|
|
@@ -3042,6 +3091,353 @@ function getERPAPITypeFromEntity(entity, entityMap) {
|
|
|
3042
3091
|
);
|
|
3043
3092
|
return entry ? Number(entry[0]) : void 0;
|
|
3044
3093
|
}
|
|
3094
|
+
const isNonEmptyString = (v) => typeof v === "string" && v.trim().length > 0;
|
|
3095
|
+
function getErrorType(error) {
|
|
3096
|
+
if (error && typeof error === "object") {
|
|
3097
|
+
const o = error;
|
|
3098
|
+
if (isNonEmptyString(o.code)) return o.code;
|
|
3099
|
+
if (isNonEmptyString(o.name)) return o.name;
|
|
3100
|
+
const ctorName = o.constructor?.name;
|
|
3101
|
+
if (isNonEmptyString(ctorName) && ctorName !== "Object") return ctorName;
|
|
3102
|
+
}
|
|
3103
|
+
return "Error";
|
|
3104
|
+
}
|
|
3105
|
+
class LogEntry {
|
|
3106
|
+
level;
|
|
3107
|
+
message;
|
|
3108
|
+
dedupeKey;
|
|
3109
|
+
eventTime;
|
|
3110
|
+
constructor(params) {
|
|
3111
|
+
this.level = params.level;
|
|
3112
|
+
this.message = params.message;
|
|
3113
|
+
this.dedupeKey = params.dedupeKey;
|
|
3114
|
+
this.eventTime = Date.now();
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
function isLogResponse(value) {
|
|
3118
|
+
if (value === null || typeof value !== "object") return false;
|
|
3119
|
+
const v = value;
|
|
3120
|
+
if (typeof v.message !== "string") return false;
|
|
3121
|
+
if ("processed" in v && typeof v.processed !== "number") return false;
|
|
3122
|
+
return true;
|
|
3123
|
+
}
|
|
3124
|
+
class MMConnectorLogger {
|
|
3125
|
+
MAX_MSG_LEN = 2e3;
|
|
3126
|
+
mmApiClient;
|
|
3127
|
+
deduper;
|
|
3128
|
+
source;
|
|
3129
|
+
constructor(source, deduper) {
|
|
3130
|
+
if (source.length < 1 || source.length > 64) {
|
|
3131
|
+
throw new Error("source must be 1-64 characters");
|
|
3132
|
+
}
|
|
3133
|
+
this.mmApiClient = new MMApiClient();
|
|
3134
|
+
this.deduper = deduper;
|
|
3135
|
+
this.source = source;
|
|
3136
|
+
}
|
|
3137
|
+
// Deduplication helpers are delegated to injected FileLogDeduper
|
|
3138
|
+
/**
|
|
3139
|
+
* Send a single log entry to the MM cloud with deduplication.
|
|
3140
|
+
*
|
|
3141
|
+
* The deduplication is handled by the injected LogDeduper.
|
|
3142
|
+
* If no deduper is injected, the log entry is sent without deduplication.
|
|
3143
|
+
*
|
|
3144
|
+
* The standard deduper, FileLogDeduper, stores the deduplication state in a file,
|
|
3145
|
+
* allowing deduplication across jobs,
|
|
3146
|
+
*
|
|
3147
|
+
* @param logEntry - The log entry to send
|
|
3148
|
+
* @returns Promise resolving to the API response or null if suppressed
|
|
3149
|
+
* @throws HTTPError if the request fails or Error if the log entry is invalid
|
|
3150
|
+
*/
|
|
3151
|
+
async sendLog(logEntry) {
|
|
3152
|
+
this.validateLogEntry(logEntry);
|
|
3153
|
+
const now = Date.now();
|
|
3154
|
+
let messageToSend = logEntry.message;
|
|
3155
|
+
if (this.deduper) {
|
|
3156
|
+
const decision = await this.deduper.decide(logEntry, now);
|
|
3157
|
+
if (decision === null) return null;
|
|
3158
|
+
messageToSend = decision;
|
|
3159
|
+
}
|
|
3160
|
+
try {
|
|
3161
|
+
const logEntryToSend = {
|
|
3162
|
+
source: this.source,
|
|
3163
|
+
level: logEntry.level,
|
|
3164
|
+
message: messageToSend
|
|
3165
|
+
};
|
|
3166
|
+
const response = await this.mmApiClient.sendConnectorLog(logEntryToSend);
|
|
3167
|
+
if (this.deduper) {
|
|
3168
|
+
await this.deduper.onSuccess(logEntry, now);
|
|
3169
|
+
}
|
|
3170
|
+
if (!isLogResponse(response)) {
|
|
3171
|
+
logger.warn("Unexpected success response format from MM API for connector log", { response });
|
|
3172
|
+
return { message: "Unexpected success response format when sending log" };
|
|
3173
|
+
}
|
|
3174
|
+
return { message: response.message };
|
|
3175
|
+
} catch (error) {
|
|
3176
|
+
logger.error("Failed to send log to MM cloud", {
|
|
3177
|
+
level: logEntry.level,
|
|
3178
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
3179
|
+
});
|
|
3180
|
+
throw error;
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
/**
|
|
3184
|
+
* @throws Error if validation fails
|
|
3185
|
+
*/
|
|
3186
|
+
validateLogEntry(logEntry) {
|
|
3187
|
+
const allowedLevels = ["info", "warn", "error"];
|
|
3188
|
+
if (!logEntry.level || !allowedLevels.includes(logEntry.level)) {
|
|
3189
|
+
throw new Error(`level must be one of: ${allowedLevels.join(", ")}`);
|
|
3190
|
+
}
|
|
3191
|
+
if (!logEntry.message || typeof logEntry.message !== "string") {
|
|
3192
|
+
throw new Error("message is required and must be a string");
|
|
3193
|
+
}
|
|
3194
|
+
logEntry.message = logEntry.message.slice(0, this.MAX_MSG_LEN);
|
|
3195
|
+
if (!logEntry.dedupeKey || typeof logEntry.dedupeKey !== "string") {
|
|
3196
|
+
throw new Error("dedupeKey is required and must be a string");
|
|
3197
|
+
}
|
|
3198
|
+
if (logEntry.dedupeKey.trim().length < 1) {
|
|
3199
|
+
throw new Error("dedupeKey must be a non-empty string");
|
|
3200
|
+
}
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* Retry all failed transmissions silently
|
|
3204
|
+
* This method attempts to retry all messages that failed to transmit
|
|
3205
|
+
* and removes them from the failed list if successful, else leaves them for the client to retry
|
|
3206
|
+
*
|
|
3207
|
+
* Expected usage is by a client to call this as part of its own retry mechanism
|
|
3208
|
+
*/
|
|
3209
|
+
async retryFailedTransmissions() {
|
|
3210
|
+
if (!this.deduper || !this.deduper.retryFailedTransmissions) {
|
|
3211
|
+
return;
|
|
3212
|
+
}
|
|
3213
|
+
await this.deduper.retryFailedTransmissions(async (entry, message) => {
|
|
3214
|
+
await this.mmApiClient.sendConnectorLog({
|
|
3215
|
+
source: this.source,
|
|
3216
|
+
level: entry.level,
|
|
3217
|
+
message
|
|
3218
|
+
});
|
|
3219
|
+
});
|
|
3220
|
+
}
|
|
3221
|
+
/**
|
|
3222
|
+
* Clean up resources
|
|
3223
|
+
*/
|
|
3224
|
+
async destroy() {
|
|
3225
|
+
await this.mmApiClient.destroy();
|
|
3226
|
+
}
|
|
3227
|
+
}
|
|
3228
|
+
class FileLogDeduper {
|
|
3229
|
+
storeFilePath;
|
|
3230
|
+
windowMs;
|
|
3231
|
+
ttlMs;
|
|
3232
|
+
sweepIntervalMs;
|
|
3233
|
+
lastSweepTsMs;
|
|
3234
|
+
DEFAULT_WINDOW_TEN_MINS = 600;
|
|
3235
|
+
DEFAULT_TTL_ONE_HOUR = 3600;
|
|
3236
|
+
DEFAULT_SWEEP_INTERVAL_FIVE_MINS = 300;
|
|
3237
|
+
DEFAULT_STORE_FILE_PATH = path.join("/tmp", "log-deduplication.json");
|
|
3238
|
+
/**
|
|
3239
|
+
* Ctor.
|
|
3240
|
+
* @param storeFilePath: The path to the file where the deduplication store is stored; recommended is to use the default
|
|
3241
|
+
* @param windowSeconds: Suppression window. Duplicates within this period are suppressed.
|
|
3242
|
+
* @param ttlSeconds: Eviction TTL. Store entries for keys inactive beyond this are removed. Enforced to be ≥ windowSeconds.
|
|
3243
|
+
* @param sweepIntervalSeconds: Efficiency parameter. How often (min interval) to run opportunistic eviction; retry always sweeps
|
|
3244
|
+
* The sweep is lazy, used only when the store is accessed
|
|
3245
|
+
*/
|
|
3246
|
+
constructor({
|
|
3247
|
+
storeFilePath = this.DEFAULT_STORE_FILE_PATH,
|
|
3248
|
+
windowSeconds = this.DEFAULT_WINDOW_TEN_MINS,
|
|
3249
|
+
ttlSeconds = this.DEFAULT_TTL_ONE_HOUR,
|
|
3250
|
+
sweepIntervalSeconds = this.DEFAULT_SWEEP_INTERVAL_FIVE_MINS
|
|
3251
|
+
} = {}) {
|
|
3252
|
+
this.storeFilePath = storeFilePath;
|
|
3253
|
+
this.windowMs = Math.max(1, windowSeconds) * 1e3;
|
|
3254
|
+
this.ttlMs = Math.max(this.windowMs, Math.max(1, ttlSeconds) * 1e3);
|
|
3255
|
+
this.sweepIntervalMs = Math.max(1, sweepIntervalSeconds) * 1e3;
|
|
3256
|
+
this.lastSweepTsMs = 0;
|
|
3257
|
+
this.ensureStoreFileExists();
|
|
3258
|
+
}
|
|
3259
|
+
/**
|
|
3260
|
+
* Deduplication gating function
|
|
3261
|
+
* Returns the formatted message to send, or null to suppress
|
|
3262
|
+
* Decision is based on the dedupeKey and the time of the entry
|
|
3263
|
+
*/
|
|
3264
|
+
async decide(entry, now) {
|
|
3265
|
+
if (!entry.dedupeKey || typeof entry.dedupeKey !== "string" || entry.dedupeKey.trim().length === 0) {
|
|
3266
|
+
throw new Error("dedupeKey is required and must be a non-empty string");
|
|
3267
|
+
}
|
|
3268
|
+
const key = entry.dedupeKey;
|
|
3269
|
+
return this.withLock(async () => {
|
|
3270
|
+
const store = this.readStore();
|
|
3271
|
+
if (now - this.lastSweepTsMs >= this.sweepIntervalMs) {
|
|
3272
|
+
this.evictExpiredInStore(store, now);
|
|
3273
|
+
this.lastSweepTsMs = now;
|
|
3274
|
+
this.writeStore(store);
|
|
3275
|
+
}
|
|
3276
|
+
const existing = store[key];
|
|
3277
|
+
if (existing) {
|
|
3278
|
+
const withinWindow = existing.lastTransmitted > 0 && existing.lastTransmitted + this.windowMs > now;
|
|
3279
|
+
if (withinWindow) {
|
|
3280
|
+
store[key] = {
|
|
3281
|
+
...existing,
|
|
3282
|
+
suppressedCount: existing.suppressedCount + 1,
|
|
3283
|
+
firstUnsentEventTs: existing.suppressedCount === 0 ? entry.eventTime ?? now : existing.firstUnsentEventTs,
|
|
3284
|
+
lastEventTs: entry.eventTime ?? now,
|
|
3285
|
+
level: entry.level,
|
|
3286
|
+
message: entry.message
|
|
3287
|
+
};
|
|
3288
|
+
this.writeStore(store);
|
|
3289
|
+
return null;
|
|
3290
|
+
}
|
|
3291
|
+
const messageToSend2 = this.formatMessage(entry.message, entry.eventTime ?? now, existing.suppressedCount, existing.firstUnsentEventTs);
|
|
3292
|
+
store[key] = {
|
|
3293
|
+
...existing,
|
|
3294
|
+
suppressedCount: 0,
|
|
3295
|
+
firstUnsentEventTs: 0,
|
|
3296
|
+
lastEventTs: entry.eventTime ?? now,
|
|
3297
|
+
level: entry.level,
|
|
3298
|
+
message: entry.message
|
|
3299
|
+
};
|
|
3300
|
+
this.writeStore(store);
|
|
3301
|
+
return messageToSend2;
|
|
3302
|
+
}
|
|
3303
|
+
const messageToSend = this.formatMessage(entry.message, entry.eventTime ?? now, 0);
|
|
3304
|
+
store[key] = {
|
|
3305
|
+
lastTransmitted: 0,
|
|
3306
|
+
suppressedCount: 0,
|
|
3307
|
+
firstUnsentEventTs: entry.eventTime ?? now,
|
|
3308
|
+
lastEventTs: entry.eventTime ?? now,
|
|
3309
|
+
level: entry.level,
|
|
3310
|
+
message: entry.message
|
|
3311
|
+
};
|
|
3312
|
+
this.writeStore(store);
|
|
3313
|
+
return messageToSend;
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
async onSuccess(entry, now) {
|
|
3317
|
+
if (!entry.dedupeKey || typeof entry.dedupeKey !== "string" || entry.dedupeKey.trim().length === 0) {
|
|
3318
|
+
throw new Error("dedupeKey is required and must be a non-empty string");
|
|
3319
|
+
}
|
|
3320
|
+
const key = entry.dedupeKey;
|
|
3321
|
+
await this.withLock(async () => {
|
|
3322
|
+
const store = this.readStore();
|
|
3323
|
+
const existing = store[key];
|
|
3324
|
+
if (existing) {
|
|
3325
|
+
store[key] = {
|
|
3326
|
+
...existing,
|
|
3327
|
+
lastTransmitted: now,
|
|
3328
|
+
firstUnsentEventTs: 0,
|
|
3329
|
+
suppressedCount: 0
|
|
3330
|
+
};
|
|
3331
|
+
this.writeStore(store);
|
|
3332
|
+
}
|
|
3333
|
+
});
|
|
3334
|
+
}
|
|
3335
|
+
async retryFailedTransmissions(send) {
|
|
3336
|
+
const now = Date.now();
|
|
3337
|
+
const entries = await this.withLock(async () => {
|
|
3338
|
+
const store = this.readStore();
|
|
3339
|
+
this.evictExpiredInStore(store, now);
|
|
3340
|
+
this.lastSweepTsMs = now;
|
|
3341
|
+
this.writeStore(store);
|
|
3342
|
+
return Object.entries(store).filter(([, rec]) => rec.lastTransmitted === 0).map(([key, rec]) => ({ key, rec }));
|
|
3343
|
+
});
|
|
3344
|
+
for (const { key, rec } of entries) {
|
|
3345
|
+
try {
|
|
3346
|
+
const message = this.formatMessage(rec.message, rec.lastEventTs, rec.suppressedCount, rec.firstUnsentEventTs);
|
|
3347
|
+
await send({ level: rec.level, message: rec.message, dedupeKey: key, eventTime: rec.lastEventTs }, message);
|
|
3348
|
+
await this.withLock(async () => {
|
|
3349
|
+
const store = this.readStore();
|
|
3350
|
+
const current = store[key];
|
|
3351
|
+
if (current) {
|
|
3352
|
+
store[key] = {
|
|
3353
|
+
...current,
|
|
3354
|
+
lastTransmitted: Date.now(),
|
|
3355
|
+
suppressedCount: 0
|
|
3356
|
+
};
|
|
3357
|
+
this.writeStore(store);
|
|
3358
|
+
}
|
|
3359
|
+
});
|
|
3360
|
+
} catch (err) {
|
|
3361
|
+
logger.error("Failed to retry failed transmission", { key, rec, error: err });
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
// --- Internals ---
|
|
3367
|
+
ensureStoreFileExists() {
|
|
3368
|
+
try {
|
|
3369
|
+
if (!fs.existsSync(this.storeFilePath)) {
|
|
3370
|
+
fs.writeFileSync(this.storeFilePath, JSON.stringify({}), "utf-8");
|
|
3371
|
+
}
|
|
3372
|
+
} catch {
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3375
|
+
readStore() {
|
|
3376
|
+
try {
|
|
3377
|
+
if (!fs.existsSync(this.storeFilePath)) return {};
|
|
3378
|
+
const content = fs.readFileSync(this.storeFilePath, "utf-8");
|
|
3379
|
+
return content ? JSON.parse(content) : {};
|
|
3380
|
+
} catch {
|
|
3381
|
+
return {};
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
writeStore(store) {
|
|
3385
|
+
try {
|
|
3386
|
+
fs.writeFileSync(this.storeFilePath, JSON.stringify(store, null, 2), "utf-8");
|
|
3387
|
+
} catch {
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3390
|
+
formatMessage(message, eventTs, suppressedCount, firstUnsentEventTs) {
|
|
3391
|
+
const timestamp = new Date(eventTs).toISOString();
|
|
3392
|
+
const base = `${timestamp} | ${message}`;
|
|
3393
|
+
if (suppressedCount > 0) {
|
|
3394
|
+
const since = firstUnsentEventTs && firstUnsentEventTs > 0 ? ` since ${new Date(firstUnsentEventTs).toISOString()}` : "";
|
|
3395
|
+
return `${base} (${suppressedCount} suppressed${since})`;
|
|
3396
|
+
}
|
|
3397
|
+
return base;
|
|
3398
|
+
}
|
|
3399
|
+
async withLock(fn) {
|
|
3400
|
+
const lockPath = `${this.storeFilePath}.lock`;
|
|
3401
|
+
const start = Date.now();
|
|
3402
|
+
while (true) {
|
|
3403
|
+
try {
|
|
3404
|
+
const fd = fs.openSync(lockPath, "wx");
|
|
3405
|
+
try {
|
|
3406
|
+
const result = await fn();
|
|
3407
|
+
return result;
|
|
3408
|
+
} finally {
|
|
3409
|
+
try {
|
|
3410
|
+
fs.closeSync(fd);
|
|
3411
|
+
} catch {
|
|
3412
|
+
}
|
|
3413
|
+
try {
|
|
3414
|
+
fs.unlinkSync(lockPath);
|
|
3415
|
+
} catch {
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
} catch {
|
|
3419
|
+
if (Date.now() - start > 3e3) {
|
|
3420
|
+
return await fn();
|
|
3421
|
+
}
|
|
3422
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
/**
|
|
3427
|
+
* Evict expired entries from the store based on the TTL and the key's last transmitted time
|
|
3428
|
+
*/
|
|
3429
|
+
evictExpiredInStore(store, now) {
|
|
3430
|
+
const keys = Object.keys(store);
|
|
3431
|
+
if (keys.length === 0) return;
|
|
3432
|
+
for (const key of keys) {
|
|
3433
|
+
const rec = store[key];
|
|
3434
|
+
const referenceTs = rec.lastTransmitted > 0 ? rec.lastTransmitted : rec.lastEventTs;
|
|
3435
|
+
if (now - referenceTs > this.ttlMs) {
|
|
3436
|
+
delete store[key];
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3045
3441
|
class ApplicationInitializer {
|
|
3046
3442
|
/**
|
|
3047
3443
|
* Performs all necessary application initialization tasks
|
|
@@ -3066,7 +3462,7 @@ class ApplicationInitializer {
|
|
|
3066
3462
|
);
|
|
3067
3463
|
} catch (error) {
|
|
3068
3464
|
logger.error("Critical initialization failure. Exiting.", error);
|
|
3069
|
-
process.
|
|
3465
|
+
process.exitCode = 1;
|
|
3070
3466
|
}
|
|
3071
3467
|
}
|
|
3072
3468
|
/**
|
|
@@ -3592,10 +3988,18 @@ export {
|
|
|
3592
3988
|
CoreConfiguration,
|
|
3593
3989
|
ERPObjType,
|
|
3594
3990
|
ERPType,
|
|
3991
|
+
E as ErpApiConnectionParams,
|
|
3595
3992
|
ErrorHandler,
|
|
3993
|
+
FileLogDeduper,
|
|
3994
|
+
GraphQLError,
|
|
3596
3995
|
GraphQLService,
|
|
3597
3996
|
HTTPClientFactory,
|
|
3997
|
+
HTTPError,
|
|
3998
|
+
LogEntry,
|
|
3598
3999
|
MMApiClient,
|
|
4000
|
+
MMBatchValidationError,
|
|
4001
|
+
MMConnectorLogger,
|
|
4002
|
+
MMReceiveLaborTicket,
|
|
3599
4003
|
MMSendLaborTicket,
|
|
3600
4004
|
MMSendPart,
|
|
3601
4005
|
MMSendPartOperation,
|
|
@@ -3605,6 +4009,7 @@ export {
|
|
|
3605
4009
|
MMSendWorkOrder,
|
|
3606
4010
|
MMSendWorkOrderOperation,
|
|
3607
4011
|
OAuthClient,
|
|
4012
|
+
RecordTrackingManager,
|
|
3608
4013
|
RestAPIService,
|
|
3609
4014
|
SqlServerHelper,
|
|
3610
4015
|
SqlServerService,
|
|
@@ -3618,10 +4023,12 @@ export {
|
|
|
3618
4023
|
formatDateWithTZOffset,
|
|
3619
4024
|
getCachedTimezoneOffset,
|
|
3620
4025
|
g as getErpApiConnectionParams,
|
|
4026
|
+
getErrorType,
|
|
3621
4027
|
c as getInitialLoadComplete,
|
|
3622
4028
|
getPayloadWithoutIDField,
|
|
3623
4029
|
a as getSQLServerConfiguration,
|
|
3624
4030
|
getUniqueRows,
|
|
4031
|
+
config as knexDatabaseConfig,
|
|
3625
4032
|
logger,
|
|
3626
4033
|
removeExtraneousFields,
|
|
3627
4034
|
runDataSyncService,
|