mirror of
https://github.com/HeyPuter/puter.git
synced 2026-11-05 13:35:20 +00:00
* feat(events): simplify onPersistent delivery
Replace delivery/targets with a boolean single. Without single, every
event runs the app's published handler in its events worker and is also
sent to every connected client; the server no longer routes between the
two. single keeps leasing each event to one consumer that acks.
delivery: 'single' | 'broadcast' is still accepted and targets is
ignored. Stored rows are read by their delivery column alone.
* docs(events): compare onLocal, onPersistent and single
* feat(events): rename single to queue
onPersistent({ queue: true }) is the work-queue mode: each event goes to
one consumer at a time and is offered again until acknowledged.
delivery: 'single' is still accepted as an alias, and subscription
objects report queue instead of single.
The docs explain the three modes (onLocal, default onPersistent, queue)
and each events recipe says which one it uses and why.
* docs(events): retries are the only repeat for a queued event
* feat(events): queue deliveries fall through to the worker on decline
A queued delivery gets one socket attempt under a 15 s lease instead of
two 60 s ones. A client with no handler for it, or whose handler fails,
sends events.nack and the worker takes it without waiting for the lease.
Declines are relayed to the region holding the lease like acks are, and
a decline for a lease that is no longer the client's is ignored.
* feat(events): drop thrown errors, retry only temporary ones
A queued handler that throws now drops the event and counts one failure
toward the pause. Network errors, timeouts, rate limits, 5xx responses
and errors thrown with retry: true are retried with the existing
backoff. One classifier is shared by puter.js and the events worker
runtime, which inlines it at build time. A dropped event is billed once,
like a completed one. ack() remains as an optional early release.
* docs(events): queued events reach every tab with a handler
40 lines
2.0 KiB
JavaScript
40 lines
2.0 KiB
JavaScript
import { describe, expect, it } from 'vitest';
|
|
import { isRetryableError } from './retryable.js';
|
|
|
|
const withFields = (fields, ErrorType = Error) =>
|
|
Object.assign(new ErrorType(fields.message ?? 'boom'), fields);
|
|
|
|
describe('which handler failures are retried', () => {
|
|
it.each([
|
|
['an error marked retry: true', withFields({ retry: true })],
|
|
['a 429', withFields({ status: 429 })],
|
|
['a 503', withFields({ status: 503 })],
|
|
['a 500 as statusCode', withFields({ statusCode: 500 })],
|
|
['a puter.js network error', withFields({ code: 'network_error' })],
|
|
['a puter.js request timeout', withFields({ code: 'request_timeout' })],
|
|
['a rate limit', withFields({ code: 'too_many_requests' })],
|
|
['a dropped events connection', withFields({ code: 'events_connection_failed' })],
|
|
['a reset socket', withFields({ code: 'ECONNRESET' })],
|
|
['an abort signal timeout', withFields({ name: 'TimeoutError' })],
|
|
['a browser fetch failure', new TypeError('Failed to fetch')],
|
|
['a node fetch failure', new TypeError('fetch failed')],
|
|
['a fetch network error', new TypeError('NetworkError when attempting to fetch resource.')],
|
|
])('retries %s', (_label, error) => {
|
|
expect(isRetryableError(error)).toBe(true);
|
|
});
|
|
|
|
it.each([
|
|
['a plain error', new Error('bad input')],
|
|
['a 400', withFields({ status: 400 })],
|
|
['a 404', withFields({ status: 404 })],
|
|
['an unrelated TypeError', new TypeError('x is not a function')],
|
|
['an unknown code', withFields({ code: 'subject_does_not_exist' })],
|
|
['a thrown string', 'nope'],
|
|
['undefined', undefined],
|
|
['terminal: true, even on a 503', withFields({ terminal: true, status: 503 })],
|
|
['events_terminal, even with retry: true', withFields({ code: 'events_terminal', retry: true })],
|
|
])('drops %s', (_label, error) => {
|
|
expect(isRetryableError(error)).toBe(false);
|
|
});
|
|
});
|