Events

Websites Puter Apps Node.js Workers

The Events API is in beta. Event shapes, limits, and behavior may change between releases.

The Events API tells your app when a user's data changes. Subscribe to a file, a folder, a path that doesn't exist yet, a key in your app's key-value store, key-value data another user shared with you, or the user's notifications, and your handler runs each time it changes. You don't poll and you don't run a server.

There are two kinds of subscription:

  • Session subscriptions (onLocal()) belong to the open page and end when it closes. Use them to keep a UI in sync.
  • Persistent subscriptions (onPersistent()) are stored on the account and keep running while your app is closed. Each change runs a handler your app published. Use them for background work, such as processing uploads as they arrive.

What you watch is written as a subject string:

Subject Watches
fs:~/Documents Everything under a folder, or one file by path or uid
fs:~/inbox/*.json:add Matching paths, one kind of change
kv:cart One key in your app's key-value store (kv:cart* for every key starting with cart)
kv:<handle>:* Keys another user shared with you
notif:app-user Your app's notifications for the user

Subjects and events has the full subject syntax and the shape of each event.

With the User-Pays Model, deliveries are billed to the user holding the subscription, not to you. See Rate Limits and Quotas for limits and costs.

Features

Watch a Folder
Watch a Key
Wait for a File
Run in the Background
List Subscriptions
Catch Up
<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
        (async () => {
            // (1) Create a directory to watch
            const dir = puter.randName();
            await puter.fs.mkdir(dir);

            // (2) Subscribe to everything under it
            const sub = await puter.events.onLocal(`fs:${dir}`, ({ event }) => {
                if (event.op === 'gap') {
                    puter.print(`missed some changes (${event.reason})<br>`);
                    return;
                }
                puter.print(`${event.op}: ${event.path}<br>`);
            });

            // (3) Change something, and the handler runs
            await puter.fs.write(`${dir}/hello.txt`, 'Hello!');

            // (4) Stop listening (cleanup)
            setTimeout(() => sub.off(), 2000);
        })();
    </script>
</body>
</html>

Watch a key-value key and a key prefix

<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
        (async () => {
            // (1) Exactly one key, and separately every key under a prefix.
            const one = await puter.events.onLocal('kv:cart', ({ event }) =>
                puter.print(`${event.op}: ${event.key}<br>`));
            const many = await puter.events.onLocal(
                `kv:${puter.appID}:cart:*`,
                ({ event }) => puter.print(`under cart: ${event.key}<br>`));

            // (2) `cart` reaches the first, `cart:items` only the second.
            await puter.kv.set('cart', { total: 0 });
            await puter.kv.set('cart:items', ['apple']);

            // (3) Cleanup
            setTimeout(async () => {
                await one.off();
                await many.off();
                await puter.kv.del('cart');
                await puter.kv.del('cart:items');
            }, 2000);
        })();
    </script>
</body>
</html>

React to a file that does not exist yet

<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
        (async () => {
            // (1) A directory to work in. `inbox/` below it does not exist yet.
            const dir = puter.randName();
            await puter.fs.mkdir(dir);

            // (2) Subscribe anyway. The subscription anchors on `dir` and
            //     matches the rest of the path as it appears.
            const sub = await puter.events.onLocal(
                `fs:${dir}/inbox/trigger.json:add`,
                ({ event }) => puter.print(`appeared: ${event.path}<br>`),
                { onError: (error) => puter.print(`subscription ended: ${error.code}<br>`) },
            );
            puter.print(`anchored on ${sub.anchor.path}, matching ${sub.match}<br>`);

            // (3) Create it, several directories deep
            await puter.fs.write(`${dir}/inbox/trigger.json`, '{}', {
                createMissingParents: true,
            });

            setTimeout(() => sub.off(), 2000);
        })();
    </script>
</body>
</html>

Keep watching with a handler after the page closes

Publishing a handler needs an app you own, so this runs in your own app rather than in the playground.

// Once, as the app's developer
await puter.events.handlers.publish('ingestUpload', async ({ event, ctx }) => {
    await fetch(ctx.endpoint, { method: 'POST', body: event.path });
});

// In your app, for each user
await puter.perms.request('backgroundEvents');
const sub = await puter.events.onPersistent({
    subject: 'fs:inbox',
    handlerName: 'ingestUpload',
    context: { endpoint: 'https://example.com/ingest' },
});

// It outlives the page. End it when it's no longer needed.
await puter.events.unsubscribe(sub.subId);

List the persistent subscriptions this account holds

<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
        (async () => {
            const dir = puter.randName();
            await puter.fs.mkdir(dir);
            const sub = await puter.events.onPersistent({
                subject: `fs:${dir}`,
                context: { label: 'inbox' },
            });

            for (const row of await puter.events.list()) {
                puter.print(`${row.subject}: ${row.queue ? 'queue' : 'every listener'}`);
                puter.print(` (context: ${row.contextKeys?.join(', ') ?? 'none'})<br>`);
            }

            await puter.events.unsubscribe(sub.subId);
        })();
    </script>
</body>
</html>

Read the notifications missed while the app was closed

<html>
<body>
    <script src="https://js.puter.com/v2/"></script>
    <script>
        (async () => {
            let cursor;
            let seen = 0;
            do {
                const page = await puter.events.fetch({
                    subject: 'notif:app-user',
                    cursor,
                });
                for (const event of page.items) {
                    puter.print(`${event.type}: ${event.notification.title ?? ''}<br>`);
                    seen++;
                }
                cursor = page.cursor;
            } while (cursor);

            if (!seen) puter.print('nothing missed<br>');
        })();
    </script>
</body>
</html>

Functions

These Events features are supported out of the box when using Puter.js:

Examples

You can see various Puter.js Events features in action from the following examples:

Recipes