Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/calendar/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,11 +327,11 @@ the user's own key-value store. It never writes an event: every decision about
the calendar is made in the app, where the contacts, the settings and the
recurrence engine are.

The subscription targets the worker only. A persistent subscription prefers a
connected browser tab, but only if that tab registered the handler in the call
that created the subscription — which a later page load has not — so adding the
socket target would stall each delivery behind two lease periods whenever the
calendar happened to be open.
The subscription is a queue (`queue: true`), so each message is handled once
and retried if the run never finishes or fails for a temporary reason; the
handler throws with `retry: true` for those, and swallows anything else. The
page never registers the handler in-page, so an open calendar tab declines each
delivery and the events worker runs it right away.

**Only people you have already dealt with.** An invitation is added
automatically when the sender is *authenticated* — delivered inside Puter, or
Expand Down
12 changes: 6 additions & 6 deletions src/calendar/src/js/mailbox.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*
* - the capability probe + batched permission request,
* - subscribing (or adopting an existing subscription) to
* `fs:~/.mail/objects/**:add` with `targets:['worker']`,
* `fs:~/.mail/objects/**:add` as a `queue` subscription,
* - `cal:mail:cfg` (mode/subId/watermark/…),
* - the watermark rescan (via `puter.email.list`), which runs the SAME
* handler source in-page (via `new Function`) so a foreground session or
Expand Down Expand Up @@ -177,8 +177,7 @@ export function createMailbox({
if (existing) return { subId: existing.id || existing.subId || '' };
const sub = await p.events.onPersistent({
subject: MAIL_SUBJECT,
delivery: 'single',
targets: ['worker'],
queue: true,
handlerName: HANDLER_NAME,
context: { v: 1, self: selfEmail(), user: username() },
});
Expand Down Expand Up @@ -250,8 +249,6 @@ export function createMailbox({
} catch {
return enterForeground('Background permission was declined.');
}
} else if (code === 'invalid_targets') {
return enterForeground('No app identity is available here.');
} else {
lastError = errorMessage(err) || 'Could not request permission.';
state = 'off';
Expand All @@ -260,6 +257,10 @@ export function createMailbox({
}
}

// Without an app there is no events worker to run the handler, and the
// server accepts the subscription anyway, so check here.
if (!getPuter().appID) return enterForeground('No app identity is available here.');

try {
return await subscribeAndScan();
} catch (err) {
Expand All @@ -279,7 +280,6 @@ export function createMailbox({
return status();
}
}
if (code === 'invalid_targets') return enterForeground('No app identity is available here.');
// The consent check happens at subscribe, so this surfaces here rather
// than out of perms.request.
if (code === 'events_background_consent_required') {
Expand Down
5 changes: 4 additions & 1 deletion src/calendar/src/workers/mail-handler.js
Original file line number Diff line number Diff line change
Expand Up @@ -564,7 +564,10 @@ async function calendarMailHandler({ event, ctx, user, fetch, ack }) {
if (n % 20 === 0) await trimRecords();
} catch (err) {
if (recordWritten) return; // the record landed — ack, don't retry
if (isTransientError(err)) throw err; // let the lease retry
// Only an error marked temporary is retried; any other throw drops the event.
if (isTransientError(err)) {
throw Object.assign(new Error((err && err.message) || String(err)), { retry: true, cause: err });
}
// Never terminal: anything else the handler can't use is ignored.
try {
await markSeen();
Expand Down
4 changes: 2 additions & 2 deletions src/calendar/tests/mail-handler.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,11 +307,11 @@ describe('mail-handler: non-calendar mail and daily/scan gaps', () => {
expect([...kv.map.keys()].some((k) => k.startsWith('cal:mail:seen:'))).toBe(true);
});

it('a transient `get` failure (5xx) is rethrown so the lease retries, and nothing is marked seen', async () => {
it('a transient `get` failure (5xx) is rethrown with retry: true, and nothing is marked seen', async () => {
const kv = makeKv();
const id = makeId();
const user = makeUser({ kv, getErr: Object.assign(new Error('server exploded'), { code: 500 }) });
await expect(run(loadHandler(), user, addEvent(id))).rejects.toThrow();
await expect(run(loadHandler(), user, addEvent(id))).rejects.toMatchObject({ retry: true, message: 'server exploded' });
expect([...kv.map.keys()].some((k) => k.startsWith('cal:mail:seen:'))).toBe(false);
});

Expand Down
14 changes: 8 additions & 6 deletions src/calendar/tests/mailbox.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ describe('mailbox: available()', () => {
});

describe('mailbox: connect() happy path', () => {
it('sends one batched perms request, then onPersistent with targets:[worker]', async () => {
it('sends one batched perms request, then a queue onPersistent', async () => {
const { mailbox, kv, puter } = makeMailbox();
const status = await mailbox.connect();

Expand All @@ -107,10 +107,11 @@ describe('mailbox: connect() happy path', () => {
expect(puter.events.onPersistent).toHaveBeenCalledTimes(1);
expect(puter.events.onPersistent.mock.calls[0][0]).toMatchObject({
subject: MAIL_SUBJECT,
delivery: 'single',
targets: ['worker'],
queue: true,
handlerName: HANDLER_NAME,
});
expect(puter.events.onPersistent.mock.calls[0][0]).not.toHaveProperty('targets');
expect(puter.events.onPersistent.mock.calls[0][0]).not.toHaveProperty('delivery');
expect(status.mode).toBe('background');
expect(status.state).toBe('ok');
expect(status.subId).toBe('sub-1');
Expand Down Expand Up @@ -158,13 +159,14 @@ describe('mailbox: connect() error mapping', () => {
expect(status.lastError).toMatch(/full Puter account/i);
});

it('invalid_targets (no app identity) -> foreground mode', async () => {
const err = Object.assign(new Error('no'), { code: 'invalid_targets' });
const puter = makePuter({ events: { onPersistent: vi.fn(async () => { throw err; }) } });
it('no app identity -> foreground mode, without subscribing', async () => {
const puter = makePuter();
delete puter.appID;
const { mailbox } = makeMailbox({ puter });
const status = await mailbox.connect();
expect(status.mode).toBe('foreground');
expect(status.state).toBe('foreground');
expect(puter.events.onPersistent).not.toHaveBeenCalled();
});

// puter.perms.request RESOLVES falsy on denial — it does not throw. Modelling
Expand Down
Loading