@objectstack/core 15.1.1 → 16.0.0-rc.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/index.js CHANGED
@@ -244,8 +244,34 @@ var LEVEL_COLORS = {
244
244
  silent: ""
245
245
  };
246
246
  var RESET = "\x1B[0m";
247
+ function colorEnabled(stream) {
248
+ if (typeof process !== "undefined") {
249
+ const noColor = process.env?.NO_COLOR;
250
+ if (noColor !== void 0 && noColor !== "") return false;
251
+ }
252
+ return Boolean(stream?.isTTY);
253
+ }
254
+ function loadNodeBuiltin(id) {
255
+ if (typeof process === "undefined") return void 0;
256
+ const getBuiltinModule = process.getBuiltinModule;
257
+ if (typeof getBuiltinModule === "function") {
258
+ try {
259
+ return getBuiltinModule.call(process, `node:${id}`);
260
+ } catch {
261
+ return void 0;
262
+ }
263
+ }
264
+ try {
265
+ return __require(id);
266
+ } catch {
267
+ return void 0;
268
+ }
269
+ }
247
270
  var ObjectLogger = class _ObjectLogger {
248
271
  constructor(config = {}, bindings = {}) {
272
+ /** Only the logger that opened the stream may close it — children share it. */
273
+ this.ownsFileStream = false;
274
+ this.fileLoggingDisabled = false;
249
275
  this.config = {
250
276
  name: config.name,
251
277
  level: config.level ?? "info",
@@ -261,12 +287,41 @@ var ObjectLogger = class _ObjectLogger {
261
287
  }
262
288
  }
263
289
  openFileStream(path) {
290
+ const fs = loadNodeBuiltin("fs");
291
+ const nodePath2 = loadNodeBuiltin("path");
292
+ if (!fs || !nodePath2) {
293
+ this.disableFileLogging(path, "no filesystem access in this runtime");
294
+ return;
295
+ }
264
296
  try {
265
- const fs = __require("fs");
266
- const dir = __require("path").dirname(path);
267
- fs.mkdirSync(dir, { recursive: true });
268
- this.fileStream = fs.createWriteStream(path, { flags: "a" });
269
- } catch {
297
+ fs.mkdirSync(nodePath2.dirname(path), { recursive: true });
298
+ const stream = fs.createWriteStream(path, { flags: "a" });
299
+ stream.on("error", (err) => this.disableFileLogging(path, err.message));
300
+ this.fileStream = stream;
301
+ this.ownsFileStream = true;
302
+ } catch (err) {
303
+ this.disableFileLogging(path, err.message);
304
+ }
305
+ }
306
+ /**
307
+ * Report — once — that an explicitly configured `file` destination is not
308
+ * being written, and stop trying.
309
+ *
310
+ * Deliberately not routed through `write()`: this says the logger cannot
311
+ * honour its own config, so `level` must not filter it. The bare `catch {}`
312
+ * this replaces is exactly how #3110 stayed hidden.
313
+ */
314
+ disableFileLogging(path, reason) {
315
+ this.fileStream = void 0;
316
+ this.ownsFileStream = false;
317
+ if (this.fileLoggingDisabled) return;
318
+ this.fileLoggingDisabled = true;
319
+ const label = this.config.name ? `[${this.config.name}] ` : "";
320
+ const notice = `${label}logger: file logging disabled \u2014 cannot write to ${path}: ${reason}`;
321
+ if (typeof process !== "undefined" && process.stderr) {
322
+ process.stderr.write(notice + "\n");
323
+ } else if (typeof console !== "undefined") {
324
+ console.warn(notice);
270
325
  }
271
326
  }
272
327
  isEnabled(level) {
@@ -294,9 +349,13 @@ var ObjectLogger = class _ObjectLogger {
294
349
  });
295
350
  const hasContext = Object.keys(context).length > 0;
296
351
  const ts = (/* @__PURE__ */ new Date()).toISOString();
352
+ const isErrorLevel = level === "error" || level === "fatal";
353
+ const proc = typeof process !== "undefined" ? process : void 0;
354
+ const stream = proc ? isErrorLevel ? proc.stderr : proc.stdout : void 0;
297
355
  let line;
356
+ let plainLine;
298
357
  if (this.config.format === "json") {
299
- line = JSON.stringify({
358
+ line = plainLine = JSON.stringify({
300
359
  time: ts,
301
360
  level,
302
361
  ...this.config.name ? { name: this.config.name } : {},
@@ -306,26 +365,24 @@ var ObjectLogger = class _ObjectLogger {
306
365
  } else if (this.config.format === "text") {
307
366
  const parts = [ts, level.toUpperCase(), message];
308
367
  if (hasContext) parts.push(JSON.stringify(context));
309
- line = parts.join(" | ");
368
+ line = plainLine = parts.join(" | ");
310
369
  } else {
311
- const color = LEVEL_COLORS[level] || "";
312
370
  const label = this.config.name ? `[${this.config.name}] ` : "";
313
- line = `${color}${ts} ${level.toUpperCase()}${RESET} ${label}${message}`;
314
- if (hasContext) line += ` ${JSON.stringify(context)}`;
371
+ const head = `${ts} ${level.toUpperCase()}`;
372
+ let tail = ` ${label}${message}`;
373
+ if (hasContext) tail += ` ${JSON.stringify(context)}`;
374
+ plainLine = head + tail;
375
+ const color = LEVEL_COLORS[level] || "";
376
+ line = color && colorEnabled(stream) ? `${color}${head}${RESET}${tail}` : plainLine;
315
377
  }
316
- const out = line + "\n";
317
- if (typeof process !== "undefined" && process.stderr) {
318
- if (level === "error" || level === "fatal") {
319
- process.stderr.write(out);
320
- } else {
321
- process.stdout?.write(out);
322
- }
378
+ if (stream) {
379
+ stream.write(line + "\n");
323
380
  } else if (typeof console !== "undefined") {
324
381
  const fn = level === "error" || level === "fatal" ? console.error : level === "warn" ? console.warn : level === "debug" ? console.debug : console.log;
325
382
  fn(line);
326
383
  }
327
384
  if (this.fileStream) {
328
- this.fileStream.write(out);
385
+ this.fileStream.write(plainLine + "\n");
329
386
  }
330
387
  }
331
388
  debug(message, meta) {
@@ -355,7 +412,8 @@ var ObjectLogger = class _ObjectLogger {
355
412
  this.info(message, args.length > 0 ? { args } : void 0);
356
413
  }
357
414
  child(context) {
358
- const child = new _ObjectLogger(this.config, { ...this.bindings, ...context });
415
+ const child = new _ObjectLogger({ ...this.config, file: void 0 }, { ...this.bindings, ...context });
416
+ child.config.file = this.config.file;
359
417
  child.fileStream = this.fileStream;
360
418
  return child;
361
419
  }
@@ -363,10 +421,11 @@ var ObjectLogger = class _ObjectLogger {
363
421
  return this.child({ traceId, spanId });
364
422
  }
365
423
  async destroy() {
366
- if (this.fileStream) {
367
- await new Promise((resolve) => this.fileStream.end(resolve));
368
- this.fileStream = void 0;
369
- }
424
+ const stream = this.fileStream;
425
+ this.fileStream = void 0;
426
+ if (!stream || !this.ownsFileStream) return;
427
+ this.ownsFileStream = false;
428
+ await new Promise((resolve) => stream.end(resolve));
370
429
  }
371
430
  };
372
431
  function createLogger(config) {
@@ -1620,6 +1679,19 @@ var ObjectKernel = class {
1620
1679
  getPluginMetrics() {
1621
1680
  return new Map(this.pluginStartTimes);
1622
1681
  }
1682
+ /**
1683
+ * Whether a plugin with the given name has been registered on this kernel.
1684
+ *
1685
+ * Registration happens synchronously in `use()` before any plugin's
1686
+ * `start()` runs, so a plugin may use this during its own start() to make
1687
+ * composition-dependent decisions deterministically — e.g. the dispatcher
1688
+ * bridge cedes `${prefix}/discovery` to `com.objectstack.rest.api` when
1689
+ * both are mounted (ADR-0076 D11: single owner per route, not
1690
+ * first-registration-wins).
1691
+ */
1692
+ hasPlugin(name) {
1693
+ return this.plugins.has(name);
1694
+ }
1623
1695
  /**
1624
1696
  * Get a service (sync helper)
1625
1697
  */
@@ -4503,6 +4575,105 @@ function calendarPartsInTzOrUtc(d, tz) {
4503
4575
  day: d.getUTCDate()
4504
4576
  };
4505
4577
  }
4578
+ function zonedDateStartToUtcMs(ymd, tz) {
4579
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(ymd);
4580
+ const wallAsUtc = m ? Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3])) : NaN;
4581
+ if (!tz || tz === "UTC" || Number.isNaN(wallAsUtc)) return wallAsUtc;
4582
+ try {
4583
+ const offsetAt = (t) => {
4584
+ const p = new Intl.DateTimeFormat("en-US", {
4585
+ timeZone: tz,
4586
+ hourCycle: "h23",
4587
+ year: "numeric",
4588
+ month: "2-digit",
4589
+ day: "2-digit",
4590
+ hour: "2-digit",
4591
+ minute: "2-digit",
4592
+ second: "2-digit"
4593
+ }).formatToParts(new Date(t));
4594
+ const g = (k) => Number(p.find((x) => x.type === k)?.value);
4595
+ return Date.UTC(g("year"), g("month") - 1, g("day"), g("hour"), g("minute"), g("second")) - t;
4596
+ };
4597
+ const off1 = offsetAt(wallAsUtc - offsetAt(wallAsUtc));
4598
+ return wallAsUtc - off1;
4599
+ } catch {
4600
+ return wallAsUtc;
4601
+ }
4602
+ }
4603
+ function isoWeekLabelUtc(d) {
4604
+ const target = new Date(d.getTime());
4605
+ const dayNum = (target.getUTCDay() + 6) % 7;
4606
+ target.setUTCDate(target.getUTCDate() - dayNum + 3);
4607
+ const firstThursday = new Date(Date.UTC(target.getUTCFullYear(), 0, 4));
4608
+ const weekNo = 1 + Math.round(
4609
+ ((target.getTime() - firstThursday.getTime()) / 864e5 - 3 + (firstThursday.getUTCDay() + 6) % 7) / 7
4610
+ );
4611
+ return `${target.getUTCFullYear()}-W${String(weekNo).padStart(2, "0")}`;
4612
+ }
4613
+ function bucketKeyToCalendarRange(key, granularity) {
4614
+ if (typeof key !== "string" || key.length === 0) return null;
4615
+ const fmt = (dt) => `${String(dt.getUTCFullYear()).padStart(4, "0")}-${String(dt.getUTCMonth() + 1).padStart(
4616
+ 2,
4617
+ "0"
4618
+ )}-${String(dt.getUTCDate()).padStart(2, "0")}`;
4619
+ switch (granularity) {
4620
+ case "year": {
4621
+ const m = /^(\d{4})$/.exec(key);
4622
+ if (!m) return null;
4623
+ const y = Number(m[1]);
4624
+ return { start: fmt(new Date(Date.UTC(y, 0, 1))), end: fmt(new Date(Date.UTC(y + 1, 0, 1))) };
4625
+ }
4626
+ case "quarter": {
4627
+ const m = /^(\d{4})-Q([1-4])$/.exec(key);
4628
+ if (!m) return null;
4629
+ const y = Number(m[1]);
4630
+ const startMonth = (Number(m[2]) - 1) * 3;
4631
+ return {
4632
+ start: fmt(new Date(Date.UTC(y, startMonth, 1))),
4633
+ end: fmt(new Date(Date.UTC(y, startMonth + 3, 1)))
4634
+ // Date.UTC rolls Q4 into next year
4635
+ };
4636
+ }
4637
+ case "month": {
4638
+ const m = /^(\d{4})-(\d{2})$/.exec(key);
4639
+ if (!m) return null;
4640
+ const mo = Number(m[2]);
4641
+ if (mo < 1 || mo > 12) return null;
4642
+ const y = Number(m[1]);
4643
+ return {
4644
+ start: fmt(new Date(Date.UTC(y, mo - 1, 1))),
4645
+ end: fmt(new Date(Date.UTC(y, mo, 1)))
4646
+ };
4647
+ }
4648
+ case "day": {
4649
+ const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(key);
4650
+ if (!m) return null;
4651
+ const y = Number(m[1]);
4652
+ const mo = Number(m[2]);
4653
+ const d = Number(m[3]);
4654
+ const start = new Date(Date.UTC(y, mo - 1, d));
4655
+ if (fmt(start) !== key) return null;
4656
+ return { start: key, end: fmt(new Date(Date.UTC(y, mo - 1, d + 1))) };
4657
+ }
4658
+ case "week": {
4659
+ const m = /^(\d{4})-W(\d{2})$/.exec(key);
4660
+ if (!m) return null;
4661
+ const isoYear = Number(m[1]);
4662
+ const week = Number(m[2]);
4663
+ if (week < 1 || week > 53) return null;
4664
+ const jan4 = new Date(Date.UTC(isoYear, 0, 4));
4665
+ const jan4Dow = (jan4.getUTCDay() + 6) % 7;
4666
+ const start = new Date(jan4.getTime());
4667
+ start.setUTCDate(jan4.getUTCDate() - jan4Dow + (week - 1) * 7);
4668
+ if (isoWeekLabelUtc(start) !== key) return null;
4669
+ const end = new Date(start.getTime());
4670
+ end.setUTCDate(start.getUTCDate() + 7);
4671
+ return { start: fmt(start), end: fmt(end) };
4672
+ }
4673
+ default:
4674
+ return null;
4675
+ }
4676
+ }
4506
4677
 
4507
4678
  // src/utils/bulk-write.ts
4508
4679
  var DEFAULT_BATCH_SIZE = 200;
@@ -4520,11 +4691,23 @@ var TRANSIENT_PATTERNS = [
4520
4691
  /too many connections/i
4521
4692
  ];
4522
4693
  var TRANSIENT_CODES = /^(ECONNRESET|ECONNREFUSED|ECONNABORTED|EPIPE|EAI_AGAIN|ETIMEDOUT|EHOSTUNREACH|ENETUNREACH|ENOTFOUND)$/i;
4694
+ var NON_TRANSIENT_PATTERNS = [
4695
+ /validation/i,
4696
+ /constraint/i,
4697
+ /\brequired\b/i,
4698
+ /\bunique\b/i,
4699
+ /duplicate/i,
4700
+ /not[\s_-]*null/i,
4701
+ /invalid/i,
4702
+ /not allowed/i,
4703
+ /out of range/i
4704
+ ];
4523
4705
  function defaultIsTransientError(err) {
4524
- const code = err?.code;
4525
- if (typeof code === "string" && TRANSIENT_CODES.test(code)) return true;
4526
4706
  const message = err?.message;
4527
4707
  const text = typeof message === "string" ? message : String(err ?? "");
4708
+ if (NON_TRANSIENT_PATTERNS.some((re) => re.test(text))) return false;
4709
+ const code = err?.code;
4710
+ if (typeof code === "string" && TRANSIENT_CODES.test(code)) return true;
4528
4711
  return TRANSIENT_PATTERNS.some((re) => re.test(text));
4529
4712
  }
4530
4713
  var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -4532,7 +4715,7 @@ async function withRetry(fn, opts) {
4532
4715
  let lastError;
4533
4716
  for (let attempt = 1; attempt <= opts.maxRetries; attempt++) {
4534
4717
  try {
4535
- return await fn();
4718
+ return await fn(attempt);
4536
4719
  } catch (err) {
4537
4720
  lastError = err;
4538
4721
  if (attempt >= opts.maxRetries || !opts.isTransientError(err)) throw err;
@@ -4562,7 +4745,31 @@ async function bulkWrite(rows, opts) {
4562
4745
  for (let start = 0; start < rows.length; start += batchSize) {
4563
4746
  const batch = rows.slice(start, start + batchSize);
4564
4747
  try {
4565
- const records = await withRetry(() => opts.writeBatch(batch), retryOpts);
4748
+ if (opts.writeBatchPartial) {
4749
+ const outcomes = await withRetry((attempt) => opts.writeBatchPartial(batch, { attempt }), retryOpts);
4750
+ if (!Array.isArray(outcomes) || outcomes.length !== batch.length) {
4751
+ throw Object.assign(
4752
+ new Error(
4753
+ `bulkWrite: writeBatchPartial returned ${Array.isArray(outcomes) ? `${outcomes.length} outcome(s)` : String(typeof outcomes)} for a ${batch.length}-row batch \u2014 treating batch as failed`
4754
+ ),
4755
+ { code: "ERR_BULK_RESULT_MISMATCH" }
4756
+ );
4757
+ }
4758
+ for (let i = 0; i < batch.length; i++) {
4759
+ const o = outcomes[i];
4760
+ results[start + i] = o.ok ? { index: start + i, ok: true, record: o.record } : { index: start + i, ok: false, error: o.error };
4761
+ }
4762
+ continue;
4763
+ }
4764
+ const records = await withRetry((attempt) => opts.writeBatch(batch, { attempt }), retryOpts);
4765
+ if (!Array.isArray(records) || records.length !== batch.length) {
4766
+ throw Object.assign(
4767
+ new Error(
4768
+ `bulkWrite: writeBatch returned ${Array.isArray(records) ? `${records.length} record(s)` : String(typeof records)} for a ${batch.length}-row batch \u2014 treating batch as failed`
4769
+ ),
4770
+ { code: "ERR_BULK_RESULT_MISMATCH" }
4771
+ );
4772
+ }
4566
4773
  for (let i = 0; i < batch.length; i++) {
4567
4774
  results[start + i] = { index: start + i, ok: true, record: records[i] };
4568
4775
  }
@@ -4574,7 +4781,7 @@ async function bulkWrite(rows, opts) {
4574
4781
  for (let i = 0; i < batch.length; i++) {
4575
4782
  const idx = start + i;
4576
4783
  try {
4577
- const record = await withRetry(() => opts.writeOne(batch[i]), retryOpts);
4784
+ const record = await withRetry((attempt) => opts.writeOne(batch[i], { attempt }), retryOpts);
4578
4785
  results[idx] = { index: idx, ok: true, record };
4579
4786
  } catch (err) {
4580
4787
  results[idx] = { index: idx, ok: false, error: err };
@@ -5529,6 +5736,7 @@ export {
5529
5736
  SecurePluginContext,
5530
5737
  SemanticVersionManager,
5531
5738
  ServiceLifecycle,
5739
+ bucketKeyToCalendarRange,
5532
5740
  buildPermissionsFromGrants,
5533
5741
  bulkWrite,
5534
5742
  calendarPartsInTz,
@@ -5574,6 +5782,7 @@ export {
5574
5782
  verifyPluginArtifact,
5575
5783
  verifyPublisherSignature,
5576
5784
  wireAuthoredTranslationSync,
5577
- withTransientRetry
5785
+ withTransientRetry,
5786
+ zonedDateStartToUtcMs
5578
5787
  };
5579
5788
  //# sourceMappingURL=index.js.map