@genesislcap/mock-server 15.49.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.
- package/README.md +198 -36
- package/dist/handlers/commitEvent.js +15 -6
- package/dist/handlers/dataLogon.d.ts +8 -2
- package/dist/handlers/dataLogon.js +168 -17
- package/dist/handlers/jsonSchema.js +14 -7
- package/dist/handlers/meta.js +15 -6
- package/dist/handlers/requestReply.js +13 -9
- package/dist/index.d.ts +1 -1
- package/dist/protocol/connection.d.ts +4 -0
- package/dist/protocol/fidelity.d.ts +2 -0
- package/dist/protocol/fidelity.js +6 -0
- package/dist/protocol/messageTypes.d.ts +7 -0
- package/dist/protocol/messageTypes.js +9 -0
- package/dist/protocol/msgNack.d.ts +4 -0
- package/dist/protocol/msgNack.js +36 -0
- package/dist/protocol/router.js +16 -6
- package/dist/server.js +71 -12
- package/dist/types.d.ts +2 -0
- package/package.json +4 -2
- package/src/handlers/commitEvent.ts +18 -6
- package/src/handlers/dataLogon.ts +204 -18
- package/src/handlers/jsonSchema.ts +17 -7
- package/src/handlers/meta.ts +18 -6
- package/src/handlers/requestReply.ts +16 -9
- package/src/index.ts +1 -0
- package/src/protocol/connection.ts +16 -0
- package/src/protocol/fidelity.ts +8 -0
- package/src/protocol/messageTypes.ts +9 -0
- package/src/protocol/msgNack.ts +50 -0
- package/src/protocol/router.ts +16 -8
- package/src/server.ts +74 -12
- package/src/types.ts +15 -1
package/src/server.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { WebSocketServer, type WebSocket } from 'ws';
|
|
3
3
|
import { Store } from './db/store.ts';
|
|
4
|
+
import { pendingKey } from './handlers/dataLogon.ts';
|
|
4
5
|
import { createHttpRequestListener } from './handlers/http.ts';
|
|
5
6
|
import { applyResourceAuth, requestingUser } from './handlers/resourceAuth.ts';
|
|
6
7
|
import { Connection, type Subscription } from './protocol/connection.ts';
|
|
8
|
+
import { isLegacyFidelity } from './protocol/fidelity.ts';
|
|
7
9
|
import { routeMessage } from './protocol/router.ts';
|
|
8
10
|
import { stampRow, type RowOperation } from './protocol/rowUpdate.ts';
|
|
9
11
|
import type {
|
|
@@ -65,8 +67,9 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
65
67
|
// grids reflect inserts/amends/deletes without the client re-querying.
|
|
66
68
|
// DELETE rows are PK-only stubs (see broadcastTableChange), so a criteria
|
|
67
69
|
// predicate would evaluate against missing fields and drop the push —
|
|
68
|
-
// leaving the client holding a ghost row.
|
|
69
|
-
//
|
|
70
|
+
// leaving the client holding a ghost row. A DELETE therefore skips the
|
|
71
|
+
// predicate; which subscriptions it reaches is decided by what each client
|
|
72
|
+
// holds (trackClientView), or, in legacy mode, it reaches all of them.
|
|
70
73
|
// 'error' is kept distinct from 'no-match' because only a clean no-match may
|
|
71
74
|
// be turned into a DELETE below; a broken predicate must not evict rows.
|
|
72
75
|
function evaluateSubscription(
|
|
@@ -103,6 +106,57 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
103
106
|
return operation === 'MODIFY' ? 'DELETE' : null;
|
|
104
107
|
}
|
|
105
108
|
|
|
109
|
+
// Decides what one 'gsf' subscription's client gets for a live change, from
|
|
110
|
+
// what that client holds — as the real dataserver does with its client
|
|
111
|
+
// index — and keeps the subscription's pending/held sets up to date.
|
|
112
|
+
// `operation` is the change after the query filter and CRITERIA_MATCH
|
|
113
|
+
// (a MODIFY that left them arrives as a DELETE); the user's rowFilter is
|
|
114
|
+
// applied here too, so leaving it counts as leaving the subscription.
|
|
115
|
+
// - A row still waiting for MORE_ROWS (past the page, or past MAX_VIEW) is
|
|
116
|
+
// never pushed: the client has never seen it. A change replaces the
|
|
117
|
+
// waiting copy in place, so the page later delivers current values; a
|
|
118
|
+
// DELETE drops it.
|
|
119
|
+
// - A held row gets the MODIFY, or a DELETE when it is gone or out of scope.
|
|
120
|
+
// - A row the client doesn't hold that is (now) in scope is pushed as an
|
|
121
|
+
// INSERT and becomes held: a brand-new row, or one a MODIFY brought back
|
|
122
|
+
// into the criteria. Out of scope, the client is told nothing.
|
|
123
|
+
// An INSERT is pushed even when the view already holds MAX_VIEW rows; the
|
|
124
|
+
// real server would also evict the oldest row in the same frame (moving
|
|
125
|
+
// view), which this engine doesn't, so the view can grow past MAX_VIEW.
|
|
126
|
+
// A row without a ROW_REF can't be matched to anything the client holds,
|
|
127
|
+
// so changes to it aren't pushed.
|
|
128
|
+
// Returns the operation to push, or null to push nothing.
|
|
129
|
+
function trackClientView(
|
|
130
|
+
subscription: Subscription,
|
|
131
|
+
connection: Connection,
|
|
132
|
+
query: QueryDef | undefined,
|
|
133
|
+
operation: RowOperation,
|
|
134
|
+
row: Row,
|
|
135
|
+
rowRef: unknown,
|
|
136
|
+
): RowOperation | null {
|
|
137
|
+
const key = pendingKey(rowRef);
|
|
138
|
+
const pending = subscription.pending;
|
|
139
|
+
const held = subscription.held;
|
|
140
|
+
if (key === undefined || !pending || !held) return null;
|
|
141
|
+
const permitted =
|
|
142
|
+
operation === 'DELETE'
|
|
143
|
+
? undefined
|
|
144
|
+
: applyResourceAuth([row], query?.auth, requestingUser(connection))[0];
|
|
145
|
+
if (pending.has(key)) {
|
|
146
|
+
if (permitted) pending.set(key, stampRow(permitted, rowRef, 'INSERT'));
|
|
147
|
+
else pending.delete(key);
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
if (held.has(key)) {
|
|
151
|
+
if (permitted) return 'MODIFY';
|
|
152
|
+
held.delete(key);
|
|
153
|
+
return 'DELETE';
|
|
154
|
+
}
|
|
155
|
+
if (!permitted) return null;
|
|
156
|
+
held.add(key);
|
|
157
|
+
return 'INSERT';
|
|
158
|
+
}
|
|
159
|
+
|
|
106
160
|
const broadcast: BroadcastFn = (resourceName, operation, row) => {
|
|
107
161
|
const query = config.queries?.[resourceName];
|
|
108
162
|
// The query's own filter (where-clause equivalent) applies to live pushes
|
|
@@ -111,7 +165,6 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
111
165
|
// rather than dropped: a swallowed removal leaves the client rendering the
|
|
112
166
|
// row with its stale pre-modify values until a full reload. An INSERT
|
|
113
167
|
// outside the filter is simply dropped (the client never held it).
|
|
114
|
-
// DELETEs always go through: dropping an unknown ROW_REF is a no-op.
|
|
115
168
|
let effectiveOperation = operation;
|
|
116
169
|
if (operation !== 'DELETE' && query?.filter && !query.filter(row)) {
|
|
117
170
|
if (operation !== 'MODIFY') return;
|
|
@@ -120,34 +173,43 @@ export function createMockServer(config: MockServerConfig): MockServer {
|
|
|
120
173
|
for (const connection of connections) {
|
|
121
174
|
for (const [sourceRef, subscription] of connection.subscriptions) {
|
|
122
175
|
if (subscription.resourceName !== resourceName) continue;
|
|
123
|
-
const
|
|
176
|
+
const resolved = resolveSubscriptionOperation(
|
|
124
177
|
subscription,
|
|
125
178
|
sourceRef,
|
|
126
179
|
effectiveOperation,
|
|
127
180
|
row,
|
|
128
181
|
);
|
|
129
|
-
if (
|
|
182
|
+
if (resolved === null) continue;
|
|
130
183
|
// Per-user permissioning (auth maps / hideFields equivalent): the
|
|
131
184
|
// ROW_REF is computed from the unstripped row, since pk fields may
|
|
132
185
|
// themselves be hidden from this user.
|
|
133
186
|
const rowRef = subscription.getRowRef(row);
|
|
187
|
+
const rowOperation = subscription.held
|
|
188
|
+
? trackClientView(subscription, connection, query, resolved, row, rowRef)
|
|
189
|
+
: resolved;
|
|
190
|
+
if (rowOperation === null) continue;
|
|
134
191
|
let outRow = row;
|
|
135
|
-
if (rowOperation
|
|
136
|
-
//
|
|
137
|
-
// client needs to drop the row, and the full row may carry
|
|
138
|
-
// this user's hideFields strips.
|
|
139
|
-
|
|
140
|
-
|
|
192
|
+
if (rowOperation === 'DELETE') {
|
|
193
|
+
// A change turned into a DELETE: send the row identity only — it is
|
|
194
|
+
// all the client needs to drop the row, and the full row may carry
|
|
195
|
+
// columns this user's hideFields strips. (A real DELETE's row is a
|
|
196
|
+
// stub already.)
|
|
197
|
+
if (operation !== 'DELETE') {
|
|
198
|
+
outRow = query ? store.rowStubFromKey(query.source, rowRef as string) : row;
|
|
199
|
+
}
|
|
200
|
+
} else if (query?.auth) {
|
|
141
201
|
const [permitted] = applyResourceAuth([row], query.auth, requestingUser(connection));
|
|
142
202
|
if (!permitted) continue;
|
|
143
203
|
outRow = permitted;
|
|
144
204
|
}
|
|
145
205
|
subscription.sequenceId += 1;
|
|
206
|
+
// A live push carries neither MORE_ROWS nor ROWS_COUNT on the real
|
|
207
|
+
// server — those describe the snapshot. Legacy mode sent ROWS_COUNT: 1.
|
|
146
208
|
connection.send({
|
|
147
209
|
MESSAGE_TYPE: 'QUERY_UPDATE',
|
|
148
210
|
SOURCE_REF: sourceRef,
|
|
149
211
|
ROW: [stampRow(outRow, rowRef, rowOperation)],
|
|
150
|
-
ROWS_COUNT: 1,
|
|
212
|
+
...(isLegacyFidelity(config) ? { ROWS_COUNT: 1 } : {}),
|
|
151
213
|
SEQUENCE_ID: subscription.sequenceId,
|
|
152
214
|
});
|
|
153
215
|
}
|
package/src/types.ts
CHANGED
|
@@ -214,7 +214,20 @@ export type OnMessageHook = (
|
|
|
214
214
|
ctx: OnMessageCtx,
|
|
215
215
|
) => GenesisMessage | false | void;
|
|
216
216
|
|
|
217
|
+
// How closely the wire envelopes follow the real server. 'gsf' (the default)
|
|
218
|
+
// matches GSF 8.15.x: LOGON_ACK before the snapshot, dataserver paging
|
|
219
|
+
// (MAX_ROWS / MAX_VIEW / MORE_ROWS), LOGOFF_ACK, FEATURE_ACK /
|
|
220
|
+
// JSON_SCHEMA_FEATURE_ACK, and the router's MSG_NACK for unknown resources and
|
|
221
|
+
// missing/duplicate subscriptions. 'legacy' restores the envelopes this engine
|
|
222
|
+
// sent up to 15.47 (every row in one snapshot, no LOGON_ACK/LOGOFF_ACK, typed
|
|
223
|
+
// UNKNOWN_RESOURCE replies that RESOLVE the client's pending promise where
|
|
224
|
+
// MSG_NACK rejects it) — an escape hatch for consumers built against those,
|
|
225
|
+
// to be removed in a later release.
|
|
226
|
+
export type FidelityMode = 'gsf' | 'legacy';
|
|
227
|
+
|
|
217
228
|
export interface MockServerConfig {
|
|
229
|
+
// Defaults to 'gsf'. See FidelityMode and the README's "Wire fidelity".
|
|
230
|
+
fidelity?: FidelityMode;
|
|
218
231
|
tables?: Record<string, TableDef>;
|
|
219
232
|
views?: Record<string, ViewDef>;
|
|
220
233
|
queries?: Record<string, QueryDef>;
|
|
@@ -223,7 +236,8 @@ export interface MockServerConfig {
|
|
|
223
236
|
// Fallback for commit events with no registered handler — lets a project
|
|
224
237
|
// with many events adopt incrementally (e.g. `() => ({})` acks everything
|
|
225
238
|
// unhandled) instead of enumerating ack-only handlers. Unregistered events
|
|
226
|
-
//
|
|
239
|
+
// get the router's MSG_NACK 404 when this is unset (EVENT_NACK with
|
|
240
|
+
// UNKNOWN_RESOURCE under fidelity: 'legacy').
|
|
227
241
|
defaultEventHandler?: EventHandler;
|
|
228
242
|
eventSchemas?: Record<string, EventSchemaOverride>;
|
|
229
243
|
auth?: AuthConfig;
|