* fix: withdrawing background consent revokes the app's events session A background handler runs as a worker session for the subscriber and app. Revoking `events:background` or uninstalling the app suspended the subscriptions but left that session valid, so a token a handler had copied out kept working until the user found the row in the sessions list. The revocation settle now revokes the session too; the next consented delivery mints a fresh one. * fix: cleanup docs
8.1 KiB
title, description, platforms
| title | description | platforms | |||
|---|---|---|---|---|---|
| puter.events.onLocal() | Subscribe to changes on a file, directory, or key-value key for as long as this client is connected. |
|
Subscribes to a subject and calls handler every time something matching it changes. The subscription belongs to this client's connection: nothing is stored, nothing runs while the page is closed, and it ends when the connection does. See Events for the subject grammar and the event shape.
Not for a Puter worker: a worker invocation is short-lived, so a subscription here only lasts as long as that one invocation. To react to changes from a worker, use onPersistent() with a worker target and a published handler.
Syntax
puter.events.onLocal(subject, handler)
puter.events.onLocal(subject, handler, options)
Parameters
subject (String) (required)
What to watch: fs:<path or uid>[:<op>] or kv:<key>.
For fs:, the path may be absolute (/alice/Documents) or home-relative (~/Documents), may name something that does not exist yet, and may contain * (within a path segment) or ** (across directories). The optional op is one of add, write, move, remove, meta — nothing emits meta yet.
For kv:, the key is matched exactly unless you end it with *, which widens it to a prefix — the opposite of puter.kv.list(), whose pattern is always a prefix. Two segments (kv:cart) means the app you are running as; three or more (kv:<appId>:<key>) names the app explicitly and is what a key containing : needs.
handler (Function) (required)
Called with a single { event } object per delivery. event.op === 'gap' means events were dropped against a limit and the details are not available — re-read what you are watching. A handler that throws is reported on the console and does not end the subscription.
options (Object) (optional)
onError(Function): Called with{ message, code }if the subscription lapses — the connection was lost and re-subscribing failed. The subscription is over at that point; callonLocal()again to resume. Without it, a lapse is reported on the console.timeout(Number): How long to wait for the server to confirm the subscription, in milliseconds. Defaults to30000.
Return value
A Promise that resolves, once the server has confirmed the subscription, to a subscription object:
subId(String | null): The server's id for the subscription. It changes whenever the connection is rebuilt, so don't store anything against it.subject(String): The subject you subscribed with, returned fully qualified — akv:subject you wrote in the two-segment form comes back askv:<appId>:<key>.anchor(Object): The subscription's anchor, as{ uid, path }. For akv:subject,uidis the app whose store is being watched andpathis the key prefix it is anchored at; for one made through a share handle,uidis the handle andpathis empty. The path is the one the anchor had when you subscribed — a later rename or move does not update it.match(String | null): The pattern events under the anchor are matched against, if the subject had one.op(String | null): The single operation this subscription is limited to, ornullfor all of them.off(Function): Ends the subscription — seesubscription.off().
The promise rejects with { message, code }:
code |
Meaning |
|---|---|
invalid_subject |
The subject is not a non-empty string, or the server could not parse it. |
invalid_handler |
handler is not a function. |
invalid_subject_op |
The :op suffix is not one of the five operations. |
invalid_subject_pattern |
The match pattern is past its bounds: 256 characters, 16 segments, one * per segment, one ** in total. |
invalid_kv_pattern |
A kv: subject has a * somewhere other than the end, or a ?. |
invalid_kv_handle_key |
A kv:<handle>:… subject names no key, or one that tries to leave the handle's granted region. |
events_cross_app_disabled |
The subject names another app's key-value data and that is not enabled here. |
forbidden |
The target app does not share its data, or this app has not been granted app-data:<appId>:kv:read on it. |
subject_does_not_exist |
The subject is not there, or this account cannot read it. |
events_subscription_limit |
This connection already holds the maximum number of subscriptions. |
too_many_requests |
Over the subscribe/unsubscribe call budget. |
events_disabled |
Events are not enabled on this server. |
reauth_required |
The session backing this connection is no longer valid. |
events_connection_failed |
The events connection could not be established, the server did not answer in time, or the server closed the connection. |
events_failed |
The server answered with something the SDK could not make sense of. |
Examples
Watch a directory and print what changes
<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 — the handler runs
await puter.fs.write(`${dir}/hello.txt`, 'Hello!');
// (4) Stop listening (cleanup)
setTimeout(() => sub.off(), 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>
Watch this app's key-value store
<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: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>