@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.
Files changed (81) hide show
  1. package/README.md +1 -1
  2. package/dist/{config-2l5vnNkA.js → config-qat9zgOl.js} +6 -6
  3. package/dist/{config-2l5vnNkA.js.map → config-qat9zgOl.js.map} +1 -1
  4. package/dist/{connector-factory-CQ8e7Tae.js → connector-factory-C2czCs9v.js} +12 -3
  5. package/dist/connector-factory-C2czCs9v.js.map +1 -0
  6. package/dist/{hashed-cache-manager-Ci59eC75.js → hashed-cache-manager-CzyFSt2B.js} +5 -4
  7. package/dist/{hashed-cache-manager-Ci59eC75.js.map → hashed-cache-manager-CzyFSt2B.js.map} +1 -1
  8. package/dist/{index-CXbOvFyf.js → index-B9wo8pld.js} +7 -7
  9. package/dist/{index-CXbOvFyf.js.map → index-B9wo8pld.js.map} +1 -1
  10. package/dist/index.d.ts +14 -6
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/{logger-QG73MndU.js → logger-Db8CkwR6.js} +929 -971
  13. package/dist/logger-Db8CkwR6.js.map +1 -0
  14. package/dist/mm-erp-sdk.js +417 -10
  15. package/dist/mm-erp-sdk.js.map +1 -1
  16. package/dist/services/data-sync-service/index.d.ts +1 -1
  17. package/dist/services/data-sync-service/index.d.ts.map +1 -1
  18. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.d.ts +2 -0
  19. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.d.ts.map +1 -1
  20. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js +42 -41
  21. package/dist/services/data-sync-service/jobs/clean-up-expired-cache.js.map +1 -1
  22. package/dist/services/data-sync-service/jobs/from-erp.d.ts.map +1 -1
  23. package/dist/services/data-sync-service/jobs/from-erp.js +11 -5
  24. package/dist/services/data-sync-service/jobs/from-erp.js.map +1 -1
  25. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.d.ts +2 -0
  26. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.d.ts.map +1 -1
  27. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js +39 -40
  28. package/dist/services/data-sync-service/jobs/retry-failed-labor-tickets.js.map +1 -1
  29. package/dist/services/data-sync-service/jobs/run-migrations.d.ts.map +1 -1
  30. package/dist/services/data-sync-service/jobs/run-migrations.js +4 -3
  31. package/dist/services/data-sync-service/jobs/run-migrations.js.map +1 -1
  32. package/dist/services/data-sync-service/jobs/to-erp.d.ts.map +1 -1
  33. package/dist/services/data-sync-service/jobs/to-erp.js +15 -5
  34. package/dist/services/data-sync-service/jobs/to-erp.js.map +1 -1
  35. package/dist/services/erp-api-services/index.d.ts +5 -1
  36. package/dist/services/erp-api-services/index.d.ts.map +1 -1
  37. package/dist/services/mm-api-service/index.d.ts +3 -2
  38. package/dist/services/mm-api-service/index.d.ts.map +1 -1
  39. package/dist/services/mm-api-service/mm-api-service.d.ts +20 -0
  40. package/dist/services/mm-api-service/mm-api-service.d.ts.map +1 -1
  41. package/dist/types/erp-types.d.ts +1 -2
  42. package/dist/types/erp-types.d.ts.map +1 -1
  43. package/dist/utils/connector-factory.d.ts.map +1 -1
  44. package/dist/utils/connector-log/log-deduper.d.ts +56 -0
  45. package/dist/utils/connector-log/log-deduper.d.ts.map +1 -0
  46. package/dist/utils/connector-log/mm-connector-logger-example.d.ts +1 -0
  47. package/dist/utils/connector-log/mm-connector-logger-example.d.ts.map +1 -0
  48. package/dist/utils/connector-log/mm-connector-logger.d.ts +74 -0
  49. package/dist/utils/connector-log/mm-connector-logger.d.ts.map +1 -0
  50. package/dist/utils/error-utils.d.ts +2 -0
  51. package/dist/utils/error-utils.d.ts.map +1 -0
  52. package/dist/utils/index.d.ts +11 -2
  53. package/dist/utils/index.d.ts.map +1 -1
  54. package/dist/utils/standard-process-drivers/index.d.ts +2 -1
  55. package/dist/utils/standard-process-drivers/index.d.ts.map +1 -1
  56. package/dist/utils/timezone.d.ts +7 -0
  57. package/dist/utils/timezone.d.ts.map +1 -1
  58. package/package.json +1 -1
  59. package/src/index.ts +19 -5
  60. package/src/services/data-sync-service/index.ts +1 -4
  61. package/src/services/data-sync-service/jobs/clean-up-expired-cache.ts +19 -7
  62. package/src/services/data-sync-service/jobs/from-erp.ts +12 -5
  63. package/src/services/data-sync-service/jobs/retry-failed-labor-tickets.ts +15 -5
  64. package/src/services/data-sync-service/jobs/run-migrations.ts +5 -2
  65. package/src/services/data-sync-service/jobs/to-erp.ts +17 -5
  66. package/src/services/erp-api-services/index.ts +9 -1
  67. package/src/services/mm-api-service/index.ts +1 -1
  68. package/src/services/mm-api-service/mm-api-service.ts +28 -0
  69. package/src/types/erp-types.ts +0 -1
  70. package/src/utils/application-initializer.ts +1 -1
  71. package/src/utils/connector-factory.ts +14 -3
  72. package/src/utils/connector-log/log-deduper.ts +284 -0
  73. package/src/utils/connector-log/mm-connector-logger-example.ts +97 -0
  74. package/src/utils/connector-log/mm-connector-logger.ts +177 -0
  75. package/src/utils/error-utils.ts +18 -0
  76. package/src/utils/index.ts +12 -5
  77. package/src/utils/mm-labor-ticket-helpers.ts +2 -2
  78. package/src/utils/standard-process-drivers/index.ts +2 -4
  79. package/src/utils/timezone.ts +28 -0
  80. package/dist/connector-factory-CQ8e7Tae.js.map +0 -1
  81. package/dist/logger-QG73MndU.js.map +0 -1
@@ -1,15 +1,16 @@
1
- import { C as CoreConfiguration, H as HashedCacheManager } from "./hashed-cache-manager-Ci59eC75.js";
2
- import { g, a } from "./hashed-cache-manager-Ci59eC75.js";
3
- import { l as logger } from "./logger-QG73MndU.js";
4
- import { g as getCachedMMToken, s as setCachedMMToken, a as setTimezoneOffsetInCache, b as getCachedTimezoneOffset, S as SQLiteCoordinator } from "./index-CXbOvFyf.js";
5
- import { c, d } from "./index-CXbOvFyf.js";
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 "./connector-factory-CQ8e7Tae.js";
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?.toISOString() || null;
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.exit(1);
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,