@jarenjs/contract 0.72.2 → 0.73.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/docs/CONTRACT-FORMAT.md +14 -9
- package/package.json +5 -5
- package/src/ledger.js +10 -6
- package/src/port/serve.js +3 -2
- package/src/stream/server.js +3 -2
package/docs/CONTRACT-FORMAT.md
CHANGED
|
@@ -998,7 +998,9 @@ The `ref` a `new` claim hands back is `{ id, generation }` — the
|
|
|
998
998
|
record's id and the **generation** the claim minted for it — and is
|
|
999
999
|
portable: it names the record across processes rather than holding it.
|
|
1000
1000
|
A settlement is fenced by both: `commit`/`fail` settle the record whose
|
|
1001
|
-
`id` AND `generation` the ref names while it is still `started
|
|
1001
|
+
`id` AND `generation` the ref names while it is still `started` and its
|
|
1002
|
+
`expiresAt` is greater than the settlement instant. Expiry is checked by
|
|
1003
|
+
the settlement itself, without waiting for a lookup or sweep. A
|
|
1002
1004
|
ref whose record expired, was reclaimed under a newer generation, or was
|
|
1003
1005
|
settled already is refused with `JC1011` (thrown or rejected) — the
|
|
1004
1006
|
binding reports it to `onError` and the response still goes out, so a
|
|
@@ -1114,8 +1116,8 @@ export function createSqliteLedger(path, { ttlMs = 86_400_000, now: clock = Date
|
|
|
1114
1116
|
const put = db.prepare("INSERT INTO ledger (id, generation, op, scope, key, hash, status, createdAt, updatedAt, expiresAt) VALUES (?, ?, ?, ?, ?, ?, 'started', ?, ?, ?)");
|
|
1115
1117
|
const drop = db.prepare('DELETE FROM ledger WHERE id = ?');
|
|
1116
1118
|
// the fence: a settlement names the id AND the generation of the claim
|
|
1117
|
-
// that started the record,
|
|
1118
|
-
const settle = db.prepare("UPDATE ledger SET status = ?, response = ?, retryable = ?, updatedAt = ? WHERE id = ? AND generation = ? AND status = 'started'");
|
|
1119
|
+
// that started the record, and the record must still be unexpired
|
|
1120
|
+
const settle = db.prepare("UPDATE ledger SET status = ?, response = ?, retryable = ?, updatedAt = ? WHERE id = ? AND generation = ? AND status = 'started' AND expiresAt > ?");
|
|
1119
1121
|
const reap = db.prepare('DELETE FROM ledger WHERE expiresAt <= ?');
|
|
1120
1122
|
const stored = (row) => (row.response === null ? null : JSON.parse(row.response));
|
|
1121
1123
|
const at = (now) => (typeof now === 'number' ? now : clock());
|
|
@@ -1147,10 +1149,12 @@ export function createSqliteLedger(path, { ttlMs = 86_400_000, now: clock = Date
|
|
|
1147
1149
|
}
|
|
1148
1150
|
},
|
|
1149
1151
|
commit(ref, response, now) {
|
|
1150
|
-
|
|
1152
|
+
const time = at(now);
|
|
1153
|
+
settled(ref, settle.run('committed', JSON.stringify(response), null, time, (ref)?.id ?? '', (ref)?.generation ?? '', time).changes);
|
|
1151
1154
|
},
|
|
1152
1155
|
fail(ref, retryable, response, now) {
|
|
1153
|
-
|
|
1156
|
+
const time = at(now);
|
|
1157
|
+
settled(ref, settle.run('failed', response === undefined ? null : JSON.stringify(response), retryable === true ? 1 : 0, time, (ref)?.id ?? '', (ref)?.generation ?? '', time).changes);
|
|
1154
1158
|
},
|
|
1155
1159
|
lookup({ op, scope, key, now }) {
|
|
1156
1160
|
const row = (one.get(ledgerId(op, scope, key)));
|
|
@@ -2395,7 +2399,7 @@ no-store`, `x-jaren-trace`. Events, in order:
|
|
|
2395
2399
|
- `end` — data `{ "reason": "closed" | "server-shutdown" }`.
|
|
2396
2400
|
|
|
2397
2401
|
`seq` is strictly increasing per stream; a violation is the client's
|
|
2398
|
-
`JC2092`. An emission whose serialized patch exceeds
|
|
2402
|
+
`JC2092`. An emission whose serialized patch's UTF-8 byte length exceeds
|
|
2399
2403
|
`policy.stream.maxPatchBytes` is replaced by a fresh `snapshot` event
|
|
2400
2404
|
at that emission's seq — the consumer swaps its document instead of
|
|
2401
2405
|
patching it; nothing is dropped.
|
|
@@ -2423,10 +2427,11 @@ informational, never an outcome).
|
|
|
2423
2427
|
**Bounds.** `serveHttp`/`servePort` take `streamLimits: { replay: {
|
|
2424
2428
|
limit, maxBytes }, queue: { events, bytes } }` (defaults 256 / 1 MiB
|
|
2425
2429
|
for both): a replay page asks for at most `replay.limit` emissions and
|
|
2426
|
-
`replay.maxBytes` serialized patch bytes; the undelivered
|
|
2427
|
-
events that arrived while a carrier write was pending or a page was
|
|
2430
|
+
`replay.maxBytes` serialized patch bytes, measured in UTF-8; the undelivered
|
|
2431
|
+
queue — the events that arrived while a carrier write was pending or a page was
|
|
2428
2432
|
loading, the SSE text or port frame as it will go on the wire — holds
|
|
2429
|
-
at most `queue.events` frames and `queue.bytes` bytes
|
|
2433
|
+
at most `queue.events` frames and `queue.bytes` UTF-8 bytes (the JSON
|
|
2434
|
+
encoding of a port frame), each charged
|
|
2430
2435
|
until its write settled. When the next event would cross either bound
|
|
2431
2436
|
the stream ends with the terminal `error` event `JC2096` (`kind:
|
|
2432
2437
|
network`, retryable — the consumer reads slower than the source
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jarenjs/contract",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.73.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.js",
|
|
7
7
|
"types": "./dist/types/index.d.ts",
|
|
@@ -102,9 +102,9 @@
|
|
|
102
102
|
"prepack": "npm run build:types"
|
|
103
103
|
},
|
|
104
104
|
"dependencies": {
|
|
105
|
-
"@jarenjs/core": "^0.
|
|
106
|
-
"@jarenjs/json": "^0.
|
|
107
|
-
"@jarenjs/validate": "^0.
|
|
108
|
-
"@jarenjs/emit": "^0.
|
|
105
|
+
"@jarenjs/core": "^0.73.0",
|
|
106
|
+
"@jarenjs/json": "^0.73.0",
|
|
107
|
+
"@jarenjs/validate": "^0.73.0",
|
|
108
|
+
"@jarenjs/emit": "^0.73.0"
|
|
109
109
|
}
|
|
110
110
|
}
|
package/src/ledger.js
CHANGED
|
@@ -162,12 +162,14 @@ export function createMemoryLedger(options = {}) {
|
|
|
162
162
|
/**
|
|
163
163
|
* The started record a ref settles, or the `JC1011` refusal.
|
|
164
164
|
* @param {unknown} ref
|
|
165
|
+
* @param {number} at - The settlement instant, also used for updatedAt
|
|
165
166
|
* @returns {LedgerRecord}
|
|
166
167
|
*/
|
|
167
|
-
function settling(ref) {
|
|
168
|
+
function settling(ref, at) {
|
|
168
169
|
const r = /** @type {any} */ (ref);
|
|
169
170
|
const record = r !== null && typeof r === 'object' && typeof r.id === 'string' ? records.get(r.id) : undefined;
|
|
170
|
-
if (record === undefined || record.generation !== r.generation || record.status !== 'started'
|
|
171
|
+
if (record === undefined || record.generation !== r.generation || record.status !== 'started'
|
|
172
|
+
|| record.expiresAt <= at) throw staleSettlement(ref);
|
|
171
173
|
return record;
|
|
172
174
|
}
|
|
173
175
|
|
|
@@ -194,18 +196,20 @@ export function createMemoryLedger(options = {}) {
|
|
|
194
196
|
return { state: 'new', ref: Object.freeze({ id, generation }) };
|
|
195
197
|
},
|
|
196
198
|
commit(ref, response, now = undefined) {
|
|
197
|
-
const
|
|
199
|
+
const at = instant(now);
|
|
200
|
+
const record = settling(ref, at);
|
|
198
201
|
record.status = 'committed';
|
|
199
202
|
record.response = response;
|
|
200
203
|
record.retryable = null;
|
|
201
|
-
record.updatedAt =
|
|
204
|
+
record.updatedAt = at;
|
|
202
205
|
},
|
|
203
206
|
fail(ref, retryable, response, now = undefined) {
|
|
204
|
-
const
|
|
207
|
+
const at = instant(now);
|
|
208
|
+
const record = settling(ref, at);
|
|
205
209
|
record.status = 'failed';
|
|
206
210
|
record.retryable = retryable === true;
|
|
207
211
|
record.response = response === undefined ? null : response;
|
|
208
|
-
record.updatedAt =
|
|
212
|
+
record.updatedAt = at;
|
|
209
213
|
},
|
|
210
214
|
lookup({ op, scope, key, now = undefined }) {
|
|
211
215
|
const record = records.get(ledgerId(op, scope, key));
|
package/src/port/serve.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { compileMessageCatalog } from '@jarenjs/core/message';
|
|
24
|
+
import { utf8ByteLength } from '@jarenjs/core/string';
|
|
24
25
|
|
|
25
26
|
import { ContractHostError, ContractFailure } from '../errors.js';
|
|
26
27
|
import { validateOperationInput, settleOperation, safeTrace, PORT_LOCAL_ERRORS, classifyDeclared } from '../pipeline.js';
|
|
@@ -503,8 +504,8 @@ export function servePort(contract, handlers, options) {
|
|
|
503
504
|
return pushFrame(id, 'error', seq, wireError(code, renderMessage(catalog, row.msgid, { op: opId }), trace, undefined, row.retryable));
|
|
504
505
|
},
|
|
505
506
|
end: (reason, seq) => pushFrame(id, 'end', seq, { reason }),
|
|
506
|
-
// the
|
|
507
|
-
size: (frame) => JSON.stringify(frame)
|
|
507
|
+
// Account for the UTF-8 encoding of the posted frame's JSON text.
|
|
508
|
+
size: (frame) => utf8ByteLength(JSON.stringify(frame)),
|
|
508
509
|
write: (frame) => post(frame, pushCtx),
|
|
509
510
|
done: () => {
|
|
510
511
|
streams.delete(id);
|
package/src/stream/server.js
CHANGED
|
@@ -53,6 +53,7 @@
|
|
|
53
53
|
import { toPromise, isThenable } from '@jarenjs/core/function';
|
|
54
54
|
import { createAwaitedSink } from '@jarenjs/core/async';
|
|
55
55
|
import { isJsonValue } from '@jarenjs/core/object';
|
|
56
|
+
import { utf8ByteLength } from '@jarenjs/core/string';
|
|
56
57
|
|
|
57
58
|
import { verdict } from '../http/wire.js';
|
|
58
59
|
|
|
@@ -337,7 +338,7 @@ function pageFault(page, after, bounds) {
|
|
|
337
338
|
if (item.kind !== 'patch') return `replay item ${i} is not { patch, seq }`;
|
|
338
339
|
if (item.seq <= last) return `replay item ${i} does not advance the seq (${item.seq} after ${last})`;
|
|
339
340
|
last = item.seq;
|
|
340
|
-
bytes += JSON.stringify(item.patch)
|
|
341
|
+
bytes += utf8ByteLength(JSON.stringify(item.patch));
|
|
341
342
|
if (bytes > bounds.maxBytes) return `a replay page holds ${bytes} patch bytes over the ${bounds.maxBytes} asked for`;
|
|
342
343
|
}
|
|
343
344
|
if (p.next !== undefined && p.items.length > 0 && p.next < last) return 'a replay page\'s next is below its last item';
|
|
@@ -617,7 +618,7 @@ export function runSubscription(route, sub, hooks, options) {
|
|
|
617
618
|
return;
|
|
618
619
|
}
|
|
619
620
|
if (item.seq <= lastSeq && lastSeq !== 0) return; // already delivered, or reflected by a snapshot
|
|
620
|
-
if (maxPatchBytes !== null && JSON.stringify(item.patch)
|
|
621
|
+
if (maxPatchBytes !== null && utf8ByteLength(JSON.stringify(item.patch)) > maxPatchBytes) {
|
|
621
622
|
// the consumer swaps its document instead of patching it (§18.1)
|
|
622
623
|
emitSnapshot(item.seq, { reset: false, earliestAvailable: null, highWatermark: null });
|
|
623
624
|
return;
|