Server: Collections
Collections let you define and expose dynamic groups of records. These groups update automatically when underlying records change, and clients receive diffs of added or removed members.
This is ideal for collaborative apps, dashboards, filtered views, and pagination.
Expose collections
To make a collection subscribable, use exposeCollection(...)
:
.("collection:all-tasks", async () => {
return await .("task:*");
});
listRecordsMatching
is a function that takes a Redis glob pattern, and an optional callback (more on that below), and returns the records that match the pattern. For example:
await .("task:1", { : 1 });
await .("task:2", { : 2 });
await .("task:3", { : 3 });
const = await .("task:*");
.();
The above would log:
[
{ taskid: 1, id: 'task:1' },
{ taskid: 2, id: 'task:2' },
{ taskid: 3, id: 'task:3' }
]
The second argument is a resolver that must return an array of records (not just IDs) for the given collection. The resolver runs:
- Immediately when a client subscribes
- Again any time a record changes (added, removed, or updated)
Collections can use dynamic patterns too:
.(
/^collection:user:(\d+):tasks$/,
async (: , : string) => {
const = .(":")[2];
return await .(`user:${}:task:*`);
}
);
Pagination example
You can implement pagination by encoding page numbers in collection names, and making use of the slice
, sort
, and map
options:
.(
/^tasks:page:\d+$/,
async (: , : string) => {
const = (.(":")[2]);
const = 10;
return await .("task:*", {
// Transform records for the client (optional - exclude heavy fields)
: () => ({
: .id,
: .title,
: .status,
: .created_at
}),
// Sort by created_at descending (newest first)
: (, ) => new (.created_at).() - new (.created_at).(),
// Paginate based on extracted page number
: {
: ( - 1) * ,
:
},
});
}
);
Client-side pagination:
Client-side pagination works by subscribing to a single page at a time:
// Subscribe to page 1
await .("tasks:page:1", {
: ({ , , }) => {
//
}
});
// Unsubscribe from page 1, then subscribe to page 2
await .("tasks:page:1");
await .("tasks:page:2");
Per-connection isolation
The resolver runs for each subscribed connection individually, which enables:
- Connection-specific authorization - each user sees only what they should
- Different views of the same collection - based on user permissions/context
- Proper isolation - each connection maintains its own version and state
.("collection:user-tasks", async (: , : string) => {
// Each connection gets its own resolver call
const = .metadata?.userId;
const = .metadata?.role;
if ( === 'admin') {
// Admins see all tasks
return await .("task:*");
} else {
// Regular users see only their own tasks
return await .(`user:${}:task:*`);
}
});
Record changes trigger diffs
Whenever a record is added, removed, or updated, Mesh will:
- Rerun the resolver for each subscribed connection
- Compare new and old record IDs for each connection
- Send a diff to clients where membership changed
Diff format:
added
: Array of full record objects that were added to the collectionremoved
: Array of full record objects that were removed from the collectionchanged
: Array of full record objects that were changed in the collection
Resync logic
Mesh tracks a version
number per collection per connection, starting at version 1. If a client receives a diff with a skipped version (e.g. expected v3, got v5), it auto-resubscribes and fetches the full list again.
This ensures correctness across reconnects, network drops, or inter-instance propagation delays.
Flexible attribute-based filtering
You can expose collections that allow clients to filter records by arbitrary attributes, using a pattern that encodes the filter as part of the collection name. This is useful for building flexible search, filter, or view features.
For example, to let clients subscribe to filtered lists of products:
.(
/^collection:products:filter:.+$/,
async (: , : string) => {
const [, , , ] = .(":");
const = .(());
const = await .("product:*");
return .( =>
.().(([, ]) => [] === )
);
}
);
On the client, you encode your filter as a JSON object, then subscribe to the filtered collection:
const = (.({ : "books", : true }));
.(`collection:products:filter:${}`);
This pattern is flexible and can be extended to support multi-field filters, value arrays, partial matching, and more. For example, you could filter by { brand: "Acme", priceRange: [10, 50] }
or any other combination of product attributes.
Use cases
- Task lists: real-time filters across shared boards
- Projects: dynamic membership as records are created/deleted
- Views: multi-tenant views per user, team, or org
- Pagination: sort and slice large datasets into pages