@genesislcap/mock-server 15.48.0 → 15.50.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.
@@ -1,6 +1,8 @@
1
1
  import { filterByCriteria } from "../db/criteria.js";
2
2
  import { NackError } from "../protocol/errors.js";
3
+ import { isLegacyFidelity } from "../protocol/fidelity.js";
3
4
  import { ERROR_CODE } from "../protocol/messageTypes.js";
5
+ import { resourceNotMappedNack } from "../protocol/msgNack.js";
4
6
  import { applyResourceAuth, requestingUser } from "./resourceAuth.js";
5
7
  // Keyed lookups: foundation-comms' createRequestMessage spreads RequestParams
6
8
  // onto the message, so REQUEST is a *top-level* field ({FIELD: value} pairs
@@ -20,15 +22,17 @@ export function handleRequestReply(message, connection, config, store) {
20
22
  const resourceName = message.MESSAGE_TYPE.replace(/^REQ_/, '');
21
23
  const requestReply = config.requestReplies?.[resourceName];
22
24
  if (!requestReply || (!requestReply.resolver && !requestReply.source)) {
23
- connection.reply(message, {
24
- MESSAGE_TYPE: `REP_${resourceName}`,
25
- ERROR: [
26
- {
27
- CODE: ERROR_CODE.UNKNOWN_RESOURCE,
28
- TEXT: `Unknown request-reply resource: ${resourceName}`,
29
- },
30
- ],
31
- });
25
+ connection.reply(message, isLegacyFidelity(config)
26
+ ? {
27
+ MESSAGE_TYPE: `REP_${resourceName}`,
28
+ ERROR: [
29
+ {
30
+ CODE: ERROR_CODE.UNKNOWN_RESOURCE,
31
+ TEXT: `Unknown request-reply resource: ${resourceName}`,
32
+ },
33
+ ],
34
+ }
35
+ : resourceNotMappedNack(resourceName));
32
36
  return;
33
37
  }
34
38
  const user = requestingUser(connection);
package/dist/index.d.ts CHANGED
@@ -3,4 +3,4 @@ export { launchStack, type LaunchStackOptions, type LaunchedStack } from './laun
3
3
  export { Store } from './db/store.ts';
4
4
  export { NackError, type NackErrorInput, type NackErrorEntry } from './protocol/errors.ts';
5
5
  export { compileCriteria, filterByCriteria, type Predicate } from './db/criteria.ts';
6
- export type { AntiJoinDef, AuthConfig, AuthUser, BroadcastFn, BroadcastTableChangeFn, EventHandler, EventHandlerCtx, EventHandlerResult, EventSchemaOverride, FieldMetadata, GenesisMessage, HttpRouteDef, JoinDef, MockServerConfig, OnMessageCtx, OnMessageHook, QueryDef, RequestingUser, RequestReplyDef, RequestReplyResolverCtx, ResourceAuthDef, ResourceItem, Row, SocketLike, SsoIdentityProvider, TableDef, ViewDef, } from './types.ts';
6
+ export type { AntiJoinDef, AuthConfig, AuthUser, BroadcastFn, BroadcastTableChangeFn, EventHandler, EventHandlerCtx, EventHandlerResult, EventSchemaOverride, FidelityMode, FieldMetadata, GenesisMessage, HttpRouteDef, JoinDef, MockServerConfig, OnMessageCtx, OnMessageHook, QueryDef, RequestingUser, RequestReplyDef, RequestReplyResolverCtx, ResourceAuthDef, ResourceItem, Row, SocketLike, SsoIdentityProvider, TableDef, ViewDef, } from './types.ts';
@@ -5,6 +5,10 @@ export interface Subscription {
5
5
  getRowRef: (row: Row) => unknown;
6
6
  predicate: Predicate;
7
7
  sequenceId: number;
8
+ pending?: Map<string, Row>;
9
+ held?: Set<string>;
10
+ maxRows?: number;
11
+ maxView?: number;
8
12
  }
9
13
  export declare class Connection {
10
14
  readonly ws: SocketLike;
@@ -0,0 +1,2 @@
1
+ import type { MockServerConfig } from '../types.ts';
2
+ export declare function isLegacyFidelity(config: MockServerConfig): boolean;
@@ -0,0 +1,6 @@
1
+ // True when the config opts back into the envelopes this engine sent up to
2
+ // 15.47 (see FidelityMode in types.ts). Read per message, like every other
3
+ // config field, so flipping it on a running server takes effect immediately.
4
+ export function isLegacyFidelity(config) {
5
+ return config.fidelity === 'legacy';
6
+ }
@@ -10,10 +10,17 @@ export declare const MESSAGE_TYPE: {
10
10
  readonly HEARTBEAT_PING: "HEARTBEAT_PING";
11
11
  readonly HEARTBEAT_PONG: "HEARTBEAT_PONG";
12
12
  readonly DATA_LOGON: "DATA_LOGON";
13
+ readonly LOGON_ACK: "LOGON_ACK";
13
14
  readonly DATA_LOGOFF: "DATA_LOGOFF";
15
+ readonly LOGOFF_ACK: "LOGOFF_ACK";
14
16
  readonly QUERY_UPDATE: "QUERY_UPDATE";
17
+ readonly MORE_ROWS: "MORE_ROWS";
18
+ readonly MORE_ROWS_ACK: "MORE_ROWS_ACK";
15
19
  readonly META_REQUEST: "META_REQUEST";
20
+ readonly FEATURE_ACK: "FEATURE_ACK";
16
21
  readonly JSON_SCHEMA_REQUEST: "JSON_SCHEMA_REQUEST";
22
+ readonly JSON_SCHEMA_FEATURE_ACK: "JSON_SCHEMA_FEATURE_ACK";
23
+ readonly MSG_NACK: "MSG_NACK";
17
24
  readonly RESOURCES_REQUEST: "RESOURCES_REQUEST";
18
25
  readonly RESOURCES_REQUEST_ACK: "RESOURCES_REQUEST_ACK";
19
26
  readonly RESOURCES_REQUEST_NACK: "RESOURCES_REQUEST_NACK";
@@ -17,10 +17,19 @@ export const MESSAGE_TYPE = {
17
17
  HEARTBEAT_PING: 'HEARTBEAT_PING',
18
18
  HEARTBEAT_PONG: 'HEARTBEAT_PONG',
19
19
  DATA_LOGON: 'DATA_LOGON',
20
+ LOGON_ACK: 'LOGON_ACK',
20
21
  DATA_LOGOFF: 'DATA_LOGOFF',
22
+ LOGOFF_ACK: 'LOGOFF_ACK',
21
23
  QUERY_UPDATE: 'QUERY_UPDATE',
24
+ MORE_ROWS: 'MORE_ROWS',
25
+ MORE_ROWS_ACK: 'MORE_ROWS_ACK',
22
26
  META_REQUEST: 'META_REQUEST',
27
+ FEATURE_ACK: 'FEATURE_ACK',
23
28
  JSON_SCHEMA_REQUEST: 'JSON_SCHEMA_REQUEST',
29
+ JSON_SCHEMA_FEATURE_ACK: 'JSON_SCHEMA_FEATURE_ACK',
30
+ // The router's generic rejection (unknown resource, missing or duplicate
31
+ // dataserver subscription) — see protocol/msgNack.ts.
32
+ MSG_NACK: 'MSG_NACK',
24
33
  RESOURCES_REQUEST: 'RESOURCES_REQUEST',
25
34
  RESOURCES_REQUEST_ACK: 'RESOURCES_REQUEST_ACK',
26
35
  RESOURCES_REQUEST_NACK: 'RESOURCES_REQUEST_NACK',
@@ -0,0 +1,4 @@
1
+ import type { Row } from '../types.ts';
2
+ export declare function resourceNotMappedNack(resourceName: string | undefined): Row;
3
+ export declare function nonExistentSubscriptionNack(): Row;
4
+ export declare function subscriptionAlreadyExistsNack(): Row;
@@ -0,0 +1,36 @@
1
+ import { MESSAGE_TYPE } from "./messageTypes.js";
2
+ const HTTP_NOT_FOUND = 404;
3
+ const HTTP_CONFLICT = 409;
4
+ // The real router's rejection frame (genesis-router MsgType.Outbound.Error,
5
+ // GSF 8.15.x): an integer HTTP status CODE, and the status line as
6
+ // STATUS_CODE, repeated under DETAILS. It is a different family from the
7
+ // handler-level EVENT_NACK/REP_ errors (string CODEs such as
8
+ // VALIDATION_ERROR) — the router sends it before a request ever reaches a
9
+ // service. foundation-comms' socket REJECTS the pending promise on MSG_NACK
10
+ // (connect/socket.ts), where a typed reply carrying ERROR resolves it.
11
+ function routerNack(code, reason, text) {
12
+ const STATUS_CODE = `${code} ${reason}`;
13
+ return {
14
+ MESSAGE_TYPE: MESSAGE_TYPE.MSG_NACK,
15
+ ERROR: [{ CODE: code, TEXT: text, STATUS_CODE }],
16
+ DETAILS: { ERROR: text, STATUS_CODE },
17
+ };
18
+ }
19
+ // The router resolves every message to a resource name first and answers this
20
+ // when no service serves it: DETAILS.DATASOURCE_NAME for DATA_LOGON, the name
21
+ // after REQ_ for request servers, DETAILS.FEATURE for META_REQUEST /
22
+ // JSON_SCHEMA_REQUEST, and the MESSAGE_TYPE itself for commit events. A
23
+ // missing name is the empty string, as on the real router.
24
+ export function resourceNotMappedNack(resourceName) {
25
+ return routerNack(HTTP_NOT_FOUND, 'Not Found', `Resource ${resourceName ?? ''} is not mapped to a service`);
26
+ }
27
+ // MORE_ROWS or DATA_LOGOFF naming a SOURCE_REF this connection has no live
28
+ // subscription on (never logged on, already logged off, or another socket's).
29
+ export function nonExistentSubscriptionNack() {
30
+ return routerNack(HTTP_NOT_FOUND, 'Not Found', 'Dataserver subscription does not exist');
31
+ }
32
+ // DATA_LOGON on a SOURCE_REF that already has a live subscription on this
33
+ // connection. The existing subscription is left untouched.
34
+ export function subscriptionAlreadyExistsNack() {
35
+ return routerNack(HTTP_CONFLICT, 'Conflict', 'Subscription exists for source ref. If using HTTP, try HTTP GET instead of HTTP POST');
36
+ }
@@ -1,11 +1,12 @@
1
1
  import { handleLogin, handleLogout } from "../handlers/auth.js";
2
2
  import { handleCommitEvent } from "../handlers/commitEvent.js";
3
- import { handleDataLogon, handleDataLogoff } from "../handlers/dataLogon.js";
3
+ import { handleDataLogon, handleDataLogoff, handleMoreRows } from "../handlers/dataLogon.js";
4
4
  import { handleHeartbeat } from "../handlers/heartbeat.js";
5
5
  import { handleJsonSchemaRequest } from "../handlers/jsonSchema.js";
6
6
  import { handleMetaRequest } from "../handlers/meta.js";
7
7
  import { handleRequestReply } from "../handlers/requestReply.js";
8
8
  import { handleResourcesRequest } from "../handlers/resources.js";
9
+ import { isLegacyFidelity } from "./fidelity.js";
9
10
  import { MESSAGE_TYPE, ERROR_CODE, isCommitEvent, isRequestReply } from "./messageTypes.js";
10
11
  // Message types a connection may send before authenticating when the OPT-IN
11
12
  // auth gate (config.auth.requireAuth = true) is enabled. Everything else
@@ -13,7 +14,11 @@ import { MESSAGE_TYPE, ERROR_CODE, isCommitEvent, isRequestReply } from "./messa
13
14
  // requires the connection to have logged in over WS — or to present a
14
15
  // SESSION_AUTH_TOKEN this server issued (over either transport).
15
16
  // RESOURCES_REQUEST is allowed pre-auth so resource discovery can't deadlock
16
- // a client that fetches it before (or while) logging in.
17
+ // a client that fetches it before (or while) logging in. MORE_ROWS, like
18
+ // DATA_LOGOFF, can only act on a subscription the connection already opened
19
+ // through the gate (logout clears them), so it answers with its own precise
20
+ // 404 rather than a NOT_LOGGED_ON echo — except under fidelity: 'legacy',
21
+ // which keeps the 15.47 gate (see isPreAuth).
17
22
  //
18
23
  // The gate is OFF by default because real clients can't be assumed to
19
24
  // satisfy it: browser frame captures show that after logging in over
@@ -29,6 +34,11 @@ const PRE_AUTH_MESSAGE_TYPES = new Set([
29
34
  MESSAGE_TYPE.DATA_LOGOFF,
30
35
  MESSAGE_TYPE.RESOURCES_REQUEST,
31
36
  ]);
37
+ function isPreAuth(messageType, config) {
38
+ if (messageType === MESSAGE_TYPE.MORE_ROWS)
39
+ return !isLegacyFidelity(config);
40
+ return PRE_AUTH_MESSAGE_TYPES.has(messageType);
41
+ }
32
42
  function ensureAuthenticated(message, connection, tokens) {
33
43
  if (connection.authenticated)
34
44
  return true;
@@ -69,9 +79,7 @@ export function routeMessage(message, connection, config, store, ctx, tokens = n
69
79
  // away; the gate itself only applies to data traffic.
70
80
  ensureAuthenticated(message, connection, tokens);
71
81
  const requireAuth = config.auth?.requireAuth ?? false;
72
- if (requireAuth &&
73
- !PRE_AUTH_MESSAGE_TYPES.has(message.MESSAGE_TYPE) &&
74
- !connection.authenticated) {
82
+ if (requireAuth && !isPreAuth(message.MESSAGE_TYPE, config) && !connection.authenticated) {
75
83
  return replyNotLoggedOn(message, connection);
76
84
  }
77
85
  switch (message.MESSAGE_TYPE) {
@@ -84,7 +92,9 @@ export function routeMessage(message, connection, config, store, ctx, tokens = n
84
92
  case MESSAGE_TYPE.DATA_LOGON:
85
93
  return handleDataLogon(message, connection, config, store);
86
94
  case MESSAGE_TYPE.DATA_LOGOFF:
87
- return handleDataLogoff(message, connection);
95
+ return handleDataLogoff(message, connection, config);
96
+ case MESSAGE_TYPE.MORE_ROWS:
97
+ return handleMoreRows(message, connection, config);
88
98
  case MESSAGE_TYPE.META_REQUEST:
89
99
  return handleMetaRequest(message, connection, config, store);
90
100
  case MESSAGE_TYPE.JSON_SCHEMA_REQUEST:
package/dist/server.js CHANGED
@@ -1,9 +1,11 @@
1
1
  import http from 'node:http';
2
2
  import { WebSocketServer } from 'ws';
3
3
  import { Store } from "./db/store.js";
4
+ import { pendingKey } from "./handlers/dataLogon.js";
4
5
  import { createHttpRequestListener } from "./handlers/http.js";
5
6
  import { applyResourceAuth, requestingUser } from "./handlers/resourceAuth.js";
6
7
  import { Connection } from "./protocol/connection.js";
8
+ import { isLegacyFidelity } from "./protocol/fidelity.js";
7
9
  import { routeMessage } from "./protocol/router.js";
8
10
  import { stampRow } from "./protocol/rowUpdate.js";
9
11
  export function createMockServer(config) {
@@ -21,8 +23,9 @@ export function createMockServer(config) {
21
23
  // grids reflect inserts/amends/deletes without the client re-querying.
22
24
  // DELETE rows are PK-only stubs (see broadcastTableChange), so a criteria
23
25
  // predicate would evaluate against missing fields and drop the push —
24
- // leaving the client holding a ghost row. Deliver every DELETE instead: a
25
- // client that never held that ROW_REF ignores it.
26
+ // leaving the client holding a ghost row. A DELETE therefore skips the
27
+ // predicate; which subscriptions it reaches is decided by what each client
28
+ // holds (trackClientView), or, in legacy mode, it reaches all of them.
26
29
  // 'error' is kept distinct from 'no-match' because only a clean no-match may
27
30
  // be turned into a DELETE below; a broken predicate must not evict rows.
28
31
  function evaluateSubscription(subscription, sourceRef, row) {
@@ -49,6 +52,53 @@ export function createMockServer(config) {
49
52
  return null;
50
53
  return operation === 'MODIFY' ? 'DELETE' : null;
51
54
  }
55
+ // Decides what one 'gsf' subscription's client gets for a live change, from
56
+ // what that client holds — as the real dataserver does with its client
57
+ // index — and keeps the subscription's pending/held sets up to date.
58
+ // `operation` is the change after the query filter and CRITERIA_MATCH
59
+ // (a MODIFY that left them arrives as a DELETE); the user's rowFilter is
60
+ // applied here too, so leaving it counts as leaving the subscription.
61
+ // - A row still waiting for MORE_ROWS (past the page, or past MAX_VIEW) is
62
+ // never pushed: the client has never seen it. A change replaces the
63
+ // waiting copy in place, so the page later delivers current values; a
64
+ // DELETE drops it.
65
+ // - A held row gets the MODIFY, or a DELETE when it is gone or out of scope.
66
+ // - A row the client doesn't hold that is (now) in scope is pushed as an
67
+ // INSERT and becomes held: a brand-new row, or one a MODIFY brought back
68
+ // into the criteria. Out of scope, the client is told nothing.
69
+ // An INSERT is pushed even when the view already holds MAX_VIEW rows; the
70
+ // real server would also evict the oldest row in the same frame (moving
71
+ // view), which this engine doesn't, so the view can grow past MAX_VIEW.
72
+ // A row without a ROW_REF can't be matched to anything the client holds,
73
+ // so changes to it aren't pushed.
74
+ // Returns the operation to push, or null to push nothing.
75
+ function trackClientView(subscription, connection, query, operation, row, rowRef) {
76
+ const key = pendingKey(rowRef);
77
+ const pending = subscription.pending;
78
+ const held = subscription.held;
79
+ if (key === undefined || !pending || !held)
80
+ return null;
81
+ const permitted = operation === 'DELETE'
82
+ ? undefined
83
+ : applyResourceAuth([row], query?.auth, requestingUser(connection))[0];
84
+ if (pending.has(key)) {
85
+ if (permitted)
86
+ pending.set(key, stampRow(permitted, rowRef, 'INSERT'));
87
+ else
88
+ pending.delete(key);
89
+ return null;
90
+ }
91
+ if (held.has(key)) {
92
+ if (permitted)
93
+ return 'MODIFY';
94
+ held.delete(key);
95
+ return 'DELETE';
96
+ }
97
+ if (!permitted)
98
+ return null;
99
+ held.add(key);
100
+ return 'INSERT';
101
+ }
52
102
  const broadcast = (resourceName, operation, row) => {
53
103
  const query = config.queries?.[resourceName];
54
104
  // The query's own filter (where-clause equivalent) applies to live pushes
@@ -57,7 +107,6 @@ export function createMockServer(config) {
57
107
  // rather than dropped: a swallowed removal leaves the client rendering the
58
108
  // row with its stale pre-modify values until a full reload. An INSERT
59
109
  // outside the filter is simply dropped (the client never held it).
60
- // DELETEs always go through: dropping an unknown ROW_REF is a no-op.
61
110
  let effectiveOperation = operation;
62
111
  if (operation !== 'DELETE' && query?.filter && !query.filter(row)) {
63
112
  if (operation !== 'MODIFY')
@@ -68,32 +117,42 @@ export function createMockServer(config) {
68
117
  for (const [sourceRef, subscription] of connection.subscriptions) {
69
118
  if (subscription.resourceName !== resourceName)
70
119
  continue;
71
- const rowOperation = resolveSubscriptionOperation(subscription, sourceRef, effectiveOperation, row);
72
- if (rowOperation === null)
120
+ const resolved = resolveSubscriptionOperation(subscription, sourceRef, effectiveOperation, row);
121
+ if (resolved === null)
73
122
  continue;
74
123
  // Per-user permissioning (auth maps / hideFields equivalent): the
75
124
  // ROW_REF is computed from the unstripped row, since pk fields may
76
125
  // themselves be hidden from this user.
77
126
  const rowRef = subscription.getRowRef(row);
127
+ const rowOperation = subscription.held
128
+ ? trackClientView(subscription, connection, query, resolved, row, rowRef)
129
+ : resolved;
130
+ if (rowOperation === null)
131
+ continue;
78
132
  let outRow = row;
79
- if (rowOperation !== operation) {
80
- // Converted to a DELETE: send the row identity only — it is all the
81
- // client needs to drop the row, and the full row may carry columns
82
- // this user's hideFields strips.
83
- outRow = query ? store.rowStubFromKey(query.source, rowRef) : row;
133
+ if (rowOperation === 'DELETE') {
134
+ // A change turned into a DELETE: send the row identity only — it is
135
+ // all the client needs to drop the row, and the full row may carry
136
+ // columns this user's hideFields strips. (A real DELETE's row is a
137
+ // stub already.)
138
+ if (operation !== 'DELETE') {
139
+ outRow = query ? store.rowStubFromKey(query.source, rowRef) : row;
140
+ }
84
141
  }
85
- else if (rowOperation !== 'DELETE' && query?.auth) {
142
+ else if (query?.auth) {
86
143
  const [permitted] = applyResourceAuth([row], query.auth, requestingUser(connection));
87
144
  if (!permitted)
88
145
  continue;
89
146
  outRow = permitted;
90
147
  }
91
148
  subscription.sequenceId += 1;
149
+ // A live push carries neither MORE_ROWS nor ROWS_COUNT on the real
150
+ // server — those describe the snapshot. Legacy mode sent ROWS_COUNT: 1.
92
151
  connection.send({
93
152
  MESSAGE_TYPE: 'QUERY_UPDATE',
94
153
  SOURCE_REF: sourceRef,
95
154
  ROW: [stampRow(outRow, rowRef, rowOperation)],
96
- ROWS_COUNT: 1,
155
+ ...(isLegacyFidelity(config) ? { ROWS_COUNT: 1 } : {}),
97
156
  SEQUENCE_ID: subscription.sequenceId,
98
157
  });
99
158
  }
package/dist/types.d.ts CHANGED
@@ -118,7 +118,9 @@ export interface OnMessageCtx {
118
118
  broadcastTableChange: BroadcastTableChangeFn;
119
119
  }
120
120
  export type OnMessageHook = (message: GenesisMessage, ctx: OnMessageCtx) => GenesisMessage | false | void;
121
+ export type FidelityMode = 'gsf' | 'legacy';
121
122
  export interface MockServerConfig {
123
+ fidelity?: FidelityMode;
122
124
  tables?: Record<string, TableDef>;
123
125
  views?: Record<string, ViewDef>;
124
126
  queries?: Record<string, QueryDef>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@genesislcap/mock-server",
3
- "version": "15.48.0",
3
+ "version": "15.50.0",
4
4
  "description": "Reusable in-memory Genesis WebSocket protocol mock server for client automation/CI, without a JVM or database.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -22,7 +22,9 @@
22
22
  "build:watch": "tsc -p tsconfig.build.json --watch",
23
23
  "lint": "echo \"@genesislcap/mock-server: no lint step\"",
24
24
  "typecheck": "tsc --noEmit",
25
- "test": "node --experimental-strip-types --test"
25
+ "test": "node --experimental-strip-types --test \"test/**/*.test.ts\"",
26
+ "test:conformance": "node --experimental-strip-types --test \"test/conformance/**/*.test.ts\"",
27
+ "conformance:record": "node --experimental-strip-types test/conformance/record.ts"
26
28
  },
27
29
  "dependencies": {
28
30
  "ws": "^8.18.0"
@@ -1,7 +1,9 @@
1
1
  import type { Store } from '../db/store.ts';
2
2
  import type { Connection } from '../protocol/connection.ts';
3
3
  import { NackError } from '../protocol/errors.ts';
4
+ import { isLegacyFidelity } from '../protocol/fidelity.ts';
4
5
  import { ERROR_CODE, MESSAGE_TYPE } from '../protocol/messageTypes.ts';
6
+ import { resourceNotMappedNack } from '../protocol/msgNack.ts';
5
7
  import type {
6
8
  BroadcastFn,
7
9
  BroadcastTableChangeFn,
@@ -36,13 +38,23 @@ export function handleCommitEvent(
36
38
  // real consumers (foundation-comms' kv.service, entity-management) match
37
39
  // these names exactly, so a name-suffixed <EVENT>_ACK reads as failure and
38
40
  // a <EVENT>_NACK passes as silent success.
41
+ // An event nothing handles never reaches an event handler on the real
42
+ // server: the router rejects it by name with MSG_NACK 404.
39
43
  if (!handler) {
40
- connection.reply(message, {
41
- MESSAGE_TYPE: MESSAGE_TYPE.EVENT_NACK,
42
- ERROR: [
43
- { CODE: ERROR_CODE.UNKNOWN_RESOURCE, TEXT: `No event handler registered for ${eventName}` },
44
- ],
45
- });
44
+ connection.reply(
45
+ message,
46
+ isLegacyFidelity(config)
47
+ ? {
48
+ MESSAGE_TYPE: MESSAGE_TYPE.EVENT_NACK,
49
+ ERROR: [
50
+ {
51
+ CODE: ERROR_CODE.UNKNOWN_RESOURCE,
52
+ TEXT: `No event handler registered for ${eventName}`,
53
+ },
54
+ ],
55
+ }
56
+ : resourceNotMappedNack(eventName),
57
+ );
46
58
  return;
47
59
  }
48
60