GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — fix: accept transmit envelopes without arguments · cardmagic/solid-objects-js@acdcba5

File tree

Expand file treeCollapse file tree
Open diff view settings
Filter options
  • docs
    • api.md
    • browser-protocol.md
  • src
    • transmit.ts
  • test
    • transmit.test.ts
Expand file treeCollapse file tree
Open diff view settings
Collapse file

‎docs/api.md‎

Copy file name to clipboardExpand all lines: docs/api.md
+19-8Lines changed: 19 additions & 8 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -445,28 +445,39 @@ outbox with at-least-once delivery, per-actor order, and retry backoff.
445445
browser runtime a generous `maxAttempts`; an effect that exhausts its
446446
attempts during a long offline period lands in dead letters, and
447447
`runtime.deadLetters.retry` re-queues it.
448-
- `receiveTransmitEnvelope(options)`: idempotent server ingest. It enqueues an
448+
- `receiveTransmitEnvelope(options)`: idempotent server ingest. `arguments`
449+
is optional in the envelope and defaults to an empty object, matching the
450+
staging side and the Ruby ingest. It enqueues an
449451
internal message with `transmit:<effectId>` as the idempotency key, so a
450452
replayed envelope applies once. The host must authenticate the sender
451453
before this call; internal delivery skips `authorizeMessage`. The call
452454
belongs inside whatever route the host application gives the transmit
453455
callback to post to:
454456

455457
```typescript
456-
import { receiveTransmitEnvelope } from "solid-objects"
458+
import { IdempotencyConflict, InvalidPayload, receiveTransmitEnvelope } from "solid-objects"
457459

458460
async function handleSyncRoute(request: Request): Promise<Response> {
459461
const sender = await authenticate(request)
460462
if (!sender) return new Response("Forbidden", { status: 403 })
461-
const envelope = await request.json()
462-
await receiveTransmitEnvelope({ runtime, envelope })
463-
return Response.json({})
463+
try {
464+
await receiveTransmitEnvelope({ runtime, envelope: await request.json() })
465+
return Response.json({})
466+
} catch (error) {
467+
if (error instanceof InvalidPayload || error instanceof IdempotencyConflict) {
468+
return new Response(null, { status: 422 })
469+
}
470+
throw error
471+
}
464472
}
465473
```
466474

467-
The example uses a Fetch-style handler; any HTTP framework works, and a
468-
422 response for a rejected envelope tells the browser side to stop
469-
retrying that effect.
475+
The example uses a Fetch-style handler; any HTTP framework works. The 422
476+
matters: it tells the sending outbox to dead-letter the effect instead of
477+
retrying it. `InvalidPayload` marks a malformed envelope, and
478+
`IdempotencyConflict` marks a replay whose arguments changed; both are
479+
permanently unappliable, and a 500 would make the outbox retry them
480+
forever.
470481

471482
- Per-actor order comes from an ordered drain: a claimed transmit effect
472483
transmits every undelivered envelope for its actor up to its own mailbox
Collapse file

‎docs/browser-protocol.md‎

Copy file name to clipboardExpand all lines: docs/browser-protocol.md
+3-2Lines changed: 3 additions & 2 deletions
  • Display the source diff
  • Display the rich diff
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,10 @@ authentication, so the trust boundary is the origin.
129129

130130
## Sync envelope
131131

132-
`solid-objects/transmit` transmits one JSON envelope per staged sync
132+
`solid-objects/transmit` transmits one JSON envelope per staged transmit
133133
effect: `effectId`, target `actorType` and `actorId`, `operation`, and an
134-
`arguments` object. The transport belongs to the host application; the
134+
optional `arguments` object that defaults to an empty object on both
135+
ingests. The transport belongs to the host application; the
135136
Playwright suite posts envelopes over `fetch`. The server calls
136137
`receiveTransmitEnvelope`, which enqueues an internal message with
137138
`transmit:<effectId>` as the idempotency key, so a replayed envelope applies once.
Collapse file

‎src/transmit.ts‎

Copy file name to clipboardExpand all lines: src/transmit.ts
+5-4Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,18 @@ export async function receiveTransmitEnvelope(options: {
4747
const { envelope } = options
4848
for (const field of ["effectId", "actorType", "actorId", "operation"] as const) {
4949
if (typeof envelope[field] !== "string" || envelope[field].length === 0) {
50-
throw new InvalidPayload(`sync envelope requires a non-empty ${field}`)
50+
throw new InvalidPayload(`transmit envelope requires a non-empty ${field}`)
5151
}
5252
}
53-
if (!isJsonObject(envelope.arguments)) {
54-
throw new InvalidPayload("sync envelope arguments must be a JSON object")
53+
const argumentsValue = envelope.arguments ?? {}
54+
if (!isJsonObject(argumentsValue)) {
55+
throw new InvalidPayload("transmit envelope arguments must be a JSON object")
5556
}
5657
const message = await options.runtime.enqueueInternalMessage({
5758
actorType: envelope.actorType,
5859
actorId: envelope.actorId,
5960
operation: envelope.operation,
60-
argumentsValue: envelope.arguments,
61+
argumentsValue,
6162
idempotencyKey: `transmit:${envelope.effectId}`,
6263
})
6364
return { messageId: message.id }
Collapse file

‎test/transmit.test.ts‎

Copy file name to clipboardExpand all lines: test/transmit.test.ts
+50Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { afterEach, describe, expect, it } from "vitest"
22
import "../src/platform/node.js"
33
import { Actor } from "../src/actor.js"
4+
import { IdempotencyConflict } from "../src/errors.js"
45
import { createRuntime, type SolidObjectsRuntime } from "../src/runtime.js"
56
import type { SolidObjectsConfiguration } from "../src/configuration.js"
67
import { sqlite } from "../src/database/sqlite.js"
@@ -253,6 +254,55 @@ describe("sync bridge", () => {
253254
})
254255
})
255256

257+
it("accepts an envelope without arguments, matching the Ruby ingest", async () => {
258+
const server = testRuntime()
259+
await server.install()
260+
server.register(ServerTransmitCounter)
261+
262+
await receiveTransmitEnvelope({
263+
runtime: server,
264+
envelope: {
265+
effectId: "effect-no-arguments",
266+
actorType: "TransmitCounter",
267+
actorId: "defaulted",
268+
operation: "increment",
269+
} as never,
270+
})
271+
await server.testing.drain({ roles: ["actors"] })
272+
273+
expect(await server.ref(ServerTransmitCounter, "defaulted").snapshot()).toEqual({
274+
count: 1,
275+
applied: [1],
276+
})
277+
})
278+
279+
it("raises IdempotencyConflict when a replay changes the arguments", async () => {
280+
const server = testRuntime()
281+
await server.install()
282+
server.register(ServerTransmitCounter)
283+
const envelope = {
284+
effectId: "effect-conflict",
285+
actorType: "TransmitCounter",
286+
actorId: "conflicted",
287+
operation: "increment",
288+
arguments: { amount: 1 },
289+
}
290+
291+
await receiveTransmitEnvelope({ runtime: server, envelope })
292+
await expect(
293+
receiveTransmitEnvelope({
294+
runtime: server,
295+
envelope: { ...envelope, arguments: { amount: 2 } },
296+
}),
297+
).rejects.toBeInstanceOf(IdempotencyConflict)
298+
await server.testing.drain({ roles: ["actors"] })
299+
300+
expect(await server.ref(ServerTransmitCounter, "conflicted").snapshot()).toEqual({
301+
count: 1,
302+
applied: [1],
303+
})
304+
})
305+
256306
it("rejects a malformed envelope on the server", async () => {
257307
const server = testRuntime()
258308
await server.install()

0 commit comments

Comments
0 (0)
You are visitor 01402520 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.