@jarenjs/contract 0.72.0 → 0.72.3

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.
@@ -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`, and a
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, so a ref of an earlier claim matches no row
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
- settled(ref, settle.run('committed', JSON.stringify(response), null, at(now), (ref)?.id ?? '', (ref)?.generation ?? '').changes);
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
- settled(ref, settle.run('failed', response === undefined ? null : JSON.stringify(response), retryable === true ? 1 : 0, at(now), (ref)?.id ?? '', (ref)?.generation ?? '').changes);
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 queue — the
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, each charged
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.72.0",
4
+ "version": "0.72.3",
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.72.0",
106
- "@jarenjs/json": "^0.72.0",
107
- "@jarenjs/validate": "^0.72.0",
108
- "@jarenjs/emit": "^0.72.0"
105
+ "@jarenjs/core": "^0.72.3",
106
+ "@jarenjs/json": "^0.72.3",
107
+ "@jarenjs/validate": "^0.72.3",
108
+ "@jarenjs/emit": "^0.72.3"
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') throw staleSettlement(ref);
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 record = settling(ref);
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 = instant(now);
204
+ record.updatedAt = at;
202
205
  },
203
206
  fail(ref, retryable, response, now = undefined) {
204
- const record = settling(ref);
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 = instant(now);
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 byte account is the frame as posted: its JSON text
507
- size: (frame) => JSON.stringify(frame).length,
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);
@@ -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).length;
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).length > maxPatchBytes) {
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;