tina4-nodejs 3.13.131 → 3.13.132

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.
@@ -262,6 +262,99 @@ var init_databaseResult = __esm({
262
262
  }
263
263
  });
264
264
 
265
+ // src/modelCollection.ts
266
+ var ModelCollection;
267
+ var init_modelCollection = __esm({
268
+ "src/modelCollection.ts"() {
269
+ "use strict";
270
+ ModelCollection = class extends Array {
271
+ /** Total rows matching the query's filter (ignores limit/offset). */
272
+ _total = 0;
273
+ /** The SQL limit that produced this page. */
274
+ _limit = 0;
275
+ /** The SQL offset that produced this page. */
276
+ _offset = 0;
277
+ /**
278
+ * Derived array operations (`map`, `filter`, `slice`, spread, …) build a plain
279
+ * `Array`, never another `ModelCollection`. This is what defuses the Array-
280
+ * subclass constructor/species trap: the engine never calls this constructor
281
+ * with a length during those operations.
282
+ */
283
+ static get [Symbol.species]() {
284
+ return Array;
285
+ }
286
+ /**
287
+ * @param items the page of hydrated model instances (or the length, when the
288
+ * engine constructs a derived array — defended against here).
289
+ * @param total total rows matching the query's filter (ignores limit/offset).
290
+ * @param limit the SQL limit that produced this page.
291
+ * @param offset the SQL offset that produced this page.
292
+ */
293
+ constructor(items, total = 0, limit = 0, offset = 0) {
294
+ super();
295
+ if (typeof items === "number") {
296
+ this.length = items;
297
+ return;
298
+ }
299
+ if (items) {
300
+ for (let i = 0; i < items.length; i++) {
301
+ this[i] = items[i];
302
+ }
303
+ }
304
+ this._total = Math.trunc(total) || 0;
305
+ this._limit = Math.trunc(limit) || 0;
306
+ this._offset = Math.trunc(offset) || 0;
307
+ }
308
+ /**
309
+ * Total rows matching the query's filter, ignoring limit/offset.
310
+ *
311
+ * This is the whole point of the collection: the page slice you are iterating
312
+ * is capped by `limit`, but this number is the full count of matching rows —
313
+ * what a pager needs to render "page 3 of 13".
314
+ */
315
+ getTotalRecords() {
316
+ return this._total;
317
+ }
318
+ /**
319
+ * The canonical pagination envelope — seven snake_case keys, identical to
320
+ * `DatabaseResult.toPaginate()` (ADR-0043) and to the other three frameworks'
321
+ * `toPaginate()` / `to_paginate()`.
322
+ *
323
+ * records the page's rows as plain objects (never re-sliced)
324
+ * total getTotalRecords() — the true total for the filter
325
+ * page floor(offset / per_page) + 1
326
+ * per_page the query's limit
327
+ * total_pages ceil(total / per_page)
328
+ * limit the SQL limit actually applied
329
+ * offset the SQL offset actually applied
330
+ *
331
+ * `records` are model dicts (via `toDict()`, the same serialisation the
332
+ * framework applies to a model in a JSON response), so the JSON a client sees
333
+ * matches `DatabaseResult` exactly — the result is uniform whether the route
334
+ * returned a raw `db.fetch()` or an ORM query.
335
+ */
336
+ toPaginate() {
337
+ const perPage = this._limit > 0 ? this._limit : this.length;
338
+ const page = perPage > 0 ? Math.floor(this._offset / perPage) + 1 : 1;
339
+ const totalPages = perPage > 0 ? Math.max(1, Math.ceil(this._total / perPage)) : 1;
340
+ const records = this.map((model) => {
341
+ const m = model;
342
+ return m && typeof m.toDict === "function" ? m.toDict() : model;
343
+ });
344
+ return {
345
+ records,
346
+ total: this._total,
347
+ page,
348
+ per_page: perPage,
349
+ total_pages: totalPages,
350
+ limit: perPage,
351
+ offset: this._offset
352
+ };
353
+ }
354
+ };
355
+ }
356
+ });
357
+
265
358
  // src/databaseUrl.ts
266
359
  import { inspect } from "node:util";
267
360
  function stripOneSlash(path8) {
@@ -5414,33 +5507,17 @@ var init_engine = __esm({
5414
5507
  i++;
5415
5508
  }
5416
5509
  }
5417
- handleCache(tokens, start2, context) {
5418
- const [content] = stripTag(tokens[start2][1]);
5419
- const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
5420
- const cacheKey = m ? m[1] : "default";
5421
- const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
5422
- sweepExpiredCache(this.fragmentCache);
5423
- const cached = this.fragmentCache.get(cacheKey);
5424
- if (cached) {
5425
- const [htmlContent, expiresAt] = cached;
5426
- if (Date.now() < expiresAt) {
5427
- let i2 = start2 + 1;
5428
- let depth2 = 0;
5429
- while (i2 < tokens.length) {
5430
- if (tokens[i2][0] === "BLOCK") {
5431
- const [tagContent] = stripTag(tokens[i2][1]);
5432
- const tag = tagContent.split(/\s+/)[0] || "";
5433
- if (tag === "cache") depth2++;
5434
- else if (tag === "endcache") {
5435
- if (depth2 === 0) return [htmlContent, i2 + 1];
5436
- depth2--;
5437
- }
5438
- }
5439
- i2++;
5440
- }
5441
- return [htmlContent, i2];
5442
- }
5443
- }
5510
+ /**
5511
+ * Collect the body tokens of a {% <openTag> %}...{% end<openTag> %} block,
5512
+ * starting from the token after the opening tag (start + 1). Nested same-tag
5513
+ * blocks are kept in the body and balanced by depth; the matching closing tag is
5514
+ * consumed but NOT included. Returns [bodyTokens, indexAfterClosingTag].
5515
+ *
5516
+ * canNest guards the open-tag count: handleSetBlock passes it so the inline
5517
+ * {% set x = 1 %} form (which has no {% endset %}) never opens a nested block —
5518
+ * only the block form {% set x %} nests. Omitted, every openTag occurrence nests.
5519
+ */
5520
+ collectBlockBody(tokens, start2, openTag, closeTag, canNest) {
5444
5521
  const bodyTokens = [];
5445
5522
  let i = start2 + 1;
5446
5523
  let depth = 0;
@@ -5448,10 +5525,10 @@ var init_engine = __esm({
5448
5525
  if (tokens[i][0] === "BLOCK") {
5449
5526
  const [tagContent] = stripTag(tokens[i][1]);
5450
5527
  const tag = tagContent.split(/\s+/)[0] || "";
5451
- if (tag === "cache") {
5528
+ if (tag === openTag && (canNest ? canNest(tagContent) : true)) {
5452
5529
  depth++;
5453
5530
  bodyTokens.push(tokens[i]);
5454
- } else if (tag === "endcache") {
5531
+ } else if (tag === closeTag) {
5455
5532
  if (depth === 0) {
5456
5533
  i++;
5457
5534
  break;
@@ -5466,6 +5543,36 @@ var init_engine = __esm({
5466
5543
  }
5467
5544
  i++;
5468
5545
  }
5546
+ return [bodyTokens, i];
5547
+ }
5548
+ handleCache(tokens, start2, context) {
5549
+ const [content] = stripTag(tokens[start2][1]);
5550
+ const m = content.match(/^cache\s+["'](.+?)["']\s*(\d+)?/);
5551
+ const cacheKey = m ? m[1] : "default";
5552
+ const ttl = m && m[2] ? parseInt(m[2], 10) : 60;
5553
+ sweepExpiredCache(this.fragmentCache);
5554
+ const cached = this.fragmentCache.get(cacheKey);
5555
+ if (cached) {
5556
+ const [htmlContent, expiresAt] = cached;
5557
+ if (Date.now() < expiresAt) {
5558
+ let i2 = start2 + 1;
5559
+ let depth = 0;
5560
+ while (i2 < tokens.length) {
5561
+ if (tokens[i2][0] === "BLOCK") {
5562
+ const [tagContent] = stripTag(tokens[i2][1]);
5563
+ const tag = tagContent.split(/\s+/)[0] || "";
5564
+ if (tag === "cache") depth++;
5565
+ else if (tag === "endcache") {
5566
+ if (depth === 0) return [htmlContent, i2 + 1];
5567
+ depth--;
5568
+ }
5569
+ }
5570
+ i2++;
5571
+ }
5572
+ return [htmlContent, i2];
5573
+ }
5574
+ }
5575
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "cache", "endcache");
5469
5576
  const rendered = this.renderTokens([...bodyTokens], context);
5470
5577
  capCache(this.fragmentCache, TEMPLATE_CACHE_MAX);
5471
5578
  this.fragmentCache.set(cacheKey, [rendered, Date.now() + ttl * 1e3]);
@@ -5630,62 +5737,20 @@ var init_engine = __esm({
5630
5737
  handleSetBlock(tokens, start2, context) {
5631
5738
  const [content] = stripTag(tokens[start2][1]);
5632
5739
  const name = (content.split(/\s+/)[1] || "").trim();
5633
- const bodyTokens = [];
5634
- let i = start2 + 1;
5635
- let depth = 0;
5636
- while (i < tokens.length) {
5637
- if (tokens[i][0] === "BLOCK") {
5638
- const [tagContent] = stripTag(tokens[i][1]);
5639
- const tag = tagContent.split(/\s+/)[0] || "";
5640
- if (tag === "set" && !tagContent.includes("=")) {
5641
- depth++;
5642
- bodyTokens.push(tokens[i]);
5643
- } else if (tag === "endset") {
5644
- if (depth === 0) {
5645
- i++;
5646
- break;
5647
- }
5648
- depth--;
5649
- bodyTokens.push(tokens[i]);
5650
- } else {
5651
- bodyTokens.push(tokens[i]);
5652
- }
5653
- } else {
5654
- bodyTokens.push(tokens[i]);
5655
- }
5656
- i++;
5657
- }
5740
+ const [bodyTokens, i] = this.collectBlockBody(
5741
+ tokens,
5742
+ start2,
5743
+ "set",
5744
+ "endset",
5745
+ (tagContent) => !tagContent.includes("=")
5746
+ );
5658
5747
  if (name) {
5659
5748
  context[name] = new SafeString(this.renderTokens([...bodyTokens], context));
5660
5749
  }
5661
5750
  return i;
5662
5751
  }
5663
5752
  handleSpaceless(tokens, start2, context) {
5664
- const bodyTokens = [];
5665
- let i = start2 + 1;
5666
- let depth = 0;
5667
- while (i < tokens.length) {
5668
- if (tokens[i][0] === "BLOCK") {
5669
- const [tagContent] = stripTag(tokens[i][1]);
5670
- const tag = tagContent.split(/\s+/)[0] || "";
5671
- if (tag === "spaceless") {
5672
- depth++;
5673
- bodyTokens.push(tokens[i]);
5674
- } else if (tag === "endspaceless") {
5675
- if (depth === 0) {
5676
- i++;
5677
- break;
5678
- }
5679
- depth--;
5680
- bodyTokens.push(tokens[i]);
5681
- } else {
5682
- bodyTokens.push(tokens[i]);
5683
- }
5684
- } else {
5685
- bodyTokens.push(tokens[i]);
5686
- }
5687
- i++;
5688
- }
5753
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "spaceless", "endspaceless");
5689
5754
  let rendered = this.renderTokens([...bodyTokens], context);
5690
5755
  rendered = rendered.replace(/>\s+</g, "><");
5691
5756
  return [rendered, i];
@@ -5694,31 +5759,7 @@ var init_engine = __esm({
5694
5759
  const [content] = stripTag(tokens[start2][1]);
5695
5760
  const modeMatch = content.match(/^autoescape\s+(false|true)/);
5696
5761
  const autoEscapeOn = !(modeMatch && modeMatch[1] === "false");
5697
- const bodyTokens = [];
5698
- let i = start2 + 1;
5699
- let depth = 0;
5700
- while (i < tokens.length) {
5701
- if (tokens[i][0] === "BLOCK") {
5702
- const [tagContent] = stripTag(tokens[i][1]);
5703
- const tag = tagContent.split(/\s+/)[0] || "";
5704
- if (tag === "autoescape") {
5705
- depth++;
5706
- bodyTokens.push(tokens[i]);
5707
- } else if (tag === "endautoescape") {
5708
- if (depth === 0) {
5709
- i++;
5710
- break;
5711
- }
5712
- depth--;
5713
- bodyTokens.push(tokens[i]);
5714
- } else {
5715
- bodyTokens.push(tokens[i]);
5716
- }
5717
- } else {
5718
- bodyTokens.push(tokens[i]);
5719
- }
5720
- i++;
5721
- }
5762
+ const [bodyTokens, i] = this.collectBlockBody(tokens, start2, "autoescape", "endautoescape");
5722
5763
  if (!autoEscapeOn) {
5723
5764
  const oldAutoEscape = this._autoEscape;
5724
5765
  this._autoEscape = false;
@@ -23511,7 +23552,7 @@ function toolbarJs() {
23511
23552
  connect();
23512
23553
  })();`;
23513
23554
  }
23514
- var cpuCount, DEV_SAFE_METHODS, DEV_MCP_PREFIXES, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, handleChat, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, handleExecute, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleToolbarCss, handleToolbarJs;
23555
+ var cpuCount, DEV_SAFE_METHODS, DEV_MCP_PREFIXES, DEV_SECRET_BASENAMES, DEV_SECRET_SUFFIXES, MessageLog, RequestInspector, ErrorTracker, DevMailboxStore, DevQueue, WsTracker, DevAdmin, handleDashboard, _reloadMtime, _reloadFile, handleMtime, handleReload, handleMessages, handleMessagesClear, handleRequests, handleRequestsClear, handleSystem, handleMessagesSearch, handleQueue, handleQueueTopics, handleQueueDeadLetters, handleQueueRetry, handleQueuePurge, handleQueueReplay, handleMailbox, handleMailboxRead, handleMailboxSeed, handleMailboxClear, handleTable, handleTables, handleSeed, handleQuery, handleBroken, handleBrokenResolve, handleBrokenClear, handleWebsockets, handleWebsocketsDisconnect, handleTool, DEFAULT_MCP_URL, handleGroundingStatus, handleGroundingToken, handleMigrate, handleSeedRun, handleTest, handleThreads, handleThreadsSub, handleConnections, handleConnectionsTest, handleConnectionsSave, __devAdminFilename, __devAdminDirname, handleGalleryList, handleVersionCheck, handleThoughts, handleSuperviseStub, DEV_FILES_IGNORED, handleFiles, DEV_ADMIN_LANG_MAP, handleFileRead, handleFileSave, handleFileRaw, handleFileRename, handleFileDelete, handleDepsSearch, handleDepsInstall, handleGitStatus, handleMcpTools, handleMcpCall, handleMcpStreamable, handleMcpDelete, handleMcpGet405, handleMcpLegacyMessage, handleMcpSse, handleScaffoldList, handleScaffoldRun, handlePlanCurrent, handlePlanList, handlePlanCreate, handlePlanSwitch, handlePlanCompleteStep, handlePlanAddStep, handlePlanNote, handlePlanArchive, handlePlanRead, handlePlanFlesh, handleIndexRebuild, handleIndexSearch, handleIndexFile, handleIndexOverview, handleDocsSearch, handleDocsClass, handleDocsMethod, handleDocsIndex, handleDocsWellKnown, handleDevAdminJs, handleToolbarCss, handleToolbarJs;
23515
23556
  var init_devAdmin = __esm({
23516
23557
  "../core/src/devAdmin.ts"() {
23517
23558
  "use strict";
@@ -23849,9 +23890,6 @@ var init_devAdmin = __esm({
23849
23890
  { method: "POST", pattern: "/__dev/api/websockets/disconnect", handler: handleWebsocketsDisconnect },
23850
23891
  // Tools
23851
23892
  { method: "POST", pattern: "/__dev/api/tool", handler: handleTool },
23852
- // Chat — proxies to Rust agent /chat (SSE passthrough). Forwards
23853
- // active_file and any other body keys verbatim. See proxyToSupervisor.
23854
- { method: "POST", pattern: "/__dev/api/chat", handler: handleChat },
23855
23893
  // Threads — proxies to Rust agent /threads. Mirrors Python's
23856
23894
  // _api_threads + _api_threads_sub.
23857
23895
  { method: "GET", pattern: "/__dev/api/threads", handler: handleThreads },
@@ -23930,8 +23968,6 @@ var init_devAdmin = __esm({
23930
23968
  { method: "GET", pattern: "/__dev/api/supervise/diff", handler: handleSuperviseStub },
23931
23969
  { method: "POST", pattern: "/__dev/api/supervise/commit", handler: handleSuperviseStub },
23932
23970
  { method: "POST", pattern: "/__dev/api/supervise/cancel", handler: handleSuperviseStub },
23933
- // Execute — proxies to the framework_port+2000 Rust agent (SSE passthrough)
23934
- { method: "POST", pattern: "/__dev/api/execute", handler: handleExecute },
23935
23971
  // Framework-grounding MCP token config — self-contained (.env upsert)
23936
23972
  { method: "GET", pattern: "/__dev/api/grounding/status", handler: handleGroundingStatus },
23937
23973
  { method: "POST", pattern: "/__dev/api/grounding/token", handler: handleGroundingToken },
@@ -24501,9 +24537,6 @@ var init_devAdmin = __esm({
24501
24537
  }
24502
24538
  res.json({ tool, status: "executed", message: `Tool '${tool}' executed (stub)`, timestamp: (/* @__PURE__ */ new Date()).toISOString() });
24503
24539
  };
24504
- handleChat = async (req2, res) => {
24505
- await proxyToSupervisor(req2, res, "/chat");
24506
- };
24507
24540
  DEFAULT_MCP_URL = "https://mcp.tina4.com";
24508
24541
  handleGroundingStatus = async (_req, res) => {
24509
24542
  const token = resolveDevEnvVar("TINA4_MCP_TOKEN");
@@ -24791,35 +24824,6 @@ var init_devAdmin = __esm({
24791
24824
  501
24792
24825
  );
24793
24826
  };
24794
- handleExecute = async (req2, res) => {
24795
- const port = parseInt(process.env.TINA4_PORT ?? process.env.PORT ?? "7148", 10);
24796
- const agentUrl = `http://127.0.0.1:${port + 2e3}/execute`;
24797
- try {
24798
- const upstream = await fetch(agentUrl, {
24799
- method: "POST",
24800
- headers: { "Content-Type": "application/json" },
24801
- body: JSON.stringify(req2.body ?? {})
24802
- });
24803
- if (!upstream.body) {
24804
- res.json({ error: "agent returned no body" }, 502);
24805
- return;
24806
- }
24807
- res.raw.writeHead(upstream.status || 200, {
24808
- "Content-Type": upstream.headers.get("content-type") || "text/event-stream",
24809
- "Cache-Control": "no-cache",
24810
- Connection: "keep-alive"
24811
- });
24812
- const reader = upstream.body.getReader();
24813
- while (true) {
24814
- const { done, value } = await reader.read();
24815
- if (done) break;
24816
- if (value) res.raw.write(Buffer.from(value));
24817
- }
24818
- res.raw.end();
24819
- } catch (e) {
24820
- res.json({ error: `agent unreachable at ${agentUrl}: ${e.message}` }, 502);
24821
- }
24822
- };
24823
24827
  DEV_FILES_IGNORED = /* @__PURE__ */ new Set([
24824
24828
  "__pycache__",
24825
24829
  "node_modules",
@@ -27297,7 +27301,9 @@ function injectIntoHtml(ctx, devToolbar, html) {
27297
27301
  // Suppress the live reloader on the AI/stable port (data-reload="0"); the
27298
27302
  // toolbar JS early-returns when data-reload !== "1". Mirrors PHP's
27299
27303
  // suppressReload flag.
27300
- reload: !ctx.isAiPortRequest
27304
+ // Also suppress on the dev-admin dashboard (any /__dev page): its SPA reloads
27305
+ // itself gently, so the toolbar's full-page reloader must not fire there.
27306
+ reload: !ctx.isAiPortRequest && !ctx.pathname.startsWith("/__dev")
27301
27307
  };
27302
27308
  return injectFeedbackWidget(ctx.req, injectDevToolbar(html, toolbarCtx));
27303
27309
  }
@@ -27552,6 +27558,51 @@ async function runDispatch(ctx, rawReq, rawRes) {
27552
27558
  if (!rawRes.headersSent) rawRes.setHeader("x-request-id", requestId);
27553
27559
  return Log.runWithRequestId(requestId, () => dispatchInner(ctx, rawReq, rawRes, requestId));
27554
27560
  }
27561
+ function loopbackBindHosts(host) {
27562
+ const normalized = host.trim().replace(/^\[|\]$/g, "").toLowerCase();
27563
+ switch (normalized) {
27564
+ case "localhost":
27565
+ return ["127.0.0.1", "::1"];
27566
+ case "127.0.0.1":
27567
+ case "0.0.0.0":
27568
+ return ["::1"];
27569
+ case "::1":
27570
+ case "::":
27571
+ return ["127.0.0.1"];
27572
+ default:
27573
+ return [];
27574
+ }
27575
+ }
27576
+ function startLoopbackSiblings(port, host, dispatch) {
27577
+ const siblingHosts = loopbackBindHosts(host);
27578
+ if (siblingHosts.length === 0) return Promise.resolve([]);
27579
+ return Promise.all(
27580
+ siblingHosts.map(
27581
+ (siblingHost) => new Promise((resolveSibling) => {
27582
+ const sibling = createServer2(dispatch);
27583
+ let settled = false;
27584
+ sibling.on("error", (err) => {
27585
+ const expected = err.code === "EADDRINUSE" || err.code === "EADDRNOTAVAIL" || err.code === "EAFNOSUPPORT";
27586
+ if (!expected) {
27587
+ Log.debug(
27588
+ `Loopback sibling ${siblingHost}:${port} not bound (${err.code ?? err.message}) \u2014 skipped, primary already serves`
27589
+ );
27590
+ }
27591
+ if (!settled) {
27592
+ settled = true;
27593
+ resolveSibling(null);
27594
+ }
27595
+ });
27596
+ sibling.listen(port, siblingHost, () => {
27597
+ settled = true;
27598
+ resolveSibling(sibling);
27599
+ });
27600
+ })
27601
+ )
27602
+ ).then(
27603
+ (results) => results.filter((s) => s !== null)
27604
+ );
27605
+ }
27555
27606
  async function startServer(config) {
27556
27607
  loadEnv(".env.local");
27557
27608
  loadEnv();
@@ -27820,8 +27871,9 @@ ${reset2}
27820
27871
  }
27821
27872
  });
27822
27873
  return new Promise((resolvePromise) => {
27823
- server.listen(port, host, () => {
27874
+ server.listen(port, host, async () => {
27824
27875
  if (!cluster.isWorker) writePidfile(port);
27876
+ const siblingServers = await startLoopbackSiblings(port, host, dispatch);
27825
27877
  const displayHost = host === "0.0.0.0" ? "localhost" : host;
27826
27878
  const isDebug = isTruthy(process.env.TINA4_DEBUG);
27827
27879
  const logLevel = (process.env.TINA4_LOG_LEVEL ?? "DEBUG").toUpperCase();
@@ -27884,14 +27936,22 @@ ${reset2}
27884
27936
  }
27885
27937
  let shuttingDown = false;
27886
27938
  const closeListeners = () => new Promise((done) => {
27887
- let pending = aiServer ? 2 : 1;
27939
+ let pending = 1 + (aiServer ? 1 : 0) + siblingServers.length;
27888
27940
  const one = () => {
27889
27941
  if (--pending === 0) done();
27890
27942
  };
27891
27943
  server.close(one);
27892
27944
  if (aiServer) aiServer.close(one);
27945
+ for (const sibling of siblingServers) {
27946
+ try {
27947
+ sibling.close(one);
27948
+ } catch {
27949
+ one();
27950
+ }
27951
+ }
27893
27952
  server.closeIdleConnections();
27894
27953
  aiServer?.closeIdleConnections();
27954
+ for (const sibling of siblingServers) sibling.closeIdleConnections();
27895
27955
  });
27896
27956
  const gracefulShutdown = async (signal) => {
27897
27957
  if (shuttingDown) return;
@@ -27919,6 +27979,7 @@ ${reset2}
27919
27979
  );
27920
27980
  server.closeAllConnections();
27921
27981
  aiServer?.closeAllConnections();
27982
+ for (const sibling of siblingServers) sibling.closeAllConnections();
27922
27983
  }
27923
27984
  try {
27924
27985
  const orm = await Promise.resolve().then(() => (init_index(), index_exports));
@@ -27946,6 +28007,12 @@ ${reset2}
27946
28007
  stopAllBackgroundTasks();
27947
28008
  if (aiServer) aiServer.close();
27948
28009
  server.close();
28010
+ for (const sibling of siblingServers) {
28011
+ try {
28012
+ sibling.close();
28013
+ } catch {
28014
+ }
28015
+ }
27949
28016
  Promise.resolve().then(() => (init_index(), index_exports)).then((orm) => orm.closeDatabase()).catch(() => {
27950
28017
  });
27951
28018
  },
@@ -35662,6 +35729,7 @@ __export(src_exports2, {
35662
35729
  isValidSessionId: () => isValidSessionId,
35663
35730
  kafkaSecurityConfig: () => kafkaSecurityConfig,
35664
35731
  loadEnv: () => loadEnv,
35732
+ loopbackBindHosts: () => loopbackBindHosts,
35665
35733
  makeCaseInsensitiveHeaders: () => makeCaseInsensitiveHeaders,
35666
35734
  matchCronField: () => matchCronField,
35667
35735
  matchesCron: () => matchesCron,
@@ -43053,6 +43121,7 @@ var init_baseModel = __esm({
43053
43121
  "src/baseModel.ts"() {
43054
43122
  "use strict";
43055
43123
  init_database();
43124
+ init_modelCollection();
43056
43125
  init_validation();
43057
43126
  init_queryBuilder();
43058
43127
  init_sqlite();
@@ -43288,6 +43357,30 @@ var init_baseModel = __esm({
43288
43357
  static getPkColumn() {
43289
43358
  return this.getDbColumn(this.getPkField());
43290
43359
  }
43360
+ /**
43361
+ * Shared read tail for the collection-returning finders (where / all / select
43362
+ * / find filter-form / withTrashed). Runs the SAME two calls `db.fetch()`
43363
+ * makes — the page fetch AND the COUNT probe over the SAME base SQL — hydrates
43364
+ * the rows into model instances, and returns a ModelCollection carrying the
43365
+ * total (ADR-0064).
43366
+ *
43367
+ * The total is FREE: `probeTotal` is the exact `COUNT(*)` probe `db.fetch()`
43368
+ * already runs; the ORM used to discard it. ZERO extra queries beyond that one
43369
+ * probe. `sql` MUST NOT carry its own LIMIT/OFFSET — `adapterFetch` applies
43370
+ * limit/offset to the page, and the probe wraps the un-limited SQL so it counts
43371
+ * the WHOLE filtered set, not the page.
43372
+ */
43373
+ static async _collect(sql, params, limit, offset, include) {
43374
+ const db = this.getDb();
43375
+ const rows = await adapterFetch(db, sql, params, limit, offset);
43376
+ const data = Array.isArray(rows) ? rows : rows?.data ?? [];
43377
+ const total = await probeTotal(db, sql, params, limit) ?? data.length;
43378
+ const instances = data.map((row) => new this(row));
43379
+ if (include) {
43380
+ await this._eagerLoad(instances, include);
43381
+ }
43382
+ return new ModelCollection(instances, total, limit, offset);
43383
+ }
43291
43384
  /**
43292
43385
  * Find a record by primary key.
43293
43386
  * @param id Primary key value.
@@ -43340,7 +43433,6 @@ var init_baseModel = __esm({
43340
43433
  );
43341
43434
  }
43342
43435
  const lim = typeof limit === "number" ? limit : 100;
43343
- const db = ModelClass.getDb();
43344
43436
  const conditions = [];
43345
43437
  const params = [];
43346
43438
  if (filter) {
@@ -43360,16 +43452,7 @@ var init_baseModel = __esm({
43360
43452
  if (orderBy) {
43361
43453
  sql += ` ORDER BY ${orderBy}`;
43362
43454
  }
43363
- const rows = await adapterFetch(db, sql, params, lim, offset);
43364
- const data = rows?.data ?? rows;
43365
- const instances = (Array.isArray(data) ? data : []).map((row) => {
43366
- const inst = new this(row);
43367
- return inst;
43368
- });
43369
- if (include) {
43370
- await ModelClass._eagerLoad(instances, include);
43371
- }
43372
- return instances;
43455
+ return ModelClass._collect(sql, params, lim, offset, include);
43373
43456
  }
43374
43457
  /**
43375
43458
  * Load a record into this instance via selectOne.
@@ -43437,7 +43520,6 @@ var init_baseModel = __esm({
43437
43520
  */
43438
43521
  static async all(limit = DEFAULT_ROW_CAP, offset = 0, include, orderBy) {
43439
43522
  const ModelClass = this;
43440
- const db = ModelClass.getDb();
43441
43523
  const conditions = [];
43442
43524
  if (ModelClass.softDelete) {
43443
43525
  conditions.push("is_deleted = 0");
@@ -43447,13 +43529,8 @@ var init_baseModel = __esm({
43447
43529
  }
43448
43530
  const whereClause = conditions.length > 0 ? ` WHERE ${conditions.join(" AND ")}` : "";
43449
43531
  const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
43450
- const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause} LIMIT ${limit} OFFSET ${offset}`;
43451
- const rows = await adapterQuery(db, sql, []);
43452
- const instances = rows.map((row) => new ModelClass(row));
43453
- if (include) {
43454
- await ModelClass._eagerLoad(instances, include);
43455
- }
43456
- return instances;
43532
+ const sql = `SELECT * FROM "${ModelClass.tableName}"${whereClause}${orderClause}`;
43533
+ return ModelClass._collect(sql, [], limit, offset, include);
43457
43534
  }
43458
43535
  /**
43459
43536
  * Query records with a WHERE clause.
@@ -43468,7 +43545,6 @@ var init_baseModel = __esm({
43468
43545
  */
43469
43546
  static async where(conditions, params, limit = DEFAULT_ROW_CAP, offset = 0, include, orderBy) {
43470
43547
  const ModelClass = this;
43471
- const db = ModelClass.getDb();
43472
43548
  const parts = [];
43473
43549
  if (ModelClass.softDelete) {
43474
43550
  parts.push("is_deleted = 0");
@@ -43478,13 +43554,8 @@ var init_baseModel = __esm({
43478
43554
  }
43479
43555
  parts.push(`(${conditions})`);
43480
43556
  const orderClause = orderBy ? ` ORDER BY ${orderBy}` : "";
43481
- const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")}${orderClause} LIMIT ${limit} OFFSET ${offset}`;
43482
- const rows = await adapterQuery(db, sql, params);
43483
- const instances = rows.map((row) => new ModelClass(row));
43484
- if (include) {
43485
- await ModelClass._eagerLoad(instances, include);
43486
- }
43487
- return instances;
43557
+ const sql = `SELECT * FROM "${ModelClass.tableName}" WHERE ${parts.join(" AND ")}${orderClause}`;
43558
+ return ModelClass._collect(sql, params, limit, offset, include);
43488
43559
  }
43489
43560
  /**
43490
43561
  * Save this instance (insert or update). Returns this on success (fluent
@@ -43964,10 +44035,7 @@ var init_baseModel = __esm({
43964
44035
  */
43965
44036
  static async select(sql, params, limit = DEFAULT_ROW_CAP, offset = 0) {
43966
44037
  const ModelClass = this;
43967
- const db = ModelClass.getDb();
43968
- const paged = SQLTranslator.appendLimit(sql, limit, offset);
43969
- const rows = await adapterQuery(db, paged, params);
43970
- return rows.map((row) => new ModelClass(row));
44038
+ return ModelClass._collect(sql, params, limit, offset);
43971
44039
  }
43972
44040
  static async selectOne(sql, params, include) {
43973
44041
  const ModelClass = this;
@@ -44039,7 +44107,6 @@ var init_baseModel = __esm({
44039
44107
  */
44040
44108
  static async withTrashed(conditions, params, limit = DEFAULT_ROW_CAP, offset = 0) {
44041
44109
  const ModelClass = this;
44042
- const db = ModelClass.getDb();
44043
44110
  const parts = [];
44044
44111
  if (ModelClass.tableFilter) {
44045
44112
  parts.push(ModelClass.tableFilter);
@@ -44051,14 +44118,7 @@ var init_baseModel = __esm({
44051
44118
  if (parts.length > 0) {
44052
44119
  sql += ` WHERE ${parts.join(" AND ")}`;
44053
44120
  }
44054
- if (limit !== void 0) {
44055
- sql += ` LIMIT ${limit}`;
44056
- }
44057
- if (offset !== void 0) {
44058
- sql += ` OFFSET ${offset}`;
44059
- }
44060
- const rows = await adapterQuery(db, sql, params);
44061
- return rows.map((row) => new ModelClass(row));
44121
+ return ModelClass._collect(sql, params, limit, offset);
44062
44122
  }
44063
44123
  /**
44064
44124
  * Count records matching conditions (respects soft delete and table filter).
@@ -46866,6 +46926,7 @@ __export(index_exports, {
46866
46926
  InvalidId: () => InvalidId,
46867
46927
  LocalStorage: () => LocalStorage,
46868
46928
  Migration: () => Migration,
46929
+ ModelCollection: () => ModelCollection,
46869
46930
  MongodbAdapter: () => MongodbAdapter,
46870
46931
  MssqlAdapter: () => MssqlAdapter,
46871
46932
  MysqlAdapter: () => MysqlAdapter,
@@ -46969,6 +47030,7 @@ var init_index = __esm({
46969
47030
  "src/index.ts"() {
46970
47031
  init_types();
46971
47032
  init_databaseResult();
47033
+ init_modelCollection();
46972
47034
  init_database();
46973
47035
  init_database();
46974
47036
  init_databaseUrl();
@@ -47028,6 +47090,7 @@ export {
47028
47090
  InvalidId,
47029
47091
  LocalStorage,
47030
47092
  Migration,
47093
+ ModelCollection,
47031
47094
  MongodbAdapter,
47032
47095
  MssqlAdapter,
47033
47096
  MysqlAdapter,