@machinemetrics/mm-erp-sdk 0.1.3 → 0.1.4-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{config-2l5vnNkA.js → config-WKwu1mMo.js} +6 -6
- package/dist/{config-2l5vnNkA.js.map → config-WKwu1mMo.js.map} +1 -1
- package/dist/{connector-factory-CQ8e7Tae.js → connector-factory-DFv3ex0X.js} +2 -2
- package/dist/{connector-factory-CQ8e7Tae.js.map → connector-factory-DFv3ex0X.js.map} +1 -1
- package/dist/{hashed-cache-manager-Ci59eC75.js → hashed-cache-manager-INiCs0JC.js} +4 -4
- package/dist/{hashed-cache-manager-Ci59eC75.js.map → hashed-cache-manager-INiCs0JC.js.map} +1 -1
- package/dist/{index-CXbOvFyf.js → index-aci_wdcn.js} +7 -7
- package/dist/{index-CXbOvFyf.js.map → index-aci_wdcn.js.map} +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/{logger-QG73MndU.js → logger-hqtl8hFM.js} +6 -6
- package/dist/{logger-QG73MndU.js.map → logger-hqtl8hFM.js.map} +1 -1
- package/dist/mm-erp-sdk.js +389 -7
- package/dist/mm-erp-sdk.js.map +1 -1
- package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js +4 -4
- package/dist/services/data-sync-service/jobs/from-erp.js +4 -4
- package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js +3 -3
- package/dist/services/data-sync-service/jobs/run-migrations.js +1 -1
- package/dist/services/data-sync-service/jobs/to-erp.js +3 -3
- package/dist/services/erp-api-services/index.d.ts +4 -1
- package/dist/services/erp-api-services/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/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 +9 -1
- 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/package.json +1 -1
- package/src/index.ts +8 -2
- package/src/services/erp-api-services/index.ts +6 -1
- package/src/services/mm-api-service/mm-api-service.ts +28 -0
- package/src/utils/connector-log/log-deduper.ts +282 -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 +10 -4
- package/src/utils/standard-process-drivers/index.ts +2 -4
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-INiCs0JC.js";
|
|
2
|
+
import { g, a } from "./hashed-cache-manager-INiCs0JC.js";
|
|
3
|
+
import { l as logger } from "./logger-hqtl8hFM.js";
|
|
4
|
+
import { g as getCachedMMToken, s as setCachedMMToken, a as setTimezoneOffsetInCache, b as getCachedTimezoneOffset, S as SQLiteCoordinator } from "./index-aci_wdcn.js";
|
|
5
|
+
import { c, d } from "./index-aci_wdcn.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-DFv3ex0X.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";
|
|
@@ -928,6 +929,38 @@ class MMApiClient {
|
|
|
928
929
|
(ticket) => new MMReceiveLaborTicket(ticket)
|
|
929
930
|
);
|
|
930
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
|
+
}
|
|
931
964
|
async deleteFailedLaborTicketIds(system, laborTicketRefs) {
|
|
932
965
|
return await this.postData(
|
|
933
966
|
`${this.resourceURLs[ERPObjType.LABOR_TICKETS]}/failed/remove`,
|
|
@@ -3041,6 +3074,351 @@ function getERPAPITypeFromEntity(entity, entityMap) {
|
|
|
3041
3074
|
);
|
|
3042
3075
|
return entry ? Number(entry[0]) : void 0;
|
|
3043
3076
|
}
|
|
3077
|
+
const isNonEmptyString = (v) => typeof v === "string" && v.trim().length > 0;
|
|
3078
|
+
function getErrorType(error) {
|
|
3079
|
+
if (error && typeof error === "object") {
|
|
3080
|
+
const o = error;
|
|
3081
|
+
if (isNonEmptyString(o.code)) return o.code;
|
|
3082
|
+
if (isNonEmptyString(o.name)) return o.name;
|
|
3083
|
+
const ctorName = o.constructor?.name;
|
|
3084
|
+
if (isNonEmptyString(ctorName) && ctorName !== "Object") return ctorName;
|
|
3085
|
+
}
|
|
3086
|
+
return "Error";
|
|
3087
|
+
}
|
|
3088
|
+
class LogEntry {
|
|
3089
|
+
level;
|
|
3090
|
+
message;
|
|
3091
|
+
dedupeKey;
|
|
3092
|
+
eventTime;
|
|
3093
|
+
constructor(params) {
|
|
3094
|
+
this.level = params.level;
|
|
3095
|
+
this.message = params.message;
|
|
3096
|
+
this.dedupeKey = params.dedupeKey;
|
|
3097
|
+
this.eventTime = Date.now();
|
|
3098
|
+
}
|
|
3099
|
+
}
|
|
3100
|
+
function isLogResponse(value) {
|
|
3101
|
+
if (value === null || typeof value !== "object") return false;
|
|
3102
|
+
const v = value;
|
|
3103
|
+
if (typeof v.message !== "string") return false;
|
|
3104
|
+
if ("processed" in v && typeof v.processed !== "number") return false;
|
|
3105
|
+
return true;
|
|
3106
|
+
}
|
|
3107
|
+
class MMConnectorLogger {
|
|
3108
|
+
MAX_MSG_LEN = 2e3;
|
|
3109
|
+
mmApiClient;
|
|
3110
|
+
deduper;
|
|
3111
|
+
source;
|
|
3112
|
+
constructor(source, deduper) {
|
|
3113
|
+
if (source.length < 1 || source.length > 64) {
|
|
3114
|
+
throw new Error("source must be 1-64 characters");
|
|
3115
|
+
}
|
|
3116
|
+
this.mmApiClient = new MMApiClient();
|
|
3117
|
+
this.deduper = deduper;
|
|
3118
|
+
this.source = source;
|
|
3119
|
+
}
|
|
3120
|
+
// Deduplication helpers are delegated to injected FileLogDeduper
|
|
3121
|
+
/**
|
|
3122
|
+
* Send a single log entry to the MM cloud with deduplication.
|
|
3123
|
+
*
|
|
3124
|
+
* The deduplication is handled by the injected LogDeduper.
|
|
3125
|
+
* If no deduper is injected, the log entry is sent without deduplication.
|
|
3126
|
+
*
|
|
3127
|
+
* The standard deduper, FileLogDeduper, stores the deduplication state in a file,
|
|
3128
|
+
* allowing deduplication across jobs,
|
|
3129
|
+
*
|
|
3130
|
+
* @param logEntry - The log entry to send
|
|
3131
|
+
* @returns Promise resolving to the API response or null if suppressed
|
|
3132
|
+
* @throws HTTPError if the request fails or Error if the log entry is invalid
|
|
3133
|
+
*/
|
|
3134
|
+
async sendLog(logEntry) {
|
|
3135
|
+
this.validateLogEntry(logEntry);
|
|
3136
|
+
const now = Date.now();
|
|
3137
|
+
let messageToSend = logEntry.message;
|
|
3138
|
+
if (this.deduper) {
|
|
3139
|
+
const decision = await this.deduper.decide(logEntry, now);
|
|
3140
|
+
if (decision === null) return null;
|
|
3141
|
+
messageToSend = decision;
|
|
3142
|
+
}
|
|
3143
|
+
try {
|
|
3144
|
+
const logEntryToSend = {
|
|
3145
|
+
source: this.source,
|
|
3146
|
+
level: logEntry.level,
|
|
3147
|
+
message: messageToSend
|
|
3148
|
+
};
|
|
3149
|
+
const response = await this.mmApiClient.sendConnectorLog(logEntryToSend);
|
|
3150
|
+
if (this.deduper) {
|
|
3151
|
+
await this.deduper.onSuccess(logEntry, now);
|
|
3152
|
+
}
|
|
3153
|
+
if (!isLogResponse(response)) {
|
|
3154
|
+
logger.warn("Unexpected success response format from MM API for connector log", { response });
|
|
3155
|
+
return { message: "Unexpected success response format when sending log" };
|
|
3156
|
+
}
|
|
3157
|
+
return { message: response.message };
|
|
3158
|
+
} catch (error) {
|
|
3159
|
+
logger.error("Failed to send log to MM cloud", {
|
|
3160
|
+
level: logEntry.level,
|
|
3161
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
3162
|
+
});
|
|
3163
|
+
throw error;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
/**
|
|
3167
|
+
* @throws Error if validation fails
|
|
3168
|
+
*/
|
|
3169
|
+
validateLogEntry(logEntry) {
|
|
3170
|
+
const allowedLevels = ["info", "warn", "error"];
|
|
3171
|
+
if (!logEntry.level || !allowedLevels.includes(logEntry.level)) {
|
|
3172
|
+
throw new Error(`level must be one of: ${allowedLevels.join(", ")}`);
|
|
3173
|
+
}
|
|
3174
|
+
if (!logEntry.message || typeof logEntry.message !== "string") {
|
|
3175
|
+
throw new Error("message is required and must be a string");
|
|
3176
|
+
}
|
|
3177
|
+
logEntry.message = logEntry.message.slice(0, this.MAX_MSG_LEN);
|
|
3178
|
+
if (!logEntry.dedupeKey || typeof logEntry.dedupeKey !== "string") {
|
|
3179
|
+
throw new Error("dedupeKey is required and must be a string");
|
|
3180
|
+
}
|
|
3181
|
+
if (logEntry.dedupeKey.trim().length < 1) {
|
|
3182
|
+
throw new Error("dedupeKey must be a non-empty string");
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
/**
|
|
3186
|
+
* Retry all failed transmissions silently
|
|
3187
|
+
* This method attempts to retry all messages that failed to transmit
|
|
3188
|
+
* and removes them from the failed list if successful, else leaves them for the client to retry
|
|
3189
|
+
*
|
|
3190
|
+
* Expected usage is by a client to call this as part of its own retry mechanism
|
|
3191
|
+
*/
|
|
3192
|
+
async retryFailedTransmissions() {
|
|
3193
|
+
if (!this.deduper || !this.deduper.retryFailedTransmissions) {
|
|
3194
|
+
return;
|
|
3195
|
+
}
|
|
3196
|
+
await this.deduper.retryFailedTransmissions(async (entry, message) => {
|
|
3197
|
+
await this.mmApiClient.sendConnectorLog({
|
|
3198
|
+
source: this.source,
|
|
3199
|
+
level: entry.level,
|
|
3200
|
+
message
|
|
3201
|
+
});
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
3204
|
+
/**
|
|
3205
|
+
* Clean up resources
|
|
3206
|
+
*/
|
|
3207
|
+
async destroy() {
|
|
3208
|
+
await this.mmApiClient.destroy();
|
|
3209
|
+
}
|
|
3210
|
+
}
|
|
3211
|
+
class FileLogDeduper {
|
|
3212
|
+
storeFilePath;
|
|
3213
|
+
windowMs;
|
|
3214
|
+
ttlMs;
|
|
3215
|
+
sweepIntervalMs;
|
|
3216
|
+
lastSweepTsMs;
|
|
3217
|
+
DEFAULT_WINDOW_TEN_MINS = 600;
|
|
3218
|
+
DEFAULT_TTL_ONE_HOUR = 3600;
|
|
3219
|
+
DEFAULT_SWEEP_INTERVAL_FIVE_MINS = 300;
|
|
3220
|
+
DEFAULT_STORE_FILE_PATH = path.join("/tmp", "log-deduplication.json");
|
|
3221
|
+
/**
|
|
3222
|
+
* Ctor.
|
|
3223
|
+
* @param storeFilePath: The path to the file where the deduplication store is stored; recommended is to use the default
|
|
3224
|
+
* @param windowSeconds: Suppression window. Duplicates within this period are suppressed.
|
|
3225
|
+
* @param ttlSeconds: Eviction TTL. Store entries for keys inactive beyond this are removed. Enforced to be ≥ windowSeconds.
|
|
3226
|
+
* @param sweepIntervalSeconds: Efficiency parameter. How often (min interval) to run opportunistic eviction; retry always sweeps
|
|
3227
|
+
* The sweep is lazy, used only when the store is accessed
|
|
3228
|
+
*/
|
|
3229
|
+
constructor({
|
|
3230
|
+
storeFilePath = this.DEFAULT_STORE_FILE_PATH,
|
|
3231
|
+
windowSeconds = this.DEFAULT_WINDOW_TEN_MINS,
|
|
3232
|
+
ttlSeconds = this.DEFAULT_TTL_ONE_HOUR,
|
|
3233
|
+
sweepIntervalSeconds = this.DEFAULT_SWEEP_INTERVAL_FIVE_MINS
|
|
3234
|
+
} = {}) {
|
|
3235
|
+
this.storeFilePath = storeFilePath;
|
|
3236
|
+
this.windowMs = Math.max(1, windowSeconds) * 1e3;
|
|
3237
|
+
this.ttlMs = Math.max(this.windowMs, Math.max(1, ttlSeconds) * 1e3);
|
|
3238
|
+
this.sweepIntervalMs = Math.max(1, sweepIntervalSeconds) * 1e3;
|
|
3239
|
+
this.lastSweepTsMs = 0;
|
|
3240
|
+
this.ensureStoreFileExists();
|
|
3241
|
+
}
|
|
3242
|
+
/**
|
|
3243
|
+
* Deduplication gating function
|
|
3244
|
+
* Returns the formatted message to send, or null to suppress
|
|
3245
|
+
* Decision is based on the dedupeKey and the time of the entry
|
|
3246
|
+
*/
|
|
3247
|
+
async decide(entry, now) {
|
|
3248
|
+
if (!entry.dedupeKey || typeof entry.dedupeKey !== "string" || entry.dedupeKey.trim().length === 0) {
|
|
3249
|
+
throw new Error("dedupeKey is required and must be a non-empty string");
|
|
3250
|
+
}
|
|
3251
|
+
const key = entry.dedupeKey;
|
|
3252
|
+
return this.withLock(async () => {
|
|
3253
|
+
const store = this.readStore();
|
|
3254
|
+
if (now - this.lastSweepTsMs >= this.sweepIntervalMs) {
|
|
3255
|
+
this.evictExpiredInStore(store, now);
|
|
3256
|
+
this.lastSweepTsMs = now;
|
|
3257
|
+
this.writeStore(store);
|
|
3258
|
+
}
|
|
3259
|
+
const existing = store[key];
|
|
3260
|
+
if (existing) {
|
|
3261
|
+
const withinWindow = existing.lastTransmitted > 0 && existing.lastTransmitted + this.windowMs > now;
|
|
3262
|
+
if (withinWindow) {
|
|
3263
|
+
store[key] = {
|
|
3264
|
+
...existing,
|
|
3265
|
+
suppressedCount: existing.suppressedCount + 1,
|
|
3266
|
+
lastEventTs: entry.eventTime ?? now,
|
|
3267
|
+
level: entry.level,
|
|
3268
|
+
message: entry.message
|
|
3269
|
+
};
|
|
3270
|
+
this.writeStore(store);
|
|
3271
|
+
return null;
|
|
3272
|
+
}
|
|
3273
|
+
const messageToSend2 = this.formatMessage(entry.message, entry.eventTime ?? now, existing.suppressedCount);
|
|
3274
|
+
store[key] = {
|
|
3275
|
+
...existing,
|
|
3276
|
+
suppressedCount: 0,
|
|
3277
|
+
lastEventTs: entry.eventTime ?? now,
|
|
3278
|
+
level: entry.level,
|
|
3279
|
+
message: entry.message
|
|
3280
|
+
};
|
|
3281
|
+
this.writeStore(store);
|
|
3282
|
+
return messageToSend2;
|
|
3283
|
+
}
|
|
3284
|
+
const messageToSend = this.formatMessage(entry.message, entry.eventTime ?? now, 0);
|
|
3285
|
+
store[key] = {
|
|
3286
|
+
lastTransmitted: 0,
|
|
3287
|
+
suppressedCount: 0,
|
|
3288
|
+
firstUnsentEventTs: entry.eventTime ?? now,
|
|
3289
|
+
lastEventTs: entry.eventTime ?? now,
|
|
3290
|
+
level: entry.level,
|
|
3291
|
+
message: entry.message
|
|
3292
|
+
};
|
|
3293
|
+
this.writeStore(store);
|
|
3294
|
+
return messageToSend;
|
|
3295
|
+
});
|
|
3296
|
+
}
|
|
3297
|
+
async onSuccess(entry, now) {
|
|
3298
|
+
if (!entry.dedupeKey || typeof entry.dedupeKey !== "string" || entry.dedupeKey.trim().length === 0) {
|
|
3299
|
+
throw new Error("dedupeKey is required and must be a non-empty string");
|
|
3300
|
+
}
|
|
3301
|
+
const key = entry.dedupeKey;
|
|
3302
|
+
await this.withLock(async () => {
|
|
3303
|
+
const store = this.readStore();
|
|
3304
|
+
const existing = store[key];
|
|
3305
|
+
if (existing) {
|
|
3306
|
+
store[key] = {
|
|
3307
|
+
...existing,
|
|
3308
|
+
lastTransmitted: now,
|
|
3309
|
+
firstUnsentEventTs: 0,
|
|
3310
|
+
suppressedCount: 0
|
|
3311
|
+
};
|
|
3312
|
+
this.writeStore(store);
|
|
3313
|
+
}
|
|
3314
|
+
});
|
|
3315
|
+
}
|
|
3316
|
+
async retryFailedTransmissions(send) {
|
|
3317
|
+
const now = Date.now();
|
|
3318
|
+
const entries = await this.withLock(async () => {
|
|
3319
|
+
const store = this.readStore();
|
|
3320
|
+
this.evictExpiredInStore(store, now);
|
|
3321
|
+
this.lastSweepTsMs = now;
|
|
3322
|
+
this.writeStore(store);
|
|
3323
|
+
return Object.entries(store).filter(([, rec]) => rec.lastTransmitted === 0).map(([key, rec]) => ({ key, rec }));
|
|
3324
|
+
});
|
|
3325
|
+
for (const { key, rec } of entries) {
|
|
3326
|
+
try {
|
|
3327
|
+
const message = this.formatMessage(rec.message, rec.lastEventTs, rec.suppressedCount, rec.firstUnsentEventTs);
|
|
3328
|
+
await send({ level: rec.level, message: rec.message, dedupeKey: key, eventTime: rec.lastEventTs }, message);
|
|
3329
|
+
await this.withLock(async () => {
|
|
3330
|
+
const store = this.readStore();
|
|
3331
|
+
const current = store[key];
|
|
3332
|
+
if (current) {
|
|
3333
|
+
store[key] = {
|
|
3334
|
+
...current,
|
|
3335
|
+
lastTransmitted: Date.now(),
|
|
3336
|
+
suppressedCount: 0
|
|
3337
|
+
};
|
|
3338
|
+
this.writeStore(store);
|
|
3339
|
+
}
|
|
3340
|
+
});
|
|
3341
|
+
} catch (err) {
|
|
3342
|
+
logger.error("Failed to retry failed transmission", { key, rec, error: err });
|
|
3343
|
+
return;
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
// --- Internals ---
|
|
3348
|
+
ensureStoreFileExists() {
|
|
3349
|
+
try {
|
|
3350
|
+
if (!fs.existsSync(this.storeFilePath)) {
|
|
3351
|
+
fs.writeFileSync(this.storeFilePath, JSON.stringify({}), "utf-8");
|
|
3352
|
+
}
|
|
3353
|
+
} catch {
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3356
|
+
readStore() {
|
|
3357
|
+
try {
|
|
3358
|
+
if (!fs.existsSync(this.storeFilePath)) return {};
|
|
3359
|
+
const content = fs.readFileSync(this.storeFilePath, "utf-8");
|
|
3360
|
+
return content ? JSON.parse(content) : {};
|
|
3361
|
+
} catch {
|
|
3362
|
+
return {};
|
|
3363
|
+
}
|
|
3364
|
+
}
|
|
3365
|
+
writeStore(store) {
|
|
3366
|
+
try {
|
|
3367
|
+
fs.writeFileSync(this.storeFilePath, JSON.stringify(store, null, 2), "utf-8");
|
|
3368
|
+
} catch {
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
formatMessage(message, eventTs, suppressedCount, firstUnsentEventTs) {
|
|
3372
|
+
const timestamp = new Date(eventTs).toISOString();
|
|
3373
|
+
const base = `${timestamp} | ${message}`;
|
|
3374
|
+
if (suppressedCount > 0) {
|
|
3375
|
+
const since = firstUnsentEventTs && firstUnsentEventTs > 0 ? ` since ${new Date(firstUnsentEventTs).toISOString()}` : "";
|
|
3376
|
+
return `${base} (${suppressedCount} suppressed${since})`;
|
|
3377
|
+
}
|
|
3378
|
+
return base;
|
|
3379
|
+
}
|
|
3380
|
+
async withLock(fn) {
|
|
3381
|
+
const lockPath = `${this.storeFilePath}.lock`;
|
|
3382
|
+
const start = Date.now();
|
|
3383
|
+
while (true) {
|
|
3384
|
+
try {
|
|
3385
|
+
const fd = fs.openSync(lockPath, "wx");
|
|
3386
|
+
try {
|
|
3387
|
+
const result = await fn();
|
|
3388
|
+
return result;
|
|
3389
|
+
} finally {
|
|
3390
|
+
try {
|
|
3391
|
+
fs.closeSync(fd);
|
|
3392
|
+
} catch {
|
|
3393
|
+
}
|
|
3394
|
+
try {
|
|
3395
|
+
fs.unlinkSync(lockPath);
|
|
3396
|
+
} catch {
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
} catch {
|
|
3400
|
+
if (Date.now() - start > 3e3) {
|
|
3401
|
+
return await fn();
|
|
3402
|
+
}
|
|
3403
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
}
|
|
3407
|
+
/**
|
|
3408
|
+
* Evict expired entries from the store based on the TTL and the key's last transmitted time
|
|
3409
|
+
*/
|
|
3410
|
+
evictExpiredInStore(store, now) {
|
|
3411
|
+
const keys = Object.keys(store);
|
|
3412
|
+
if (keys.length === 0) return;
|
|
3413
|
+
for (const key of keys) {
|
|
3414
|
+
const rec = store[key];
|
|
3415
|
+
const referenceTs = rec.lastTransmitted > 0 ? rec.lastTransmitted : rec.lastEventTs;
|
|
3416
|
+
if (now - referenceTs > this.ttlMs) {
|
|
3417
|
+
delete store[key];
|
|
3418
|
+
}
|
|
3419
|
+
}
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3044
3422
|
class ApplicationInitializer {
|
|
3045
3423
|
/**
|
|
3046
3424
|
* Performs all necessary application initialization tasks
|
|
@@ -3592,9 +3970,12 @@ export {
|
|
|
3592
3970
|
ERPObjType,
|
|
3593
3971
|
ERPType,
|
|
3594
3972
|
ErrorHandler,
|
|
3973
|
+
FileLogDeduper,
|
|
3595
3974
|
GraphQLService,
|
|
3596
3975
|
HTTPClientFactory,
|
|
3597
3976
|
MMApiClient,
|
|
3977
|
+
MMBatchValidationError,
|
|
3978
|
+
MMConnectorLogger,
|
|
3598
3979
|
MMSendLaborTicket,
|
|
3599
3980
|
MMSendPart,
|
|
3600
3981
|
MMSendPartOperation,
|
|
@@ -3617,6 +3998,7 @@ export {
|
|
|
3617
3998
|
formatDateWithTZOffset,
|
|
3618
3999
|
getCachedTimezoneOffset,
|
|
3619
4000
|
g as getErpApiConnectionParams,
|
|
4001
|
+
getErrorType,
|
|
3620
4002
|
c as getInitialLoadComplete,
|
|
3621
4003
|
getPayloadWithoutIDField,
|
|
3622
4004
|
a as getSQLServerConfiguration,
|