@ultimat3/cli 27.4.0 → 27.5.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/cli",
3
- "version": "27.4.0",
3
+ "version": "27.5.0",
4
4
  "description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -39,33 +39,33 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "@babel/core": "^8.0.1",
42
- "@ultimat3/action": "27.4.0",
43
- "@ultimat3/admin": "27.4.0",
44
- "@ultimat3/ai": "27.4.0",
45
- "@ultimat3/auth": "27.4.0",
46
- "@ultimat3/cache": "27.4.0",
47
- "@ultimat3/core": "27.4.0",
48
- "@ultimat3/db": "27.4.0",
49
- "@ultimat3/entity": "27.4.0",
50
- "@ultimat3/flags": "27.4.0",
51
- "@ultimat3/http": "27.4.0",
52
- "@ultimat3/i18n": "27.4.0",
53
- "@ultimat3/jobs": "27.4.0",
54
- "@ultimat3/mail": "27.4.0",
55
- "@ultimat3/manifest": "27.4.0",
56
- "@ultimat3/mcp": "27.4.0",
57
- "@ultimat3/money": "27.4.0",
58
- "@ultimat3/notify": "27.4.0",
59
- "@ultimat3/policy": "27.4.0",
60
- "@ultimat3/pwa": "27.4.0",
61
- "@ultimat3/query": "27.4.0",
62
- "@ultimat3/realtime": "27.4.0",
63
- "@ultimat3/render": "27.4.0",
64
- "@ultimat3/schema": "27.4.0",
65
- "@ultimat3/seo": "27.4.0",
66
- "@ultimat3/storage": "27.4.0",
67
- "@ultimat3/testing": "27.4.0",
68
- "@ultimat3/time": "27.4.0",
42
+ "@ultimat3/action": "27.5.0",
43
+ "@ultimat3/admin": "27.5.0",
44
+ "@ultimat3/ai": "27.5.0",
45
+ "@ultimat3/auth": "27.5.0",
46
+ "@ultimat3/cache": "27.5.0",
47
+ "@ultimat3/core": "27.5.0",
48
+ "@ultimat3/db": "27.5.0",
49
+ "@ultimat3/entity": "27.5.0",
50
+ "@ultimat3/flags": "27.5.0",
51
+ "@ultimat3/http": "27.5.0",
52
+ "@ultimat3/i18n": "27.5.0",
53
+ "@ultimat3/jobs": "27.5.0",
54
+ "@ultimat3/mail": "27.5.0",
55
+ "@ultimat3/manifest": "27.5.0",
56
+ "@ultimat3/mcp": "27.5.0",
57
+ "@ultimat3/money": "27.5.0",
58
+ "@ultimat3/notify": "27.5.0",
59
+ "@ultimat3/policy": "27.5.0",
60
+ "@ultimat3/pwa": "27.5.0",
61
+ "@ultimat3/query": "27.5.0",
62
+ "@ultimat3/realtime": "27.5.0",
63
+ "@ultimat3/render": "27.5.0",
64
+ "@ultimat3/schema": "27.5.0",
65
+ "@ultimat3/seo": "27.5.0",
66
+ "@ultimat3/storage": "27.5.0",
67
+ "@ultimat3/testing": "27.5.0",
68
+ "@ultimat3/time": "27.5.0",
69
69
  "babel-preset-solid": "^1.9.15"
70
70
  }
71
71
  }
package/src/role-start.ts CHANGED
@@ -39,7 +39,7 @@ import { rolesUnderRealtime } from './role-realtime';
39
39
  import { startReplicator } from './role-replicator';
40
40
  import type { RunningRoles, StartRolesOptions } from './role-start-types';
41
41
  import { DEV_ROLES } from './role-start-types';
42
- import { prepareSync, type RunningSync } from './role-sync';
42
+ import { prepareSync, type RunningSync, syncReadsChanges } from './role-sync';
43
43
  import { startWorkerWake } from './role-wake';
44
44
  import { devHooks } from './runtime-hooks';
45
45
  import { workerOptionsFor } from './runtime-jobs';
@@ -393,6 +393,7 @@ export async function startRoles(options: StartRolesOptions): Promise<RunningRol
393
393
  dbMode: options.runtime.services.db.mode,
394
394
  transport: options.runtime.transport.name,
395
395
  replicatorHere: replicator !== null,
396
+ readsChanges: syncReadsChanges(),
396
397
  });
397
398
  started.push(async () => live.stop());
398
399
  // The web server and the sync node mark ready when they bind; a process with neither — a
package/src/role-sync.ts CHANGED
@@ -14,6 +14,7 @@ import { listQueries } from '@ultimat3/query';
14
14
  import type { SyncNode, SyncWs } from '@ultimat3/realtime/server';
15
15
  import {
16
16
  ChannelHub,
17
+ describeChannels,
17
18
  LiveQueryRegistry,
18
19
  listenSyncNode,
19
20
  liveQueryDefinition,
@@ -195,6 +196,19 @@ export function registerLiveQueries(options: StartRolesOptions): LiveQueryRegist
195
196
  return registry;
196
197
  }
197
198
 
199
+ /**
200
+ * Whether a node in this process would read a committed change at all: a `live: true` query, or a
201
+ * declared channel that lists `records`. What is left when neither exists is events channels, and
202
+ * those ride the transport from whoever publishes them (`publishChannelEvent`) — so the boot asks
203
+ * this before it holds a real database to having a replicator (`runtime-live-feed.ts`).
204
+ */
205
+ export function syncReadsChanges(): boolean {
206
+ return (
207
+ listQueries().some((target) => target.isLive) ||
208
+ describeChannels().some((declared) => declared.records.length > 0)
209
+ );
210
+ }
211
+
198
212
  /**
199
213
  * Who dialled the socket, resolved by `@ultimat3/http`'s `clientAddress()` — the rule the web role's
200
214
  * `ctx.ip` follows, at the same `TRUSTED_PROXY_HOPS`. It keys an anonymous live reader's rate
@@ -40,6 +40,12 @@ export interface LiveFeedInput {
40
40
  readonly transport: string;
41
41
  /** Whether the `replicator` role runs in THIS process. */
42
42
  readonly replicatorHere: boolean;
43
+ /**
44
+ * Whether anything this node serves reads a committed change: a `live: true` query, or a channel
45
+ * that lists `records` (`role-sync.ts`'s `syncReadsChanges`). An events-only channel does not —
46
+ * the app publishes its frames through the transport — so such a node needs no replicator.
47
+ */
48
+ readonly readsChanges: boolean;
43
49
  }
44
50
 
45
51
  /** The label the `x dev` boot line carries beside `db=`, `events=` and `storage=`. */
@@ -49,11 +55,16 @@ export const liveFeedLabel = (feed: LiveFeed): string => `live=${feed}`;
49
55
  * `embedded` → the in-process bridge; anything else → replication, which is the WAL decoder's
50
56
  * job whether the replicator role runs in this process or another. Never both: with a real
51
57
  * database the decoder already delivers this process's own writes, and a bridge beside it would
52
- * deliver every one of them twice. No sync node, no feed to speak of.
58
+ * deliver every one of them twice. No sync node, no feed to speak of — and none for a node on a
59
+ * real database that serves only events channels, which the transport alone carries.
53
60
  */
54
61
  export async function startLiveFeed(input: LiveFeedInput): Promise<RunningLiveFeed> {
55
62
  if (input.sync === null) return { feed: 'none', bridge: null, stop: () => undefined };
56
63
  if (input.dbMode !== 'embedded') {
64
+ // Nothing here reads the log, so nothing is owed one: refusing this node demanded a
65
+ // replication slot (and the cluster-wide REPLICATION grant) to carry channel events the
66
+ // write-ahead log never held. `none`, said on the boot line, never a `replication` nobody runs.
67
+ if (!input.readsChanges) return { feed: 'none', bridge: null, stop: () => undefined };
57
68
  // A decoder this node can hear: in this process, or on a bus other processes share. Neither
58
69
  // is a sync node that reported `replication` while nothing could ever reach it.
59
70
  if (input.transport === 'in-process' && !input.replicatorHere)
@@ -27,7 +27,7 @@ import {
27
27
  setMailDriver,
28
28
  } from '@ultimat3/mail';
29
29
  import type { Transport, TransportSelection } from '@ultimat3/realtime/server';
30
- import { selectTransport } from '@ultimat3/realtime/server';
30
+ import { selectTransport, setChannelTransport } from '@ultimat3/realtime/server';
31
31
  import type { Storage } from '@ultimat3/storage';
32
32
  import { defineStorage, localDriver, s3Driver, usesDevStorageSecret } from '@ultimat3/storage';
33
33
  import { loadAppConfig } from './app-config-load';
@@ -415,6 +415,10 @@ export async function startServices(
415
415
  started.push(() => bus.transport.close());
416
416
  transport = bus.transport;
417
417
  }
418
+ // The bus an app's own code publishes a channel event on (`publishChannelEvent`), in EVERY
419
+ // role: the hub is the `sync` role's, so a job or an action reached none, and an events-only
420
+ // channel — the one kind that needs no replicator — could be fed by nothing but a socket.
421
+ started.push(setChannelTransport(transport));
418
422
  // Only a transport that can be disconnected gets a check. `NatsTransport.connected` is a
419
423
  // synchronous getter over the client's own state, so no probe is needed; the in-process bus
420
424
  // has nothing to lose a connection to, and a check that can only answer `true` is a number in