@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 +28 -28
- package/src/role-start.ts +2 -1
- package/src/role-sync.ts +14 -0
- package/src/runtime-live-feed.ts +12 -1
- package/src/runtime-services.ts +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ultimat3/cli",
|
|
3
|
-
"version": "27.
|
|
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.
|
|
43
|
-
"@ultimat3/admin": "27.
|
|
44
|
-
"@ultimat3/ai": "27.
|
|
45
|
-
"@ultimat3/auth": "27.
|
|
46
|
-
"@ultimat3/cache": "27.
|
|
47
|
-
"@ultimat3/core": "27.
|
|
48
|
-
"@ultimat3/db": "27.
|
|
49
|
-
"@ultimat3/entity": "27.
|
|
50
|
-
"@ultimat3/flags": "27.
|
|
51
|
-
"@ultimat3/http": "27.
|
|
52
|
-
"@ultimat3/i18n": "27.
|
|
53
|
-
"@ultimat3/jobs": "27.
|
|
54
|
-
"@ultimat3/mail": "27.
|
|
55
|
-
"@ultimat3/manifest": "27.
|
|
56
|
-
"@ultimat3/mcp": "27.
|
|
57
|
-
"@ultimat3/money": "27.
|
|
58
|
-
"@ultimat3/notify": "27.
|
|
59
|
-
"@ultimat3/policy": "27.
|
|
60
|
-
"@ultimat3/pwa": "27.
|
|
61
|
-
"@ultimat3/query": "27.
|
|
62
|
-
"@ultimat3/realtime": "27.
|
|
63
|
-
"@ultimat3/render": "27.
|
|
64
|
-
"@ultimat3/schema": "27.
|
|
65
|
-
"@ultimat3/seo": "27.
|
|
66
|
-
"@ultimat3/storage": "27.
|
|
67
|
-
"@ultimat3/testing": "27.
|
|
68
|
-
"@ultimat3/time": "27.
|
|
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
|
package/src/runtime-live-feed.ts
CHANGED
|
@@ -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)
|
package/src/runtime-services.ts
CHANGED
|
@@ -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
|