@genesislcap/mock-server 15.57.0 → 15.58.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 +62 -8
- package/dist/db/store.d.ts +6 -0
- package/dist/db/store.js +62 -0
- package/dist/handlers/commitEvent.js +95 -17
- package/dist/protocol/errors.js +3 -1
- package/dist/types.d.ts +1 -0
- package/package.json +1 -1
- package/src/db/store.ts +73 -0
- package/src/handlers/commitEvent.ts +111 -17
- package/src/protocol/errors.ts +6 -3
- package/src/types.ts +20 -5
package/README.md
CHANGED
|
@@ -347,9 +347,9 @@ and a `"<code> <reason>"` `STATUS_CODE`, and the envelope carries `WARNING`:
|
|
|
347
347
|
`new NackError([...])` reports several errors at once. Anything else a handler
|
|
348
348
|
throws is `StandardError INTERNAL_ERROR` with its message. A handler can
|
|
349
349
|
return `{ warnings: [...] }` for a NACK with an empty `ERROR` and those
|
|
350
|
-
warnings,
|
|
351
|
-
|
|
352
|
-
[
|
|
350
|
+
warnings, which `IGNORE_WARNINGS: true` turns into a commit, or pass
|
|
351
|
+
`{ warnings }` to `NackError` to send them with its errors (see
|
|
352
|
+
[Commit semantics](#commit-semantics-validate-warnings-and-ignore_warnings)). A request server
|
|
353
353
|
that fails answers a service `MSG_NACK` carrying the same items:
|
|
354
354
|
`{WARNING, ERROR, MESSAGE_TYPE: 'MSG_NACK'}` (genesis-messages `MsgNack`). The
|
|
355
355
|
router's `MSG_NACK` keeps its own shape (no `@type`,
|
|
@@ -399,12 +399,14 @@ key fields names the primary key, else it is `<TABLE>_BY_<fields>` without the
|
|
|
399
399
|
the table and on views over it.
|
|
400
400
|
|
|
401
401
|
**`VALIDATE: true`** reaches every handler as `ctx.validate`, and nothing is
|
|
402
|
-
written. The generic events have no
|
|
403
|
-
|
|
402
|
+
written (except under `fidelity: 'legacy'`). The generic events have no
|
|
403
|
+
`onValidate`, so they ack `GENERATED: []` straight away (recorded for
|
|
404
|
+
`COUNTERPARTY_INSERT`): no duplicate check, no
|
|
404
405
|
sequence value used, no lookup for a modify or delete. A hand-written handler
|
|
405
406
|
should run its checks and return before writing when `ctx.validate` is set
|
|
406
|
-
(recorded: `TRADE_INSERT`'s `onValidate` NACK still comes back)
|
|
407
|
-
|
|
407
|
+
(recorded: `TRADE_INSERT`'s `onValidate` NACK still comes back); the engine
|
|
408
|
+
rolls back anything it writes anyway (see
|
|
409
|
+
[Commit semantics](#commit-semantics-validate-warnings-and-ignore_warnings)).
|
|
408
410
|
|
|
409
411
|
**An `eventHandlers` entry of the same name wins**, so a table can list all
|
|
410
412
|
three verbs and still write out the one with business rules.
|
|
@@ -446,6 +448,56 @@ field's errors in field order, then missing fields, then unknown ones.
|
|
|
446
448
|
on the recorded schemas. A modify therefore needs every required field, as on
|
|
447
449
|
the real server. `fidelity: 'legacy'` checks nothing.
|
|
448
450
|
|
|
451
|
+
## Commit semantics: `VALIDATE`, warnings and `IGNORE_WARNINGS`
|
|
452
|
+
|
|
453
|
+
A handler stands for a GSF event handler's `onValidate` and `onCommit` at
|
|
454
|
+
once. The engine gives its outcome the semantics of GSF's
|
|
455
|
+
`TransactionalEventHandlerContainer` (genesis-pal-eventhandler, v8.15.29; the
|
|
456
|
+
typed `SyncValidatingEventHandler` is the same): `VALIDATE` runs `onValidate`
|
|
457
|
+
only; otherwise `onCommit` runs when `onValidate` acks, or when it NACKs with
|
|
458
|
+
warnings only (`error.isEmpty()`) and the message has `IGNORE_WARNINGS: true`;
|
|
459
|
+
any other NACK is the reply. So each handler runs in a transaction, and the
|
|
460
|
+
event commits only when it acks:
|
|
461
|
+
|
|
462
|
+
| The handler… | Without `IGNORE_WARNINGS` | With `IGNORE_WARNINGS: true` | Under `VALIDATE: true` |
|
|
463
|
+
|---|---|---|---|
|
|
464
|
+
| Returns `{ generated? }` | `EVENT_ACK`, committed | The same | `EVENT_ACK {GENERATED: []}`, rolled back |
|
|
465
|
+
| Returns `{ warnings, generated? }` | `EVENT_NACK {ERROR: [], WARNING}`, rolled back | `EVENT_ACK {GENERATED}` (no `WARNING`), committed | `EVENT_NACK {ERROR: [], WARNING}`, rolled back |
|
|
466
|
+
| Throws `NackError` (errors, warnings or both), or anything else | `EVENT_NACK`, rolled back | The same | The same |
|
|
467
|
+
|
|
468
|
+
*Rolled back* means every row the handler inserted, changed or deleted is
|
|
469
|
+
put back, and the live pushes it asked for through `ctx.broadcast` /
|
|
470
|
+
`ctx.broadcastTableChange` are dropped. They go out only once the event has
|
|
471
|
+
committed, right after its `EVENT_ACK`, the order the real server's
|
|
472
|
+
recordings show: a client can't count on its grid holding the change when
|
|
473
|
+
the ACK arrives. A push that fails then is logged, and the event stays
|
|
474
|
+
committed. Sequence and auto-increment values a rolled-back insert used stay
|
|
475
|
+
used, as a database sequence's do. A `VALIDATE` ack has `GENERATED: []`, as
|
|
476
|
+
the recorded ones do.
|
|
477
|
+
|
|
478
|
+
So a handler writes, then returns its warnings, and lets the engine decide.
|
|
479
|
+
A handler that returns its warnings *before* writing, as this README advised
|
|
480
|
+
before `IGNORE_WARNINGS` was modelled, now acks an ignored warning without
|
|
481
|
+
having written anything. `ctx.ignoreWarnings` is the message's flag (also
|
|
482
|
+
under `VALIDATE`, where warnings still NACK): read it to skip work, or before
|
|
483
|
+
throwing `NackError.warning(...)`, which always NACKs (the handler stopped
|
|
484
|
+
there, before its writes; GSF exceptions are always errors). Keep
|
|
485
|
+
`ctx.broadcastTableChange` for later (a timer, as the showcase's price ticker
|
|
486
|
+
does) and it pushes straight away once the event is over. The transaction
|
|
487
|
+
covers the store and the `ctx` broadcast functions only, not the server's
|
|
488
|
+
own `broadcast` / `broadcastTableChange` or side effects like timers. It
|
|
489
|
+
saves a table on the event's first write to it, a copy of its rows: cheap for
|
|
490
|
+
a mock's tables, noticeable only with very large ones and many events.
|
|
491
|
+
|
|
492
|
+
Where this differs from GSF: a GSF handler without `transactional = true`
|
|
493
|
+
keeps whatever its `onCommit` wrote before failing, and a transactional one
|
|
494
|
+
commits writes made before it *returns* `nack(...)`; here every NACK rolls
|
|
495
|
+
back. `IGNORE_WARNINGS` was not recorded (the showcase has no handler that
|
|
496
|
+
warns): `test/commitSemantics.test.ts` follows the Kotlin.
|
|
497
|
+
`fidelity: 'legacy'` keeps the old behaviour: no transaction (a handler's
|
|
498
|
+
writes stay whatever it replies), warnings always NACK (in `ERROR`), and
|
|
499
|
+
`ctx.ignoreWarnings` is always `false`.
|
|
500
|
+
|
|
449
501
|
## Running several servers in parallel (and one-command dev stacks)
|
|
450
502
|
|
|
451
503
|
Every `createMockServer()` instance is fully isolated — its own store,
|
|
@@ -668,6 +720,7 @@ createMockServer({ ...config, fidelity: 'legacy' });
|
|
|
668
720
|
| `META_REQUEST` content | The GSF shapes below; event META too | `{NAME, TYPE}` fields, `KEY: []` on dataservers, `REPLY_FIELD` alone on request servers; events are unknown |
|
|
669
721
|
| `JSON_SCHEMA_REQUEST` content | Per resource type, as below | One minimal `{DETAILS: {properties: {F: {type}}, required}}` as both INBOUND and OUTBOUND; a query or request-server name resolves to its source table |
|
|
670
722
|
| Declared schemas (`TableDef.fields`, query and view `fields`, `derivedTypes`) | Used for metadata and rows | Ignored: metadata is inferred, rows go out as stored |
|
|
723
|
+
| Commit events | Committed only on `EVENT_ACK`; `IGNORE_WARNINGS` lets returned warnings commit ([Commit semantics](#commit-semantics-validate-warnings-and-ignore_warnings)) | A handler's writes stay whatever it replies; warnings always NACK |
|
|
671
724
|
| A change to a table a view joins | Pushed only through joins with `backwardsJoin: true`, as the view rows it inserts, deletes or changes ([Views](#views-the-join-model)) | Every view row pushed as a MODIFY, whatever the join |
|
|
672
725
|
| `DATA_LOGON` `ORDER_BY` / `REVERSE` / `FIELDS`, criteria on unexposed fields | Honoured / `LOGON_NACK` | Ignored |
|
|
673
726
|
| `REQ_` | As in [Request servers](#request-servers-req_): `PARAMETRIC_TYPE`, criteria-only paging and ordering, `REQUEST` wildcards, ranges and arrays, a reply block, `MSG_NACK` failures | Every matching row in insertion order; `REQUEST` from the top level or `DETAILS`, `'*'`/`null`/`''` match anything, an array value any of its values; `CRITERIA_MATCH` fails open; no `RECORD_ID`/`TIMESTAMP`; errors as a `REP_` with `ERROR` |
|
|
@@ -1151,7 +1204,8 @@ plus end-to-end WS/HTTP tests (auth gating, filtered-subscription DELETE
|
|
|
1151
1204
|
delivery, keyed `REQ_*` lookups, session reuse, clean shutdown — and, in
|
|
1152
1205
|
`test/dataserver.test.ts`, the GSF envelopes, paging and the `'legacy'`
|
|
1153
1206
|
fidelity mode; `test/requestReply.test.ts` covers both kinds of request server,
|
|
1154
|
-
`test/views.test.ts` the view join model and its live pushes
|
|
1207
|
+
`test/views.test.ts` the view join model and its live pushes,
|
|
1208
|
+
`test/commitSemantics.test.ts` when an event commits)
|
|
1155
1209
|
via `node --test`, no build step. Only `test/**/*.test.ts`
|
|
1156
1210
|
files are run, so shared helpers (`test/support.ts`) can sit beside them.
|
|
1157
1211
|
|
package/dist/db/store.d.ts
CHANGED
|
@@ -14,7 +14,13 @@ export declare class Store {
|
|
|
14
14
|
private seedRows;
|
|
15
15
|
private removedRows;
|
|
16
16
|
private previousRows;
|
|
17
|
+
private savepoint;
|
|
17
18
|
readonly views: Map<string, ViewDef>;
|
|
19
|
+
begin(): void;
|
|
20
|
+
commit(): void;
|
|
21
|
+
rollback(): void;
|
|
22
|
+
get inTransaction(): boolean;
|
|
23
|
+
private save;
|
|
18
24
|
registerTable(tableName: string, definition: TableDef): this;
|
|
19
25
|
private counterOf;
|
|
20
26
|
private countersFor;
|
package/dist/db/store.js
CHANGED
|
@@ -67,6 +67,12 @@ function counterKey(prefix, format) {
|
|
|
67
67
|
}
|
|
68
68
|
// An AUTO_INCREMENT counter belongs to its table's field.
|
|
69
69
|
const autoIncrementKey = (tableName, field) => `${tableName}\u0000${field}`;
|
|
70
|
+
function restoreEntry(maps, tableName, saved) {
|
|
71
|
+
if (saved === undefined)
|
|
72
|
+
maps.delete(tableName);
|
|
73
|
+
else
|
|
74
|
+
maps.set(tableName, saved);
|
|
75
|
+
}
|
|
70
76
|
export class Store {
|
|
71
77
|
tables = new Map();
|
|
72
78
|
tableConfig = new Map();
|
|
@@ -87,7 +93,56 @@ export class Store {
|
|
|
87
93
|
// it, as it was before the first such modify (by RECORD_ID) — see
|
|
88
94
|
// takePreviousRecord. Bounded like removedRows.
|
|
89
95
|
previousRows = new Map();
|
|
96
|
+
// While a transaction is open (see begin), each table it has written to,
|
|
97
|
+
// as it was before.
|
|
98
|
+
savepoint;
|
|
90
99
|
views = new Map();
|
|
100
|
+
// Opens a transaction: the commit-event path wraps every handler in one
|
|
101
|
+
// (handlers/commitEvent.ts), so an event that NACKs leaves the tables as
|
|
102
|
+
// they were, as a GSF event whose onValidate NACKs never reaches its
|
|
103
|
+
// onCommit. Writes go through as usual; the first write to a table saves
|
|
104
|
+
// it, and rollback() puts back every table the transaction wrote to.
|
|
105
|
+
// Sequence and auto-increment counters and the RECORD_ID clock are not put
|
|
106
|
+
// back: a value used stays used, as a database sequence's does. Not nested,
|
|
107
|
+
// and the engine's own: an event that finds one open answers INTERNAL_ERROR.
|
|
108
|
+
// Saving copies the table's row map, O(rows) per event and table written.
|
|
109
|
+
begin() {
|
|
110
|
+
if (this.savepoint)
|
|
111
|
+
throw new Error('A store transaction is already open');
|
|
112
|
+
this.savepoint = new Map();
|
|
113
|
+
}
|
|
114
|
+
// Keeps every write since begin().
|
|
115
|
+
commit() {
|
|
116
|
+
this.savepoint = undefined;
|
|
117
|
+
}
|
|
118
|
+
// Undoes every write since begin().
|
|
119
|
+
rollback() {
|
|
120
|
+
const savepoint = this.savepoint;
|
|
121
|
+
this.savepoint = undefined;
|
|
122
|
+
for (const [tableName, saved] of savepoint ?? []) {
|
|
123
|
+
this.tables.set(tableName, saved.rows);
|
|
124
|
+
restoreEntry(this.removedRows, tableName, saved.removed);
|
|
125
|
+
restoreEntry(this.previousRows, tableName, saved.previous);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
get inTransaction() {
|
|
129
|
+
return this.savepoint !== undefined;
|
|
130
|
+
}
|
|
131
|
+
// Saves a table before the open transaction's first change to it.
|
|
132
|
+
save(tableName) {
|
|
133
|
+
if (!this.savepoint || this.savepoint.has(tableName))
|
|
134
|
+
return;
|
|
135
|
+
const rows = this.tables.get(tableName);
|
|
136
|
+
if (!rows)
|
|
137
|
+
return;
|
|
138
|
+
const removed = this.removedRows.get(tableName);
|
|
139
|
+
const previous = this.previousRows.get(tableName);
|
|
140
|
+
this.savepoint.set(tableName, {
|
|
141
|
+
rows: new Map(rows),
|
|
142
|
+
removed: removed && new Map(removed),
|
|
143
|
+
previous: previous && new Map(previous),
|
|
144
|
+
});
|
|
145
|
+
}
|
|
91
146
|
registerTable(tableName, definition) {
|
|
92
147
|
const { pkField, sequencePrefix, sequenceWidth = 3, sequenceFormat = 'prefix', rows = [], } = definition;
|
|
93
148
|
const pkFields = normalizePkFields(pkField);
|
|
@@ -113,6 +168,8 @@ export class Store {
|
|
|
113
168
|
}
|
|
114
169
|
this.tables.set(tableName, table);
|
|
115
170
|
this.tableConfig.set(tableName, config);
|
|
171
|
+
// Registering a table again replaces it whole: a rollback leaves it be.
|
|
172
|
+
this.savepoint?.delete(tableName);
|
|
116
173
|
this.removedRows.delete(tableName);
|
|
117
174
|
this.previousRows.delete(tableName);
|
|
118
175
|
this.seedRows.set(tableName, rows.map((row) => ({ ...row })));
|
|
@@ -326,6 +383,7 @@ export class Store {
|
|
|
326
383
|
const config = this.tableConfig.get(tableName);
|
|
327
384
|
if (!table || !config)
|
|
328
385
|
throw new Error(`Unknown table: ${tableName}`);
|
|
386
|
+
this.save(tableName);
|
|
329
387
|
const row = { ...record };
|
|
330
388
|
this.generateValues(tableName, config, row);
|
|
331
389
|
const missing = config.pkFields.filter((field) => row[field] === undefined);
|
|
@@ -348,6 +406,7 @@ export class Store {
|
|
|
348
406
|
const config = this.tableConfig.get(tableName);
|
|
349
407
|
if (!table || !config || !table.has(pk))
|
|
350
408
|
return undefined;
|
|
409
|
+
this.save(tableName);
|
|
351
410
|
const existing = table.get(pk);
|
|
352
411
|
this.notePrevious(tableName, existing);
|
|
353
412
|
const updated = { ...existing, ...patch };
|
|
@@ -373,6 +432,7 @@ export class Store {
|
|
|
373
432
|
const existing = table?.get(pk);
|
|
374
433
|
if (!table || !existing)
|
|
375
434
|
return false;
|
|
435
|
+
this.save(tableName);
|
|
376
436
|
table.delete(pk);
|
|
377
437
|
this.noteRemoved(tableName, pk, existing);
|
|
378
438
|
return true;
|
|
@@ -383,6 +443,7 @@ export class Store {
|
|
|
383
443
|
// doesn't), including the old record when a key's row was replaced before
|
|
384
444
|
// one broadcast.
|
|
385
445
|
takeRemovedRows(tableName, key) {
|
|
446
|
+
this.save(tableName);
|
|
386
447
|
const removed = this.removedRows.get(tableName);
|
|
387
448
|
const records = removed?.get(key) ?? [];
|
|
388
449
|
removed?.delete(key);
|
|
@@ -396,6 +457,7 @@ export class Store {
|
|
|
396
457
|
const previous = this.previousRows.get(tableName);
|
|
397
458
|
if (id === undefined || !previous)
|
|
398
459
|
return undefined;
|
|
460
|
+
this.save(tableName);
|
|
399
461
|
const record = previous.get(id);
|
|
400
462
|
previous.delete(id);
|
|
401
463
|
return record;
|
|
@@ -12,12 +12,70 @@ function resolveEventHandler(eventName, config) {
|
|
|
12
12
|
crudEventHandler(eventName, config) ??
|
|
13
13
|
config.defaultEventHandler);
|
|
14
14
|
}
|
|
15
|
+
function openTransaction(store, ctx) {
|
|
16
|
+
const pending = [];
|
|
17
|
+
let open = true;
|
|
18
|
+
store.begin();
|
|
19
|
+
const hold = (push) => {
|
|
20
|
+
if (open)
|
|
21
|
+
pending.push(push);
|
|
22
|
+
else
|
|
23
|
+
push();
|
|
24
|
+
};
|
|
25
|
+
return {
|
|
26
|
+
broadcast: (resourceName, operation, row) => hold(() => ctx.broadcast(resourceName, operation, row)),
|
|
27
|
+
broadcastTableChange: (tableName, operation, pk) => hold(() => ctx.broadcastTableChange(tableName, operation, pk)),
|
|
28
|
+
commit() {
|
|
29
|
+
open = false;
|
|
30
|
+
store.commit();
|
|
31
|
+
},
|
|
32
|
+
flush() {
|
|
33
|
+
for (const push of pending.splice(0)) {
|
|
34
|
+
try {
|
|
35
|
+
push();
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.error('[mock-server] Error pushing a committed change:', error);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
rollback() {
|
|
43
|
+
if (!open)
|
|
44
|
+
return;
|
|
45
|
+
open = false;
|
|
46
|
+
pending.length = 0;
|
|
47
|
+
store.rollback();
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
15
51
|
// Commit events (EVENT_<ENTITY>_INSERT/_AMEND/_DELETE, or whatever suffix
|
|
16
52
|
// convention your project uses) are looked up by exact MESSAGE_TYPE against
|
|
17
53
|
// config.eventHandlers, then the tables' generic CRUD events. A handler
|
|
18
|
-
// receives (details, ctx) and either returns { generated
|
|
19
|
-
//
|
|
20
|
-
//
|
|
54
|
+
// receives (details, ctx) and either returns { generated?, warnings? } or
|
|
55
|
+
// throws NackError(errors) on failure.
|
|
56
|
+
//
|
|
57
|
+
// A handler stands for a GSF event handler's onValidate and onCommit at once,
|
|
58
|
+
// and the engine gives its outcome the commit semantics of GSF's
|
|
59
|
+
// TransactionalEventHandlerContainer (genesis-pal-eventhandler, GSF v8.15.29,
|
|
60
|
+
// transactionalEventFlow/defaultEventFlow; the typed handlers'
|
|
61
|
+
// SyncValidatingEventHandler.process is the same):
|
|
62
|
+
//
|
|
63
|
+
// VALIDATE: true → onValidate only: its reply, nothing committed
|
|
64
|
+
// onValidate ACKs → onCommit runs: its reply
|
|
65
|
+
// onValidate NACKs with warnings only (`result.error.isEmpty()`)
|
|
66
|
+
// and IGNORE_WARNINGS: true → onCommit runs: its reply (an ACK carries
|
|
67
|
+
// no WARNING)
|
|
68
|
+
// any other NACK → that NACK, nothing committed
|
|
69
|
+
//
|
|
70
|
+
// So the handler runs in a transaction (openTransaction), and the event
|
|
71
|
+
// commits only when it acks: returned warnings without IGNORE_WARNINGS, a
|
|
72
|
+
// thrown NackError (errors, with or without warnings), any other exception,
|
|
73
|
+
// and VALIDATE all roll back what the handler wrote, and drop the live pushes
|
|
74
|
+
// it asked for. Returned warnings with IGNORE_WARNINGS commit and ack with
|
|
75
|
+
// the handler's GENERATED. A thrown NackError always NACKs, warnings-only
|
|
76
|
+
// ones included: the handler stopped there, before its writes (a GSF
|
|
77
|
+
// exception is always an error). A VALIDATE ack has GENERATED: [], as the
|
|
78
|
+
// recorded ones do: whatever the handler generated was rolled back.
|
|
21
79
|
export function handleCommitEvent(message, connection, config, store, ctx) {
|
|
22
80
|
const eventName = message.MESSAGE_TYPE;
|
|
23
81
|
// defaultEventHandler catches anything unregistered — lets projects with
|
|
@@ -47,15 +105,21 @@ export function handleCommitEvent(message, connection, config, store, ctx) {
|
|
|
47
105
|
}
|
|
48
106
|
// Before any handler runs, the message is checked against the event's
|
|
49
107
|
// declared schema (eventValidation.ts), as GSF's decoder does; each failure
|
|
50
|
-
// is a FieldError in one EVENT_NACK.
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
// handler that honours it acks without writing.
|
|
108
|
+
// is a FieldError in one EVENT_NACK. A handler's failure is an EVENT_NACK
|
|
109
|
+
// with GSF's error items (see protocol/errors.ts): a thrown NackError gives
|
|
110
|
+
// its own, anything else is StandardError INTERNAL_ERROR.
|
|
111
|
+
// Legacy fidelity keeps what this engine did before: no schema check, no
|
|
112
|
+
// transaction (a handler's writes stay whatever it replies), and warnings
|
|
113
|
+
// always NACK, IGNORE_WARNINGS or not.
|
|
57
114
|
const legacy = isLegacyFidelity(config);
|
|
115
|
+
const validate = message.VALIDATE === true;
|
|
116
|
+
const ignoreWarnings = !legacy && message.IGNORE_WARNINGS === true;
|
|
117
|
+
let transaction;
|
|
58
118
|
try {
|
|
119
|
+
// Inside the try: a store transaction someone else left open fails this
|
|
120
|
+
// event (INTERNAL_ERROR) instead of leaving it unanswered.
|
|
121
|
+
if (!legacy)
|
|
122
|
+
transaction = openTransaction(store, ctx);
|
|
59
123
|
const schemaErrors = legacy ? [] : validateEventMessage(message, connection, config, store);
|
|
60
124
|
if (schemaErrors.length > 0)
|
|
61
125
|
throw new NackError(schemaErrors);
|
|
@@ -63,22 +127,36 @@ export function handleCommitEvent(message, connection, config, store, ctx) {
|
|
|
63
127
|
connection,
|
|
64
128
|
store,
|
|
65
129
|
message,
|
|
66
|
-
|
|
67
|
-
|
|
130
|
+
broadcast: transaction?.broadcast ?? ctx.broadcast,
|
|
131
|
+
broadcastTableChange: transaction?.broadcastTableChange ?? ctx.broadcastTableChange,
|
|
132
|
+
validate,
|
|
133
|
+
ignoreWarnings,
|
|
68
134
|
}) ?? {};
|
|
69
|
-
if (result.warnings?.length) {
|
|
135
|
+
if (result.warnings?.length && (validate || !ignoreWarnings)) {
|
|
136
|
+
transaction?.rollback();
|
|
70
137
|
connection.reply(message, {
|
|
71
138
|
MESSAGE_TYPE: MESSAGE_TYPE.EVENT_NACK,
|
|
72
139
|
...warningPayload(result.warnings, legacy),
|
|
73
140
|
});
|
|
74
141
|
return;
|
|
75
142
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
143
|
+
const generated = validate && !legacy ? [] : (result.generated ?? []);
|
|
144
|
+
// A GENERATED that can't go on the wire (a BigInt, a cycle) fails the
|
|
145
|
+
// event here, before anything is committed.
|
|
146
|
+
JSON.stringify(generated);
|
|
147
|
+
if (validate)
|
|
148
|
+
transaction?.rollback();
|
|
149
|
+
else
|
|
150
|
+
transaction?.commit();
|
|
151
|
+
try {
|
|
152
|
+
connection.reply(message, { MESSAGE_TYPE: MESSAGE_TYPE.EVENT_ACK, GENERATED: generated });
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
transaction?.flush();
|
|
156
|
+
}
|
|
80
157
|
}
|
|
81
158
|
catch (error) {
|
|
159
|
+
transaction?.rollback();
|
|
82
160
|
connection.reply(message, {
|
|
83
161
|
MESSAGE_TYPE: MESSAGE_TYPE.EVENT_NACK,
|
|
84
162
|
...nackPayload(error, legacy),
|
package/dist/protocol/errors.js
CHANGED
|
@@ -264,7 +264,9 @@ export class NackError extends Error {
|
|
|
264
264
|
static nack(text) {
|
|
265
265
|
return new NackError({ '@type': 'StandardError', CODE: ErrorCode.INTERNAL_ERROR, TEXT: text });
|
|
266
266
|
}
|
|
267
|
-
// A NACK that only warns: ERROR stays empty, WARNING carries these.
|
|
267
|
+
// A NACK that only warns: ERROR stays empty, WARNING carries these. Thrown,
|
|
268
|
+
// it NACKs even under IGNORE_WARNINGS (the handler stopped there); check
|
|
269
|
+
// ctx.ignoreWarnings first, or return { warnings } instead.
|
|
268
270
|
static warning(warnings) {
|
|
269
271
|
return new NackError([], { warnings: asList(warnings) });
|
|
270
272
|
}
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
package/src/db/store.ts
CHANGED
|
@@ -116,6 +116,21 @@ function counterKey(prefix: string, format: SequenceFormat): string {
|
|
|
116
116
|
// An AUTO_INCREMENT counter belongs to its table's field.
|
|
117
117
|
const autoIncrementKey = (tableName: string, field: string) => `${tableName}\u0000${field}`;
|
|
118
118
|
|
|
119
|
+
// What a transaction keeps of a table from before its first write in it: the
|
|
120
|
+
// rows, and the removed and previous records the write's bookkeeping changes.
|
|
121
|
+
// Row objects are never changed in place once stored, so copying the maps
|
|
122
|
+
// is enough.
|
|
123
|
+
interface TableSavepoint {
|
|
124
|
+
rows: Map<Pk, Row>;
|
|
125
|
+
removed: Map<Pk, Row[]> | undefined;
|
|
126
|
+
previous: Map<string, Row> | undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function restoreEntry<V>(maps: Map<string, V>, tableName: string, saved: V | undefined): void {
|
|
130
|
+
if (saved === undefined) maps.delete(tableName);
|
|
131
|
+
else maps.set(tableName, saved);
|
|
132
|
+
}
|
|
133
|
+
|
|
119
134
|
export class Store {
|
|
120
135
|
private tables = new Map<string, Map<Pk, Row>>();
|
|
121
136
|
private tableConfig = new Map<string, TableConfig>();
|
|
@@ -136,8 +151,59 @@ export class Store {
|
|
|
136
151
|
// it, as it was before the first such modify (by RECORD_ID) — see
|
|
137
152
|
// takePreviousRecord. Bounded like removedRows.
|
|
138
153
|
private previousRows = new Map<string, Map<string, Row>>();
|
|
154
|
+
// While a transaction is open (see begin), each table it has written to,
|
|
155
|
+
// as it was before.
|
|
156
|
+
private savepoint: Map<string, TableSavepoint> | undefined;
|
|
139
157
|
readonly views = new Map<string, ViewDef>();
|
|
140
158
|
|
|
159
|
+
// Opens a transaction: the commit-event path wraps every handler in one
|
|
160
|
+
// (handlers/commitEvent.ts), so an event that NACKs leaves the tables as
|
|
161
|
+
// they were, as a GSF event whose onValidate NACKs never reaches its
|
|
162
|
+
// onCommit. Writes go through as usual; the first write to a table saves
|
|
163
|
+
// it, and rollback() puts back every table the transaction wrote to.
|
|
164
|
+
// Sequence and auto-increment counters and the RECORD_ID clock are not put
|
|
165
|
+
// back: a value used stays used, as a database sequence's does. Not nested,
|
|
166
|
+
// and the engine's own: an event that finds one open answers INTERNAL_ERROR.
|
|
167
|
+
// Saving copies the table's row map, O(rows) per event and table written.
|
|
168
|
+
begin(): void {
|
|
169
|
+
if (this.savepoint) throw new Error('A store transaction is already open');
|
|
170
|
+
this.savepoint = new Map();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Keeps every write since begin().
|
|
174
|
+
commit(): void {
|
|
175
|
+
this.savepoint = undefined;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Undoes every write since begin().
|
|
179
|
+
rollback(): void {
|
|
180
|
+
const savepoint = this.savepoint;
|
|
181
|
+
this.savepoint = undefined;
|
|
182
|
+
for (const [tableName, saved] of savepoint ?? []) {
|
|
183
|
+
this.tables.set(tableName, saved.rows);
|
|
184
|
+
restoreEntry(this.removedRows, tableName, saved.removed);
|
|
185
|
+
restoreEntry(this.previousRows, tableName, saved.previous);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
get inTransaction(): boolean {
|
|
190
|
+
return this.savepoint !== undefined;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Saves a table before the open transaction's first change to it.
|
|
194
|
+
private save(tableName: string): void {
|
|
195
|
+
if (!this.savepoint || this.savepoint.has(tableName)) return;
|
|
196
|
+
const rows = this.tables.get(tableName);
|
|
197
|
+
if (!rows) return;
|
|
198
|
+
const removed = this.removedRows.get(tableName);
|
|
199
|
+
const previous = this.previousRows.get(tableName);
|
|
200
|
+
this.savepoint.set(tableName, {
|
|
201
|
+
rows: new Map(rows),
|
|
202
|
+
removed: removed && new Map(removed),
|
|
203
|
+
previous: previous && new Map(previous),
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
141
207
|
registerTable(tableName: string, definition: TableDef): this {
|
|
142
208
|
const {
|
|
143
209
|
pkField,
|
|
@@ -170,6 +236,8 @@ export class Store {
|
|
|
170
236
|
}
|
|
171
237
|
this.tables.set(tableName, table);
|
|
172
238
|
this.tableConfig.set(tableName, config);
|
|
239
|
+
// Registering a table again replaces it whole: a rollback leaves it be.
|
|
240
|
+
this.savepoint?.delete(tableName);
|
|
173
241
|
this.removedRows.delete(tableName);
|
|
174
242
|
this.previousRows.delete(tableName);
|
|
175
243
|
this.seedRows.set(
|
|
@@ -398,6 +466,7 @@ export class Store {
|
|
|
398
466
|
const config = this.tableConfig.get(tableName);
|
|
399
467
|
if (!table || !config) throw new Error(`Unknown table: ${tableName}`);
|
|
400
468
|
|
|
469
|
+
this.save(tableName);
|
|
401
470
|
const row = { ...record };
|
|
402
471
|
this.generateValues(tableName, config, row);
|
|
403
472
|
const missing = config.pkFields.filter((field) => row[field] === undefined);
|
|
@@ -421,6 +490,7 @@ export class Store {
|
|
|
421
490
|
const table = this.tables.get(tableName);
|
|
422
491
|
const config = this.tableConfig.get(tableName);
|
|
423
492
|
if (!table || !config || !table.has(pk)) return undefined;
|
|
493
|
+
this.save(tableName);
|
|
424
494
|
const existing = table.get(pk)!;
|
|
425
495
|
this.notePrevious(tableName, existing);
|
|
426
496
|
const updated = { ...existing, ...patch };
|
|
@@ -445,6 +515,7 @@ export class Store {
|
|
|
445
515
|
const table = this.tables.get(tableName);
|
|
446
516
|
const existing = table?.get(pk);
|
|
447
517
|
if (!table || !existing) return false;
|
|
518
|
+
this.save(tableName);
|
|
448
519
|
table.delete(pk);
|
|
449
520
|
this.noteRemoved(tableName, pk, existing);
|
|
450
521
|
return true;
|
|
@@ -456,6 +527,7 @@ export class Store {
|
|
|
456
527
|
// doesn't), including the old record when a key's row was replaced before
|
|
457
528
|
// one broadcast.
|
|
458
529
|
takeRemovedRows(tableName: string, key: Pk): Row[] {
|
|
530
|
+
this.save(tableName);
|
|
459
531
|
const removed = this.removedRows.get(tableName);
|
|
460
532
|
const records = removed?.get(key) ?? [];
|
|
461
533
|
removed?.delete(key);
|
|
@@ -469,6 +541,7 @@ export class Store {
|
|
|
469
541
|
const id = recordKey(row);
|
|
470
542
|
const previous = this.previousRows.get(tableName);
|
|
471
543
|
if (id === undefined || !previous) return undefined;
|
|
544
|
+
this.save(tableName);
|
|
472
545
|
const record = previous.get(id);
|
|
473
546
|
previous.delete(id);
|
|
474
547
|
return record;
|
|
@@ -33,12 +33,90 @@ function resolveEventHandler(
|
|
|
33
33
|
);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
// One event's transaction: the store's (Store.begin), plus the live pushes
|
|
37
|
+
// the handler asks for, held back until the event has committed and acked —
|
|
38
|
+
// a client must never see a change that is then rolled back, and on the real
|
|
39
|
+
// server the dataserver's update reaches the client after the EVENT_ACK (the
|
|
40
|
+
// recordings' order), so a client can't count on its grid holding the change
|
|
41
|
+
// when the ACK arrives. Once the transaction is over, the broadcast
|
|
42
|
+
// functions push straight away again, so a handler can keep them for later
|
|
43
|
+
// (the showcase's price ticker does).
|
|
44
|
+
interface EventTransaction {
|
|
45
|
+
broadcast: BroadcastFn;
|
|
46
|
+
broadcastTableChange: BroadcastTableChangeFn;
|
|
47
|
+
// Keeps the writes; the held pushes wait for flush().
|
|
48
|
+
commit(): void;
|
|
49
|
+
// Sends the held pushes. The event is committed whatever a push does: a
|
|
50
|
+
// failing one is logged, and the rest still go out.
|
|
51
|
+
flush(): void;
|
|
52
|
+
// Undoes the writes and drops the held pushes. Does nothing once the
|
|
53
|
+
// transaction is over.
|
|
54
|
+
rollback(): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function openTransaction(store: Store, ctx: CommitEventCtx): EventTransaction {
|
|
58
|
+
const pending: Array<() => void> = [];
|
|
59
|
+
let open = true;
|
|
60
|
+
store.begin();
|
|
61
|
+
const hold = (push: () => void) => {
|
|
62
|
+
if (open) pending.push(push);
|
|
63
|
+
else push();
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
broadcast: (resourceName, operation, row) =>
|
|
67
|
+
hold(() => ctx.broadcast(resourceName, operation, row)),
|
|
68
|
+
broadcastTableChange: (tableName, operation, pk) =>
|
|
69
|
+
hold(() => ctx.broadcastTableChange(tableName, operation, pk)),
|
|
70
|
+
commit() {
|
|
71
|
+
open = false;
|
|
72
|
+
store.commit();
|
|
73
|
+
},
|
|
74
|
+
flush() {
|
|
75
|
+
for (const push of pending.splice(0)) {
|
|
76
|
+
try {
|
|
77
|
+
push();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error('[mock-server] Error pushing a committed change:', error);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
rollback() {
|
|
84
|
+
if (!open) return;
|
|
85
|
+
open = false;
|
|
86
|
+
pending.length = 0;
|
|
87
|
+
store.rollback();
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
36
92
|
// Commit events (EVENT_<ENTITY>_INSERT/_AMEND/_DELETE, or whatever suffix
|
|
37
93
|
// convention your project uses) are looked up by exact MESSAGE_TYPE against
|
|
38
94
|
// config.eventHandlers, then the tables' generic CRUD events. A handler
|
|
39
|
-
// receives (details, ctx) and either returns { generated
|
|
40
|
-
//
|
|
41
|
-
//
|
|
95
|
+
// receives (details, ctx) and either returns { generated?, warnings? } or
|
|
96
|
+
// throws NackError(errors) on failure.
|
|
97
|
+
//
|
|
98
|
+
// A handler stands for a GSF event handler's onValidate and onCommit at once,
|
|
99
|
+
// and the engine gives its outcome the commit semantics of GSF's
|
|
100
|
+
// TransactionalEventHandlerContainer (genesis-pal-eventhandler, GSF v8.15.29,
|
|
101
|
+
// transactionalEventFlow/defaultEventFlow; the typed handlers'
|
|
102
|
+
// SyncValidatingEventHandler.process is the same):
|
|
103
|
+
//
|
|
104
|
+
// VALIDATE: true → onValidate only: its reply, nothing committed
|
|
105
|
+
// onValidate ACKs → onCommit runs: its reply
|
|
106
|
+
// onValidate NACKs with warnings only (`result.error.isEmpty()`)
|
|
107
|
+
// and IGNORE_WARNINGS: true → onCommit runs: its reply (an ACK carries
|
|
108
|
+
// no WARNING)
|
|
109
|
+
// any other NACK → that NACK, nothing committed
|
|
110
|
+
//
|
|
111
|
+
// So the handler runs in a transaction (openTransaction), and the event
|
|
112
|
+
// commits only when it acks: returned warnings without IGNORE_WARNINGS, a
|
|
113
|
+
// thrown NackError (errors, with or without warnings), any other exception,
|
|
114
|
+
// and VALIDATE all roll back what the handler wrote, and drop the live pushes
|
|
115
|
+
// it asked for. Returned warnings with IGNORE_WARNINGS commit and ack with
|
|
116
|
+
// the handler's GENERATED. A thrown NackError always NACKs, warnings-only
|
|
117
|
+
// ones included: the handler stopped there, before its writes (a GSF
|
|
118
|
+
// exception is always an error). A VALIDATE ack has GENERATED: [], as the
|
|
119
|
+
// recorded ones do: whatever the handler generated was rolled back.
|
|
42
120
|
export function handleCommitEvent(
|
|
43
121
|
message: GenesisMessage,
|
|
44
122
|
connection: Connection,
|
|
@@ -79,15 +157,20 @@ export function handleCommitEvent(
|
|
|
79
157
|
|
|
80
158
|
// Before any handler runs, the message is checked against the event's
|
|
81
159
|
// declared schema (eventValidation.ts), as GSF's decoder does; each failure
|
|
82
|
-
// is a FieldError in one EVENT_NACK.
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// handler that honours it acks without writing.
|
|
160
|
+
// is a FieldError in one EVENT_NACK. A handler's failure is an EVENT_NACK
|
|
161
|
+
// with GSF's error items (see protocol/errors.ts): a thrown NackError gives
|
|
162
|
+
// its own, anything else is StandardError INTERNAL_ERROR.
|
|
163
|
+
// Legacy fidelity keeps what this engine did before: no schema check, no
|
|
164
|
+
// transaction (a handler's writes stay whatever it replies), and warnings
|
|
165
|
+
// always NACK, IGNORE_WARNINGS or not.
|
|
89
166
|
const legacy = isLegacyFidelity(config);
|
|
167
|
+
const validate = message.VALIDATE === true;
|
|
168
|
+
const ignoreWarnings = !legacy && message.IGNORE_WARNINGS === true;
|
|
169
|
+
let transaction: EventTransaction | undefined;
|
|
90
170
|
try {
|
|
171
|
+
// Inside the try: a store transaction someone else left open fails this
|
|
172
|
+
// event (INTERNAL_ERROR) instead of leaving it unanswered.
|
|
173
|
+
if (!legacy) transaction = openTransaction(store, ctx);
|
|
91
174
|
const schemaErrors = legacy ? [] : validateEventMessage(message, connection, config, store);
|
|
92
175
|
if (schemaErrors.length > 0) throw new NackError(schemaErrors);
|
|
93
176
|
const result =
|
|
@@ -95,21 +178,32 @@ export function handleCommitEvent(
|
|
|
95
178
|
connection,
|
|
96
179
|
store,
|
|
97
180
|
message,
|
|
98
|
-
|
|
99
|
-
|
|
181
|
+
broadcast: transaction?.broadcast ?? ctx.broadcast,
|
|
182
|
+
broadcastTableChange: transaction?.broadcastTableChange ?? ctx.broadcastTableChange,
|
|
183
|
+
validate,
|
|
184
|
+
ignoreWarnings,
|
|
100
185
|
}) ?? {};
|
|
101
|
-
if (result.warnings?.length) {
|
|
186
|
+
if (result.warnings?.length && (validate || !ignoreWarnings)) {
|
|
187
|
+
transaction?.rollback();
|
|
102
188
|
connection.reply(message, {
|
|
103
189
|
MESSAGE_TYPE: MESSAGE_TYPE.EVENT_NACK,
|
|
104
190
|
...warningPayload(result.warnings, legacy),
|
|
105
191
|
});
|
|
106
192
|
return;
|
|
107
193
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
194
|
+
const generated = validate && !legacy ? [] : (result.generated ?? []);
|
|
195
|
+
// A GENERATED that can't go on the wire (a BigInt, a cycle) fails the
|
|
196
|
+
// event here, before anything is committed.
|
|
197
|
+
JSON.stringify(generated);
|
|
198
|
+
if (validate) transaction?.rollback();
|
|
199
|
+
else transaction?.commit();
|
|
200
|
+
try {
|
|
201
|
+
connection.reply(message, { MESSAGE_TYPE: MESSAGE_TYPE.EVENT_ACK, GENERATED: generated });
|
|
202
|
+
} finally {
|
|
203
|
+
transaction?.flush();
|
|
204
|
+
}
|
|
112
205
|
} catch (error) {
|
|
206
|
+
transaction?.rollback();
|
|
113
207
|
connection.reply(message, {
|
|
114
208
|
MESSAGE_TYPE: MESSAGE_TYPE.EVENT_NACK,
|
|
115
209
|
...nackPayload(error, legacy),
|
package/src/protocol/errors.ts
CHANGED
|
@@ -245,8 +245,9 @@ function asList(inputs: string | NackErrorInput | NackErrorInput[] | undefined):
|
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
export interface NackErrorOptions {
|
|
248
|
-
// Sent in the NACK's WARNING list.
|
|
249
|
-
//
|
|
248
|
+
// Sent in the NACK's WARNING list. A thrown NackError always fails the
|
|
249
|
+
// event, IGNORE_WARNINGS or not: return warnings from the handler instead
|
|
250
|
+
// ({ warnings }) for ones that should give way to it.
|
|
250
251
|
warnings?: NackErrorInput | NackErrorInput[];
|
|
251
252
|
}
|
|
252
253
|
|
|
@@ -334,7 +335,9 @@ export class NackError extends Error {
|
|
|
334
335
|
return new NackError({ '@type': 'StandardError', CODE: ErrorCode.INTERNAL_ERROR, TEXT: text });
|
|
335
336
|
}
|
|
336
337
|
|
|
337
|
-
// A NACK that only warns: ERROR stays empty, WARNING carries these.
|
|
338
|
+
// A NACK that only warns: ERROR stays empty, WARNING carries these. Thrown,
|
|
339
|
+
// it NACKs even under IGNORE_WARNINGS (the handler stopped there); check
|
|
340
|
+
// ctx.ignoreWarnings first, or return { warnings } instead.
|
|
338
341
|
static warning(warnings: string | NackErrorInput | NackErrorInput[]): NackError {
|
|
339
342
|
return new NackError([], { warnings: asList(warnings) });
|
|
340
343
|
}
|
package/src/types.ts
CHANGED
|
@@ -426,14 +426,29 @@ export interface EventHandlerCtx {
|
|
|
426
426
|
// succeed. GSF then runs only the handler's onValidate and answers its NACK,
|
|
427
427
|
// or an EVENT_ACK with GENERATED: [] — nothing is written. A handler that
|
|
428
428
|
// writes should run its checks and return before writing when this is set;
|
|
429
|
-
// the generic CRUD events do.
|
|
429
|
+
// the generic CRUD events do. The engine rolls back whatever it writes
|
|
430
|
+
// anyway, and acks with GENERATED: [] whatever the handler returns (see
|
|
431
|
+
// handlers/commitEvent.ts; not under fidelity: 'legacy').
|
|
430
432
|
validate: boolean;
|
|
433
|
+
// `IGNORE_WARNINGS: true` on the message: warnings the handler returns
|
|
434
|
+
// don't stop the event. The engine applies it: outside VALIDATE, returned
|
|
435
|
+
// warnings commit the handler's writes and ACK when this is set, and roll
|
|
436
|
+
// them back and NACK when it isn't. So write, then return the warnings:
|
|
437
|
+
// a handler that returns them before writing (as advised before this
|
|
438
|
+
// flag was modelled) now ACKs an ignored warning without writing. Read it
|
|
439
|
+
// to skip work that only matters when the event will commit, or before
|
|
440
|
+
// throwing NackError.warning(...), which always NACKs. Always false under
|
|
441
|
+
// fidelity: 'legacy', which doesn't model it.
|
|
442
|
+
ignoreWarnings: boolean;
|
|
431
443
|
}
|
|
432
444
|
|
|
433
|
-
// `warnings`
|
|
434
|
-
//
|
|
435
|
-
//
|
|
436
|
-
//
|
|
445
|
+
// `warnings` (a non-empty list) is a GSF onValidate's warning-only
|
|
446
|
+
// EventNack: without IGNORE_WARNINGS, or under VALIDATE, the reply is an
|
|
447
|
+
// EVENT_NACK carrying them in WARNING with an empty ERROR, and nothing the
|
|
448
|
+
// handler wrote is kept; with IGNORE_WARNINGS it is an EVENT_ACK with
|
|
449
|
+
// `generated` (no WARNING), and the writes are kept. Errors are thrown
|
|
450
|
+
// (NackError), never returned: a thrown NackError NACKs with its errors and
|
|
451
|
+
// any warnings it carries, and rolls back too.
|
|
437
452
|
export type EventHandlerResult = { generated?: Row[]; warnings?: NackErrorInput[] } | void;
|
|
438
453
|
|
|
439
454
|
export type EventHandler = (details: Row, ctx: EventHandlerCtx) => EventHandlerResult;
|