@hotmeshio/long-tail 0.28.0 → 0.28.2
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/build/api/files.d.ts +5 -0
- package/build/api/files.js +19 -2
- package/build/api/knowledge.d.ts +3 -1
- package/build/lib/db/schemas/042_knowledge_edition_labels.sql +9 -0
- package/build/lib/events/publish.js +3 -2
- package/build/lib/machines/line.d.ts +3 -0
- package/build/lib/machines/line.js +7 -1
- package/build/lib/machines/protocol/codec.d.ts +6 -0
- package/build/lib/machines/protocol/codec.js +1 -1
- package/build/lib/machines/protocol/contract.d.ts +2 -0
- package/build/lib/machines/protocol/contract.js +12 -7
- package/build/lib/machines/site/mosquitto-conf.d.ts +7 -3
- package/build/lib/machines/site/mosquitto-conf.js +3 -8
- package/build/lib/machines/site/site-conf.d.ts +4 -1
- package/build/lib/machines/site/site-conf.js +5 -1
- package/build/lib/machines/transport/nats-bus.d.ts +4 -0
- package/build/lib/machines/transport/stream-bus.d.ts +7 -1
- package/build/lib/machines/transport/stream-bus.js +20 -6
- package/build/lib/machines/transport/streams.d.ts +11 -2
- package/build/lib/machines/transport/streams.js +33 -5
- package/build/lib/messaging/memory.js +10 -3
- package/build/lib/messaging/nats/permissions.js +1 -0
- package/build/lib/messaging/nats/streams.d.ts +21 -4
- package/build/lib/messaging/nats/streams.js +71 -10
- package/build/lib/storage/local.d.ts +2 -0
- package/build/lib/storage/local.js +3 -0
- package/build/lib/storage/s3.d.ts +3 -2
- package/build/lib/storage/s3.js +6 -1
- package/build/lib/storage/types.d.ts +7 -1
- package/build/routes/file-browser.js +3 -1
- package/build/routes/knowledge.js +6 -5
- package/build/routes/machines.js +33 -0
- package/build/services/knowledge/index.d.ts +4 -2
- package/build/services/knowledge/index.js +8 -2
- package/build/services/knowledge/lookup-cache.d.ts +8 -0
- package/build/services/knowledge/lookup-cache.js +30 -3
- package/build/services/knowledge/lookup-refs.js +1 -1
- package/build/services/knowledge/sql.d.ts +4 -2
- package/build/services/knowledge/sql.js +8 -3
- package/build/services/machines/config.d.ts +2 -0
- package/build/services/machines/config.js +1 -0
- package/build/services/machines/contracts.d.ts +18 -0
- package/build/services/machines/contracts.js +15 -1
- package/build/services/machines/forms.d.ts +26 -1
- package/build/services/machines/forms.js +17 -1
- package/build/services/machines/gateway/actions.d.ts +13 -1
- package/build/services/machines/gateway/admission.d.ts +15 -1
- package/build/services/machines/gateway/admission.js +47 -0
- package/build/services/machines/gateway/api-escalations.js +4 -0
- package/build/services/machines/gateway/dispatcher.d.ts +9 -4
- package/build/services/machines/gateway/dispatcher.js +42 -14
- package/build/services/machines/gateway/executor.d.ts +2 -0
- package/build/services/machines/gateway/executor.js +18 -2
- package/build/services/machines/gateway/gateway.d.ts +5 -0
- package/build/services/machines/gateway/gateway.js +11 -3
- package/build/services/machines/gateway/log.d.ts +3 -0
- package/build/services/machines/gateway/log.js +3 -0
- package/build/services/machines/gateway/lt-http.js +11 -2
- package/build/services/machines/gateway/pacing.js +1 -1
- package/build/services/machines/gateway/plan-machine.js +26 -11
- package/build/services/machines/gateway/plan-shared.d.ts +5 -3
- package/build/services/machines/gateway/plan-shared.js +6 -4
- package/build/services/machines/gateway/reply-waiter.d.ts +2 -0
- package/build/services/machines/gateway/state.d.ts +24 -1
- package/build/services/machines/gateway/state.js +67 -6
- package/build/services/machines/gateway/wire-window.d.ts +21 -0
- package/build/services/machines/gateway/wire-window.js +61 -0
- package/build/services/machines/gateway/wire.js +1 -0
- package/build/services/machines/leadership.d.ts +2 -0
- package/build/services/machines/leadership.js +7 -1
- package/build/services/machines/roles.d.ts +5 -4
- package/build/services/machines/roles.js +13 -4
- package/build/services/machines/select.d.ts +3 -2
- package/build/services/machines/select.js +4 -3
- package/build/services/machines/surface.d.ts +10 -1
- package/build/services/machines/surface.js +27 -5
- package/build/system/activities/knowledge.d.ts +4 -0
- package/build/system/activities/knowledge.js +15 -0
- package/build/system/activities/sql.d.ts +2 -0
- package/build/system/activities/sql.js +6 -1
- package/build/system/mcp-servers/knowledge.js +4 -2
- package/build/tsconfig.tsbuildinfo +1 -1
- package/build/types/escalation.d.ts +9 -6
- package/build/types/escalation.js +8 -2
- package/build/types/machines-view.d.ts +16 -0
- package/build/types/machines.d.ts +1 -0
- package/build/types/messaging.d.ts +7 -1
- package/build/types/startup.d.ts +2 -0
- package/docs/dashboard.md +1 -1
- package/docs/hitl/lookups.md +6 -4
- package/docs/machines.md +26 -17
- package/package.json +1 -1
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
3
|
* Durable streams on JetStream. A spec becomes a file-backed stream or an
|
|
4
|
-
* explicit-ack durable consumer; ensuring either is safe on every boot
|
|
5
|
-
* existing consumer keeps its position while
|
|
4
|
+
* explicit-ack durable consumer; ensuring either is safe on every boot and
|
|
5
|
+
* from several lines at once. An existing consumer keeps its position while
|
|
6
|
+
* its subjects match the spec; one whose subjects changed is made again, since
|
|
7
|
+
* a position in another protocol's messages means nothing to this one.
|
|
6
8
|
*/
|
|
7
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
10
|
exports.NatsStreams = void 0;
|
|
9
11
|
exports.streamConfig = streamConfig;
|
|
10
12
|
exports.consumerConfig = consumerConfig;
|
|
11
13
|
exports.ensureStream = ensureStream;
|
|
14
|
+
exports.describeStream = describeStream;
|
|
12
15
|
exports.ensureConsumer = ensureConsumer;
|
|
16
|
+
exports.describeConsumer = describeConsumer;
|
|
13
17
|
exports.payloadOf = payloadOf;
|
|
14
18
|
const nats_1 = require("nats");
|
|
15
19
|
const constants_1 = require("../constants");
|
|
16
20
|
const DEFAULT_ACK_WAIT_MS = 30_000;
|
|
17
21
|
const DEFAULT_NAK_DELAY_MS = 1_000;
|
|
22
|
+
/** Consumer events after which the consumer delivers nothing more: its stream or itself is gone, or the server fell silent. */
|
|
23
|
+
const CONSUMER_LOST = new Set(['consumer_deleted', 'consumer_not_found', 'stream_not_found', 'heartbeats_missed']);
|
|
24
|
+
const sameSubjects = (a = [], b = []) => [...a].sort().join('|') === [...b].sort().join('|');
|
|
18
25
|
function streamConfig(spec) {
|
|
19
26
|
return { name: spec.name, subjects: spec.subjects, max_age: (0, nats_1.nanos)(spec.maxAgeMs), storage: nats_1.StorageType.File };
|
|
20
27
|
}
|
|
@@ -27,7 +34,11 @@ function consumerConfig(spec) {
|
|
|
27
34
|
filter_subjects: spec.filterSubjects,
|
|
28
35
|
};
|
|
29
36
|
}
|
|
30
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Create the stream, or update an existing one to the spec. Two lines may
|
|
39
|
+
* ensure the same stream at once: the create that loses the race finds the
|
|
40
|
+
* stream in use and updates it instead.
|
|
41
|
+
*/
|
|
31
42
|
async function ensureStream(jsm, spec) {
|
|
32
43
|
const config = streamConfig(spec);
|
|
33
44
|
try {
|
|
@@ -36,12 +47,35 @@ async function ensureStream(jsm, spec) {
|
|
|
36
47
|
catch (err) {
|
|
37
48
|
if (!/stream not found/i.test(err?.message ?? ''))
|
|
38
49
|
throw err;
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
try {
|
|
51
|
+
await jsm.streams.add(config);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
catch (added) {
|
|
55
|
+
if (!/already in use/i.test(added?.message ?? ''))
|
|
56
|
+
throw added;
|
|
57
|
+
}
|
|
41
58
|
}
|
|
42
59
|
await jsm.streams.update(spec.name, config);
|
|
43
60
|
}
|
|
44
|
-
/**
|
|
61
|
+
/** The stream as JetStream holds it, or null when there is none. */
|
|
62
|
+
async function describeStream(jsm, name) {
|
|
63
|
+
try {
|
|
64
|
+
const info = await jsm.streams.info(name);
|
|
65
|
+
return { name, subjects: info.config.subjects ?? [], maxAgeMs: Math.round((info.config.max_age ?? 0) / 1_000_000) };
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
if (/stream not found/i.test(err?.message ?? ''))
|
|
69
|
+
return null;
|
|
70
|
+
throw err;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Create the consumer, or keep an existing one whose subjects match the spec,
|
|
75
|
+
* position and all. One whose subjects differ is deleted and made again at
|
|
76
|
+
* the stream's newest message, since its old position was in another
|
|
77
|
+
* protocol's messages.
|
|
78
|
+
*/
|
|
45
79
|
async function ensureConsumer(jsm, stream, spec) {
|
|
46
80
|
let existing;
|
|
47
81
|
try {
|
|
@@ -53,10 +87,22 @@ async function ensureConsumer(jsm, stream, spec) {
|
|
|
53
87
|
await jsm.consumers.add(stream, consumerConfig(spec));
|
|
54
88
|
return;
|
|
55
89
|
}
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
90
|
+
if (sameSubjects(spec.filterSubjects, existing.config.filter_subjects))
|
|
91
|
+
return;
|
|
92
|
+
await jsm.consumers.delete(stream, spec.name);
|
|
93
|
+
await jsm.consumers.add(stream, consumerConfig(spec));
|
|
94
|
+
}
|
|
95
|
+
/** The consumer as JetStream holds it, or null when there is none. */
|
|
96
|
+
async function describeConsumer(jsm, stream, name) {
|
|
97
|
+
try {
|
|
98
|
+
const info = await jsm.consumers.info(stream, name);
|
|
99
|
+
return { name, filterSubjects: info.config.filter_subjects ?? (info.config.filter_subject ? [info.config.filter_subject] : []) };
|
|
100
|
+
}
|
|
101
|
+
catch (err) {
|
|
102
|
+
if (/consumer not found|stream not found/i.test(err?.message ?? ''))
|
|
103
|
+
return null;
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
60
106
|
}
|
|
61
107
|
function payloadOf(m) {
|
|
62
108
|
return Buffer.from(m.data).toString('utf8');
|
|
@@ -72,6 +118,12 @@ class NatsStreams {
|
|
|
72
118
|
ensureConsumer(stream, spec) {
|
|
73
119
|
return ensureConsumer(this.jsm, stream, spec);
|
|
74
120
|
}
|
|
121
|
+
describeStream(name) {
|
|
122
|
+
return describeStream(this.jsm, name);
|
|
123
|
+
}
|
|
124
|
+
describeConsumer(stream, name) {
|
|
125
|
+
return describeConsumer(this.jsm, stream, name);
|
|
126
|
+
}
|
|
75
127
|
async append(subject, payload) {
|
|
76
128
|
await this.js.publish(subject, payload);
|
|
77
129
|
}
|
|
@@ -79,6 +131,15 @@ class NatsStreams {
|
|
|
79
131
|
async consume(stream, consumer, handler, opts = {}) {
|
|
80
132
|
const nakDelayMs = opts.nakDelayMs ?? DEFAULT_NAK_DELAY_MS;
|
|
81
133
|
const messages = await (await this.js.consumers.get(stream, consumer)).consume();
|
|
134
|
+
// The consumer's own status: once its stream or itself is gone, or the server fell silent, the reader is told.
|
|
135
|
+
void (async () => {
|
|
136
|
+
for await (const status of await messages.status()) {
|
|
137
|
+
if (!CONSUMER_LOST.has(String(status.type)))
|
|
138
|
+
continue;
|
|
139
|
+
opts.onLost?.(String(status.type));
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
})().catch(() => { });
|
|
82
143
|
void (async () => {
|
|
83
144
|
for await (const m of messages) {
|
|
84
145
|
const delivery = await handler({ subject: m.subject, payload: payloadOf(m), deliveries: m.info.deliveryCount })
|
|
@@ -32,10 +32,12 @@ export declare class LocalStorageBackend implements StorageBackend {
|
|
|
32
32
|
directories: string[];
|
|
33
33
|
nextToken?: string;
|
|
34
34
|
}>;
|
|
35
|
+
/** The digest is read from the file itself, so it is always the stored bytes' own. */
|
|
35
36
|
getMetadata(key: string): Promise<{
|
|
36
37
|
size: number;
|
|
37
38
|
modified_at: string;
|
|
38
39
|
content_type: string;
|
|
40
|
+
sha256: string;
|
|
39
41
|
}>;
|
|
40
42
|
getSignedUrl(key: string, expiresInSeconds: number): Promise<string>;
|
|
41
43
|
}
|
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.LocalStorageBackend = void 0;
|
|
40
|
+
const crypto_1 = require("crypto");
|
|
40
41
|
const fs = __importStar(require("fs"));
|
|
41
42
|
const path = __importStar(require("path"));
|
|
42
43
|
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
@@ -168,6 +169,7 @@ class LocalStorageBackend {
|
|
|
168
169
|
nextToken: hasMore ? String(offset + limit) : undefined,
|
|
169
170
|
};
|
|
170
171
|
}
|
|
172
|
+
/** The digest is read from the file itself, so it is always the stored bytes' own. */
|
|
171
173
|
async getMetadata(key) {
|
|
172
174
|
const resolved = resolveAndValidate(key);
|
|
173
175
|
if (!fs.existsSync(resolved)) {
|
|
@@ -178,6 +180,7 @@ class LocalStorageBackend {
|
|
|
178
180
|
size: stat.size,
|
|
179
181
|
modified_at: stat.mtime.toISOString(),
|
|
180
182
|
content_type: (0, mime_1.mimeFromPath)(key),
|
|
183
|
+
sha256: (0, crypto_1.createHash)('sha256').update(fs.readFileSync(resolved)).digest('hex'),
|
|
181
184
|
};
|
|
182
185
|
}
|
|
183
186
|
async getSignedUrl(key, expiresInSeconds) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StorageBackend } from './types';
|
|
1
|
+
import type { StorageBackend, WriteOptions } from './types';
|
|
2
2
|
export declare class S3StorageBackend implements StorageBackend {
|
|
3
3
|
private client;
|
|
4
4
|
private signingClient;
|
|
@@ -6,7 +6,7 @@ export declare class S3StorageBackend implements StorageBackend {
|
|
|
6
6
|
private bucketReady;
|
|
7
7
|
constructor();
|
|
8
8
|
private ensureBucket;
|
|
9
|
-
write(key: string, data: Buffer): Promise<{
|
|
9
|
+
write(key: string, data: Buffer, options?: WriteOptions): Promise<{
|
|
10
10
|
ref: string;
|
|
11
11
|
size: number;
|
|
12
12
|
}>;
|
|
@@ -42,6 +42,7 @@ export declare class S3StorageBackend implements StorageBackend {
|
|
|
42
42
|
size: number;
|
|
43
43
|
modified_at: string;
|
|
44
44
|
content_type: string;
|
|
45
|
+
sha256?: string;
|
|
45
46
|
}>;
|
|
46
47
|
getSignedUrl(key: string, expiresInSeconds: number): Promise<string>;
|
|
47
48
|
}
|
package/build/lib/storage/s3.js
CHANGED
|
@@ -39,6 +39,8 @@ const os = __importStar(require("os"));
|
|
|
39
39
|
const path = __importStar(require("path"));
|
|
40
40
|
const client_s3_1 = require("@aws-sdk/client-s3");
|
|
41
41
|
const s3_request_presigner_1 = require("@aws-sdk/s3-request-presigner");
|
|
42
|
+
/** The object metadata key the digest is kept under. */
|
|
43
|
+
const SHA256_METADATA = 'sha256';
|
|
42
44
|
const mime_1 = require("./mime");
|
|
43
45
|
const STAGING_DIR = path.join(os.tmpdir(), 'lt-staging');
|
|
44
46
|
class S3StorageBackend {
|
|
@@ -91,12 +93,13 @@ class S3StorageBackend {
|
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
}
|
|
94
|
-
async write(key, data) {
|
|
96
|
+
async write(key, data, options) {
|
|
95
97
|
await this.bucketReady;
|
|
96
98
|
await this.client.send(new client_s3_1.PutObjectCommand({
|
|
97
99
|
Bucket: this.bucket,
|
|
98
100
|
Key: normalizeKey(key),
|
|
99
101
|
Body: data,
|
|
102
|
+
...(options?.sha256 ? { Metadata: { [SHA256_METADATA]: options.sha256 } } : {}),
|
|
100
103
|
}));
|
|
101
104
|
return { ref: key, size: data.length };
|
|
102
105
|
}
|
|
@@ -222,10 +225,12 @@ class S3StorageBackend {
|
|
|
222
225
|
Bucket: this.bucket,
|
|
223
226
|
Key: normalizeKey(key),
|
|
224
227
|
}));
|
|
228
|
+
const sha256 = resp.Metadata?.[SHA256_METADATA];
|
|
225
229
|
return {
|
|
226
230
|
size: resp.ContentLength || 0,
|
|
227
231
|
modified_at: resp.LastModified?.toISOString() || new Date().toISOString(),
|
|
228
232
|
content_type: resp.ContentType || (0, mime_1.mimeFromPath)(key),
|
|
233
|
+
...(sha256 ? { sha256 } : {}),
|
|
229
234
|
};
|
|
230
235
|
}
|
|
231
236
|
async getSignedUrl(key, expiresInSeconds) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
export interface WriteOptions {
|
|
2
|
+
/** The SHA-256 of `data`, hex, kept with the object. */
|
|
3
|
+
sha256?: string;
|
|
4
|
+
}
|
|
1
5
|
export interface StorageBackend {
|
|
2
6
|
/** Write data to storage. Returns the key and size. */
|
|
3
|
-
write(key: string, data: Buffer): Promise<{
|
|
7
|
+
write(key: string, data: Buffer, options?: WriteOptions): Promise<{
|
|
4
8
|
ref: string;
|
|
5
9
|
size: number;
|
|
6
10
|
}>;
|
|
@@ -53,6 +57,8 @@ export interface StorageBackend {
|
|
|
53
57
|
size: number;
|
|
54
58
|
modified_at: string;
|
|
55
59
|
content_type: string;
|
|
60
|
+
/** The object's SHA-256, hex, when the backend knows it. */
|
|
61
|
+
sha256?: string;
|
|
56
62
|
}>;
|
|
57
63
|
/** Generate a time-limited signed URL for unauthenticated file access. */
|
|
58
64
|
getSignedUrl(key: string, expiresInSeconds: number): Promise<string>;
|
|
@@ -128,6 +128,7 @@ router.get('/download/{*filePath}', auth_1.requireBuilder, async (req, res) => {
|
|
|
128
128
|
* POST /api/file-browser/upload
|
|
129
129
|
* Upload a file to storage.
|
|
130
130
|
* Query: ?path=images/photo.png (target path including filename)
|
|
131
|
+
* &sha256=<hex> (optional; the upload is refused with 422 when the bytes differ)
|
|
131
132
|
* Body: raw file bytes (Content-Type should match the file type)
|
|
132
133
|
*/
|
|
133
134
|
router.post('/upload', auth_1.requireBuilder, async (req, res) => {
|
|
@@ -146,7 +147,8 @@ router.post('/upload', auth_1.requireBuilder, async (req, res) => {
|
|
|
146
147
|
res.status(400).json({ error: 'Empty file body' });
|
|
147
148
|
return;
|
|
148
149
|
}
|
|
149
|
-
const
|
|
150
|
+
const sha256 = typeof req.query.sha256 === 'string' ? req.query.sha256 : undefined;
|
|
151
|
+
const result = await api.uploadFile({ path: targetPath, buffer, sha256 });
|
|
150
152
|
res.status(result.status).json(result.data ?? { error: result.error });
|
|
151
153
|
}
|
|
152
154
|
catch (err) {
|
|
@@ -74,8 +74,8 @@ router.get('/entries', async (req, res) => {
|
|
|
74
74
|
/**
|
|
75
75
|
* GET /api/knowledge/entry
|
|
76
76
|
* Get a single knowledge entry — the live edition, or an immutable snapshot
|
|
77
|
-
* when ?version=N is present.
|
|
78
|
-
* Query: ?domain=...&key=...&version=N
|
|
77
|
+
* when ?version=N (or ?version=<label>) is present.
|
|
78
|
+
* Query: ?domain=...&key=...&version=N|label
|
|
79
79
|
*/
|
|
80
80
|
router.get('/entry', async (req, res) => {
|
|
81
81
|
const domain = req.query.domain;
|
|
@@ -85,9 +85,10 @@ router.get('/entry', async (req, res) => {
|
|
|
85
85
|
return;
|
|
86
86
|
}
|
|
87
87
|
if (req.query.version !== undefined) {
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
88
|
+
const raw = String(req.query.version);
|
|
89
|
+
const version = /^[1-9][0-9]*$/.test(raw) ? Number(raw) : raw;
|
|
90
|
+
if (version === '' || version === 'current') {
|
|
91
|
+
res.status(400).json({ error: 'version must be a positive integer or an edition label' });
|
|
91
92
|
return;
|
|
92
93
|
}
|
|
93
94
|
const result = await api.getEntryVersion({ domain, key, version });
|
package/build/routes/machines.js
CHANGED
|
@@ -42,6 +42,8 @@ const registry_1 = require("../services/machines/registry");
|
|
|
42
42
|
const registry_sync_1 = require("../services/machines/registry-sync");
|
|
43
43
|
const line_index_1 = require("../services/machines/line-index");
|
|
44
44
|
const topics_1 = require("../lib/machines/protocol/topics");
|
|
45
|
+
const browser_view_1 = require("../lib/events/browser-view");
|
|
46
|
+
const wire_window_1 = require("../services/machines/gateway/wire-window");
|
|
45
47
|
const router = (0, http_1.Router)();
|
|
46
48
|
/**
|
|
47
49
|
* The machine surface for the dashboard's Lines view. Reads and the dry run
|
|
@@ -170,6 +172,37 @@ router.get('/lines/:siteId/:lineId', auth_1.requireRoleManager, (req, res) => {
|
|
|
170
172
|
};
|
|
171
173
|
res.status(200).json(body);
|
|
172
174
|
});
|
|
175
|
+
/** The event as this viewer reads it: builders see the payload, everyone else the record without it. */
|
|
176
|
+
function wireForViewer(event, builder) {
|
|
177
|
+
if (builder)
|
|
178
|
+
return event;
|
|
179
|
+
const { payload: _payload, ...rest } = event;
|
|
180
|
+
return rest;
|
|
181
|
+
}
|
|
182
|
+
router.get('/lines/:siteId/:lineId/wire', auth_1.requireRoleManager, async (req, res) => {
|
|
183
|
+
const { surface, siteId, lineId, line } = lineOf(req);
|
|
184
|
+
if (!surface || !line) {
|
|
185
|
+
res.status(404).json({ error: surface ? `no line ${siteId}/${lineId}` : NO_LINES });
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const { direction, kind } = req.query;
|
|
189
|
+
if (direction !== undefined && !(0, wire_window_1.isWireDirection)(direction)) {
|
|
190
|
+
res.status(400).json({ error: 'direction must be in or out' });
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (kind !== undefined && !(0, wire_window_1.isWireKind)(kind)) {
|
|
194
|
+
res.status(400).json({ error: 'kind is not a wire kind' });
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const query = {
|
|
198
|
+
limit: queryInt(req.query.limit), machine: queryString(req.query.machine), since: queryString(req.query.since),
|
|
199
|
+
...(direction !== undefined ? { direction } : {}), ...(kind !== undefined ? { kind } : {}),
|
|
200
|
+
};
|
|
201
|
+
const events = surface.wire(siteId, lineId, query);
|
|
202
|
+
const viewer = await (0, browser_view_1.viewerFor)(req.auth?.userId);
|
|
203
|
+
const body = { siteId, lineId, leading: events !== null, events: (events ?? []).map((e) => wireForViewer(e, viewer.builder)) };
|
|
204
|
+
res.status(200).json(body);
|
|
205
|
+
});
|
|
173
206
|
router.post('/lines/:siteId/:lineId/inspect', auth_1.requireRoleManager, (req, res) => {
|
|
174
207
|
const { surface, siteId, lineId, line } = lineOf(req);
|
|
175
208
|
if (!surface || !line) {
|
|
@@ -2,15 +2,17 @@ export * from './lookup-cache';
|
|
|
2
2
|
export * from './lookup-refs';
|
|
3
3
|
export interface KnowledgeVersionMeta {
|
|
4
4
|
version: number;
|
|
5
|
+
label: string | null;
|
|
5
6
|
change_summary: string | null;
|
|
6
7
|
created_at: string;
|
|
7
8
|
is_current: boolean;
|
|
8
9
|
}
|
|
9
|
-
/** The immutable snapshot at (domain, key, version), uncached full shape. */
|
|
10
|
-
export declare function getKnowledgeVersion(domain: string, key: string, version: number): Promise<{
|
|
10
|
+
/** The immutable snapshot at (domain, key, version), by number or by the edition's label; uncached full shape. */
|
|
11
|
+
export declare function getKnowledgeVersion(domain: string, key: string, version: number | string): Promise<{
|
|
11
12
|
domain: string;
|
|
12
13
|
key: string;
|
|
13
14
|
version: number;
|
|
15
|
+
label: string | null;
|
|
14
16
|
data: Record<string, unknown>;
|
|
15
17
|
tags: string[];
|
|
16
18
|
created_at: string;
|
|
@@ -22,19 +22,24 @@ exports.listKnowledgeVersions = listKnowledgeVersions;
|
|
|
22
22
|
* writes produce, and resolves escalation lookup refs against them.
|
|
23
23
|
*/
|
|
24
24
|
const db_1 = require("../../lib/db");
|
|
25
|
+
const lookup_cache_1 = require("./lookup-cache");
|
|
25
26
|
const sql_1 = require("./sql");
|
|
26
27
|
__exportStar(require("./lookup-cache"), exports);
|
|
27
28
|
__exportStar(require("./lookup-refs"), exports);
|
|
28
|
-
/** The immutable snapshot at (domain, key, version), uncached full shape. */
|
|
29
|
+
/** The immutable snapshot at (domain, key, version), by number or by the edition's label; uncached full shape. */
|
|
29
30
|
async function getKnowledgeVersion(domain, key, version) {
|
|
30
31
|
const pool = (0, db_1.getPool)();
|
|
31
|
-
const
|
|
32
|
+
const number = typeof version === 'string' ? await (0, lookup_cache_1.editionOfLabel)(domain, key, version) : version;
|
|
33
|
+
if (number === null)
|
|
34
|
+
return null;
|
|
35
|
+
const { rows } = await pool.query(sql_1.GET_KNOWLEDGE_VERSION, [domain, key, number]);
|
|
32
36
|
if (!rows[0])
|
|
33
37
|
return null;
|
|
34
38
|
return {
|
|
35
39
|
domain: rows[0].domain,
|
|
36
40
|
key: rows[0].key,
|
|
37
41
|
version: rows[0].version,
|
|
42
|
+
label: rows[0].label ?? null,
|
|
38
43
|
data: rows[0].data ?? {},
|
|
39
44
|
tags: rows[0].tags ?? [],
|
|
40
45
|
created_at: rows[0].created_at.toISOString(),
|
|
@@ -46,6 +51,7 @@ async function listKnowledgeVersions(domain, key) {
|
|
|
46
51
|
const { rows } = await pool.query(sql_1.LIST_KNOWLEDGE_VERSIONS, [domain, key]);
|
|
47
52
|
return rows.map((r) => ({
|
|
48
53
|
version: r.version,
|
|
54
|
+
label: r.label ?? null,
|
|
49
55
|
change_summary: r.change_summary ?? null,
|
|
50
56
|
created_at: r.created_at.toISOString(),
|
|
51
57
|
is_current: r.is_current === true,
|
|
@@ -10,6 +10,8 @@ export interface ResolvedLookup {
|
|
|
10
10
|
version: number | null;
|
|
11
11
|
/** The ref follows the newest edition rather than pinning one. */
|
|
12
12
|
current?: true;
|
|
13
|
+
/** The ref pins an edition by this label. */
|
|
14
|
+
label?: string;
|
|
13
15
|
as?: string;
|
|
14
16
|
data: Record<string, unknown> | null;
|
|
15
17
|
missing?: boolean;
|
|
@@ -25,6 +27,12 @@ export declare function resolveRefEdition(ref: EscalationLookupRef, asOf?: strin
|
|
|
25
27
|
version: number;
|
|
26
28
|
snapshot: KnowledgeSnapshot;
|
|
27
29
|
} | null>;
|
|
30
|
+
/**
|
|
31
|
+
* The edition a label names, or, for a label that is a whole number no edition
|
|
32
|
+
* carries, that edition's number. Null when the label names nothing; that is
|
|
33
|
+
* never cached, so a label given later becomes visible.
|
|
34
|
+
*/
|
|
35
|
+
export declare function editionOfLabel(domain: string, key: string, label: string): Promise<number | null>;
|
|
28
36
|
/**
|
|
29
37
|
* Resolve a metadata `lookups` array into the form-context `lookup` domain:
|
|
30
38
|
* { [as ?? key]: data }. Returns null when there is nothing to resolve, so
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.getKnowledgeSnapshot = getKnowledgeSnapshot;
|
|
4
4
|
exports.resolveRefEdition = resolveRefEdition;
|
|
5
|
+
exports.editionOfLabel = editionOfLabel;
|
|
5
6
|
exports.resolveLookupContext = resolveLookupContext;
|
|
6
7
|
exports.resolveLookupRefs = resolveLookupRefs;
|
|
7
8
|
/**
|
|
@@ -18,6 +19,8 @@ const sql_1 = require("./sql");
|
|
|
18
19
|
const SNAPSHOT_CACHE_MAX_ENTRIES = 256;
|
|
19
20
|
// Map preserves insertion order — delete+set on read keeps it LRU.
|
|
20
21
|
const snapshotCache = new Map();
|
|
22
|
+
// A label names one edition for good, so the number it resolves to is kept too.
|
|
23
|
+
const labelCache = new Map();
|
|
21
24
|
function remember(cacheKey, snapshot) {
|
|
22
25
|
snapshotCache.set(cacheKey, snapshot);
|
|
23
26
|
while (snapshotCache.size > SNAPSHOT_CACHE_MAX_ENTRIES) {
|
|
@@ -52,7 +55,9 @@ async function getKnowledgeSnapshot(domain, key, version) {
|
|
|
52
55
|
*/
|
|
53
56
|
async function resolveRefEdition(ref, asOf) {
|
|
54
57
|
if (!(0, escalation_1.isCurrentLookupRef)(ref)) {
|
|
55
|
-
const version = ref.version;
|
|
58
|
+
const version = (0, escalation_1.isLabelLookupRef)(ref) ? await editionOfLabel(ref.domain, ref.key, ref.version) : ref.version;
|
|
59
|
+
if (version === null)
|
|
60
|
+
return null;
|
|
56
61
|
const snapshot = await getKnowledgeSnapshot(ref.domain, ref.key, version);
|
|
57
62
|
return snapshot ? { version, snapshot } : null;
|
|
58
63
|
}
|
|
@@ -71,6 +76,26 @@ async function resolveRefEdition(ref, asOf) {
|
|
|
71
76
|
remember(cacheKey, snapshot);
|
|
72
77
|
return { version, snapshot };
|
|
73
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* The edition a label names, or, for a label that is a whole number no edition
|
|
81
|
+
* carries, that edition's number. Null when the label names nothing; that is
|
|
82
|
+
* never cached, so a label given later becomes visible.
|
|
83
|
+
*/
|
|
84
|
+
async function editionOfLabel(domain, key, label) {
|
|
85
|
+
const cacheKey = `${domain} ${key} #${label}`;
|
|
86
|
+
const hit = labelCache.get(cacheKey);
|
|
87
|
+
if (hit !== undefined)
|
|
88
|
+
return hit;
|
|
89
|
+
const { rows } = await (0, db_1.getPool)().query(sql_1.GET_KNOWLEDGE_VERSION_BY_LABEL, [domain, key, label]);
|
|
90
|
+
if (rows[0]) {
|
|
91
|
+
const version = Number(rows[0].version);
|
|
92
|
+
labelCache.set(cacheKey, version);
|
|
93
|
+
while (labelCache.size > SNAPSHOT_CACHE_MAX_ENTRIES)
|
|
94
|
+
labelCache.delete(labelCache.keys().next().value);
|
|
95
|
+
return version;
|
|
96
|
+
}
|
|
97
|
+
return /^[1-9][0-9]*$/.test(label) ? Number(label) : null;
|
|
98
|
+
}
|
|
74
99
|
/** Structural check for one ref — malformed entries in stored metadata are skipped, not fatal. */
|
|
75
100
|
function isLookupRef(ref) {
|
|
76
101
|
if (!ref || typeof ref !== 'object')
|
|
@@ -78,7 +103,7 @@ function isLookupRef(ref) {
|
|
|
78
103
|
const r = ref;
|
|
79
104
|
return typeof r.domain === 'string' && r.domain.length > 0
|
|
80
105
|
&& typeof r.key === 'string' && r.key.length > 0
|
|
81
|
-
&& ((0, escalation_1.isCurrentLookupRef)(r) || (typeof r.version === 'number' && Number.isInteger(r.version) && r.version >= 1))
|
|
106
|
+
&& ((0, escalation_1.isCurrentLookupRef)(r) || (0, escalation_1.isLabelLookupRef)(r) || (typeof r.version === 'number' && Number.isInteger(r.version) && r.version >= 1))
|
|
82
107
|
&& (r.as === undefined || (typeof r.as === 'string' && r.as.length > 0));
|
|
83
108
|
}
|
|
84
109
|
/**
|
|
@@ -112,12 +137,14 @@ async function resolveLookupRefs(refs, asOf) {
|
|
|
112
137
|
if (!isLookupRef(ref))
|
|
113
138
|
continue;
|
|
114
139
|
const current = (0, escalation_1.isCurrentLookupRef)(ref);
|
|
140
|
+
const label = (0, escalation_1.isLabelLookupRef)(ref) ? ref.version : undefined;
|
|
115
141
|
const edition = await resolveRefEdition(ref, asOf);
|
|
116
142
|
resolved.push({
|
|
117
143
|
domain: ref.domain,
|
|
118
144
|
key: ref.key,
|
|
119
|
-
version: edition?.version ?? (current ? null : ref.version),
|
|
145
|
+
version: edition?.version ?? (current || label !== undefined ? null : ref.version),
|
|
120
146
|
...(current ? { current: true } : {}),
|
|
147
|
+
...(label !== undefined ? { label } : {}),
|
|
121
148
|
...(ref.as ? { as: ref.as } : {}),
|
|
122
149
|
data: edition ? edition.snapshot.data : null,
|
|
123
150
|
...(edition ? {} : { missing: true }),
|
|
@@ -21,7 +21,7 @@ async function describeMissingLookupRefs(refs) {
|
|
|
21
21
|
const where = versions.length
|
|
22
22
|
? `editions: ${versions.map((v) => `v${v}`).join(', ')}`
|
|
23
23
|
: `no knowledge entry ${ref.domain}/${ref.key}`;
|
|
24
|
-
const named = (0, escalation_1.isCurrentLookupRef)(ref) ? 'current' : `v${ref.version}`;
|
|
24
|
+
const named = (0, escalation_1.isCurrentLookupRef)(ref) ? 'current' : (0, escalation_1.isLabelLookupRef)(ref) ? `label ${JSON.stringify(ref.version)}` : `v${ref.version}`;
|
|
25
25
|
problems.push(`Lookup ref ${ref.domain}/${ref.key} ${named} names no edition (${where})`);
|
|
26
26
|
}
|
|
27
27
|
return problems;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
export declare const GET_KNOWLEDGE_VERSION = "\n SELECT domain, key, version, data, tags, change_summary, created_at\n FROM lt_knowledge_versions\n WHERE domain = $1 AND key = $2 AND version = $3";
|
|
2
|
-
|
|
1
|
+
export declare const GET_KNOWLEDGE_VERSION = "\n SELECT domain, key, version, label, data, tags, change_summary, created_at\n FROM lt_knowledge_versions\n WHERE domain = $1 AND key = $2 AND version = $3";
|
|
2
|
+
/** The edition a label names, when one does. */
|
|
3
|
+
export declare const GET_KNOWLEDGE_VERSION_BY_LABEL = "\n SELECT version\n FROM lt_knowledge_versions\n WHERE domain = $1 AND key = $2 AND label = $3";
|
|
4
|
+
export declare const LIST_KNOWLEDGE_VERSIONS = "\n SELECT v.version, v.label, v.change_summary, v.created_at,\n (v.version = k.current_version) AS is_current\n FROM lt_knowledge_versions v\n JOIN lt_knowledge k ON k.domain = v.domain AND k.key = v.key\n WHERE v.domain = $1 AND v.key = $2\n ORDER BY v.version DESC";
|
|
3
5
|
/** The newest edition, or the newest created at or before $3 when it is set. */
|
|
4
6
|
export declare const GET_LATEST_KNOWLEDGE_VERSION = "\n SELECT version, data, tags\n FROM lt_knowledge_versions\n WHERE domain = $1 AND key = $2\n AND ($3::timestamptz IS NULL OR created_at <= $3)\n ORDER BY version DESC\n LIMIT 1";
|
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.GET_LATEST_KNOWLEDGE_VERSION = exports.LIST_KNOWLEDGE_VERSIONS = exports.GET_KNOWLEDGE_VERSION = void 0;
|
|
3
|
+
exports.GET_LATEST_KNOWLEDGE_VERSION = exports.LIST_KNOWLEDGE_VERSIONS = exports.GET_KNOWLEDGE_VERSION_BY_LABEL = exports.GET_KNOWLEDGE_VERSION = void 0;
|
|
4
4
|
exports.GET_KNOWLEDGE_VERSION = `
|
|
5
|
-
SELECT domain, key, version, data, tags, change_summary, created_at
|
|
5
|
+
SELECT domain, key, version, label, data, tags, change_summary, created_at
|
|
6
6
|
FROM lt_knowledge_versions
|
|
7
7
|
WHERE domain = $1 AND key = $2 AND version = $3`;
|
|
8
|
+
/** The edition a label names, when one does. */
|
|
9
|
+
exports.GET_KNOWLEDGE_VERSION_BY_LABEL = `
|
|
10
|
+
SELECT version
|
|
11
|
+
FROM lt_knowledge_versions
|
|
12
|
+
WHERE domain = $1 AND key = $2 AND label = $3`;
|
|
8
13
|
exports.LIST_KNOWLEDGE_VERSIONS = `
|
|
9
|
-
SELECT v.version, v.change_summary, v.created_at,
|
|
14
|
+
SELECT v.version, v.label, v.change_summary, v.created_at,
|
|
10
15
|
(v.version = k.current_version) AS is_current
|
|
11
16
|
FROM lt_knowledge_versions v
|
|
12
17
|
JOIN lt_knowledge k ON k.domain = v.domain AND k.key = v.key
|
|
@@ -42,6 +42,8 @@ export interface MachineLineConfig {
|
|
|
42
42
|
progressStep: number;
|
|
43
43
|
/** Telemetry messages are published as wire events too; off by default, since they are read live and many. */
|
|
44
44
|
wireTelemetry: boolean;
|
|
45
|
+
/** How many of the line's last wire events the leader keeps for GET .../wire. */
|
|
46
|
+
wireWindow: number;
|
|
45
47
|
ltBaseUrl: string;
|
|
46
48
|
}
|
|
47
49
|
type Env = Record<string, string | undefined>;
|
|
@@ -75,6 +75,7 @@ function loadMachineLineConfig(env = process.env) {
|
|
|
75
75
|
lifecycle: oneOf('MACHINE_LIFECYCLE', env.MACHINE_LIFECYCLE || SWITCH.OFF, Object.values(SWITCH)) === SWITCH.ON,
|
|
76
76
|
progressStep: percentStep(env, 'MACHINE_PROGRESS_STEP', 25),
|
|
77
77
|
wireTelemetry: oneOf('MACHINE_WIRE_TELEMETRY', env.MACHINE_WIRE_TELEMETRY || SWITCH.OFF, Object.values(SWITCH)) === SWITCH.ON,
|
|
78
|
+
wireWindow: Math.max(1, int(env, 'MACHINE_WIRE_WINDOW', 500)),
|
|
78
79
|
ltBaseUrl: env.LT_BASE_URL ?? 'http://localhost:3000',
|
|
79
80
|
};
|
|
80
81
|
if (transport === exports.MACHINE_TRANSPORT.NATS) {
|
|
@@ -16,12 +16,16 @@ export declare const ROW_TYPE: {
|
|
|
16
16
|
readonly CONTRACT_VIOLATION: "contract-violation";
|
|
17
17
|
/** An address claims a station the line already knows by another address, or the reverse: a person approves or rejects. */
|
|
18
18
|
readonly MACHINE_REGISTRATION: "machine-registration";
|
|
19
|
+
/** Units a machine finished on hold wait for a quality decision: release, rework or scrap. */
|
|
20
|
+
readonly QUALITY_HOLD: "quality-hold";
|
|
19
21
|
};
|
|
20
22
|
export declare const LINE_ROLE: {
|
|
21
23
|
/** People who clear alarms, bring machines back and decide failed handoffs. */
|
|
22
24
|
readonly OPERATOR: "line-operator";
|
|
23
25
|
/** People who take finished units off the line: the final handoff. */
|
|
24
26
|
readonly EXIT: "line-exit";
|
|
27
|
+
/** People who decide units a machine finished on hold. */
|
|
28
|
+
readonly QUALITY: "line-quality";
|
|
25
29
|
/** The gateway's own identity. */
|
|
26
30
|
readonly CONTROLLER: "line-controller";
|
|
27
31
|
};
|
|
@@ -116,6 +120,8 @@ export declare const HANDOFF_ERROR: {
|
|
|
116
120
|
readonly SOFTWARE_UNSUPPORTED: "software-unsupported";
|
|
117
121
|
readonly WITHDRAWN: "withdrawn";
|
|
118
122
|
readonly CANCELLED: "cancelled";
|
|
123
|
+
/** An arrival no open handoff into the machine expects, shown to the operator role once per machine and carrier. */
|
|
124
|
+
readonly UNMATCHED_ARRIVAL: "unmatched-arrival";
|
|
119
125
|
};
|
|
120
126
|
export type HandoffErrorCode = typeof HANDOFF_ERROR[keyof typeof HANDOFF_ERROR];
|
|
121
127
|
/** A material lot a machine consumed for a unit. */
|
|
@@ -172,6 +178,10 @@ export declare const DECISION: {
|
|
|
172
178
|
readonly RESTORED: "restored";
|
|
173
179
|
readonly APPROVE: "approve";
|
|
174
180
|
readonly REJECT: "reject";
|
|
181
|
+
/** A quality decision on held units: they continue from where they are. */
|
|
182
|
+
readonly RELEASE: "release";
|
|
183
|
+
/** A quality decision on held units: they run again from a station on the route. */
|
|
184
|
+
readonly REWORK: "rework";
|
|
175
185
|
};
|
|
176
186
|
export type Decision = typeof DECISION[keyof typeof DECISION];
|
|
177
187
|
/** The decisions an operator may take on a failed handoff; retry only while every unit is still at the sender. */
|
|
@@ -180,11 +190,19 @@ export interface OperatorResolution {
|
|
|
180
190
|
decision: Decision;
|
|
181
191
|
note?: string;
|
|
182
192
|
}
|
|
193
|
+
/** The decisions a quality role may take on units a machine finished on hold. */
|
|
194
|
+
export declare const HOLD_DECISIONS: readonly Decision[];
|
|
195
|
+
export interface QualityResolution extends OperatorResolution {
|
|
196
|
+
/** For `rework`: the station the units run again from; the holding station when absent. */
|
|
197
|
+
station?: string;
|
|
198
|
+
}
|
|
183
199
|
/** Signal keys for the unit workflow; every attempt mints a fresh key. */
|
|
184
200
|
export declare const signalKeys: {
|
|
185
201
|
handoff: (jobKey: string, step: number, attempt: number) => string;
|
|
186
202
|
handoffError: (jobKey: string, step: number, attempt: number, round?: number) => string;
|
|
187
203
|
jobStop: (jobKey: string, step: number, attempt: number) => string;
|
|
204
|
+
hold: (jobKey: string, step: number, attempt: number, round?: number) => string;
|
|
205
|
+
disposition: (jobKey: string, step: number, attempt: number) => string;
|
|
188
206
|
};
|
|
189
207
|
/** Metadata on a machine-command row. */
|
|
190
208
|
export interface CommandFacets {
|