5.8 KiB
Meteor 3.0 Migration Guide
Reference document capturing patterns, constraints, and lessons learned during the async migration of WeKan from Meteor 2.16 toward Meteor 3.0 readiness.
1. Dual-Compatibility Strategy
WeKan runs on Meteor 2.16 with Blaze 2.x. The goal is dual compatibility: changes must work on 2.16 now and remain compatible with a future Meteor 3.0 upgrade.
Key constraint: Blaze 2.x does NOT support async template helpers. Client-side code must receive synchronous data.
2. ReactiveCache Facade Pattern
ReactiveCache dispatches to ReactiveCacheServer (async MongoDB) or ReactiveCacheClient (sync Minimongo).
Rule: Facade methods must NOT be async. They return a Promise on the server and data on the client. Server callers await; client code uses the return value directly.
// CORRECT:
getBoard(boardId) {
if (Meteor.isServer) {
return ReactiveCacheServer.getBoard(boardId); // Returns Promise
} else {
return ReactiveCacheClient.getBoard(boardId); // Returns data
}
}
// WRONG:
async getBoard(boardId) { ... } // Wraps client return in Promise too!
3. Model Helpers (Collection.helpers)
Model helpers defined via Collection.helpers({}) are used by Blaze templates. They must NOT be async.
// CORRECT:
Cards.helpers({
board() {
return ReactiveCache.getBoard(this.boardId); // Promise on server, data on client
},
});
// WRONG:
Cards.helpers({
async board() { // Blaze gets Promise instead of data
return await ReactiveCache.getBoard(this.boardId);
},
});
Server-side callers of these helpers must await the result:
// In a Meteor method or hook (server-only):
const board = await card.board();
4. Allow/Deny Callbacks Must Be Synchronous
Meteor 2.x evaluates allow/deny callbacks synchronously. An async callback returns a Promise:
- allow callback returning Promise (truthy) → always passes
- deny callback returning Promise (truthy) → always denies
Rule: Never use async in allow/deny. Replace ReactiveCache calls with direct sync Mongo calls.
// CORRECT:
Cards.allow({
insert(userId, doc) {
return allowIsBoardMemberWithWriteAccess(userId, Boards.findOne(doc.boardId));
},
fetch: ['boardId'],
});
// WRONG:
Cards.allow({
async insert(userId, doc) {
return allowIsBoardMemberWithWriteAccess(userId, await ReactiveCache.getBoard(doc.boardId));
},
});
Sync alternatives for common patterns:
| Async (broken in allow/deny) | Sync replacement |
|---|---|
await ReactiveCache.getBoard(id) |
Boards.findOne(id) |
await ReactiveCache.getCard(id) |
Cards.findOne(id) |
await ReactiveCache.getCurrentUser() |
Meteor.users.findOne(userId) |
await ReactiveCache.getBoards({...}) |
Boards.find({...}).fetch() |
await card.board() |
Boards.findOne(card.boardId) |
Note: These sync Mongo calls (findOne, find().fetch()) are available in Meteor 2.x. In Meteor 3.0, they will be replaced by findOneAsync / find().fetchAsync(), which will require allow/deny callbacks to be reworked again (or replaced by Meteor 3.0's new permission model).
5. Server-Only Code CAN Be Async
Code that runs exclusively on the server can safely use async/await:
Meteor.methods({})— method bodiesMeteor.publish()— publication functionsJsonRoutes.add()— REST API handlersCollection.before.*/Collection.after.*— collection hooks (viamatb33:collection-hooks)- Standalone server functions
Meteor.methods({
async createCard(data) {
const board = await ReactiveCache.getBoard(data.boardId); // OK
// ...
},
});
6. forEach with await Anti-Pattern
Array.forEach() does not handle async callbacks — iterations run concurrently without awaiting.
// WRONG:
items.forEach(async (item) => {
await processItem(item); // Runs all in parallel, not sequentially
});
// CORRECT:
for (const item of items) {
await processItem(item); // Runs sequentially
}
7. Client-Side Collection Updates
Meteor requires client-side collection updates to use _id as the selector:
// CORRECT:
Lists.updateAsync(listId, { $set: { title: newTitle } });
// WRONG - fails with "Untrusted code may only update documents by ID":
Lists.updateAsync({ _id: listId, boardId: boardId }, { $set: { title: newTitle } });
8. Sync Meteor 2.x APIs to Convert for 3.0
These Meteor 2.x sync APIs will need conversion when upgrading to Meteor 3.0:
| Meteor 2.x (sync) | Meteor 3.0 (async) |
|---|---|
Collection.findOne() |
Collection.findOneAsync() |
Collection.find().fetch() |
Collection.find().fetchAsync() |
Collection.insert() |
Collection.insertAsync() |
Collection.update() |
Collection.updateAsync() |
Collection.remove() |
Collection.removeAsync() |
Collection.upsert() |
Collection.upsertAsync() |
Meteor.user() |
Meteor.userAsync() |
Meteor.userId() |
Remains sync |
Current status: Server-side code already uses async patterns via ReactiveCache. The sync findOne() calls in allow/deny callbacks will need to be addressed when Meteor 3.0's allow/deny system supports async (or is replaced).
9. Files Reference
Key files involved in the async migration:
| File | Role |
|---|---|
imports/reactiveCache.js |
ReactiveCache facade + Server/Client/Index implementations |
server/lib/utils.js |
Permission helper functions (allowIsBoardMember*) |
models/*.js |
Collection schemas, helpers, allow/deny, hooks, methods |
server/publications/*.js |
Meteor publications |
server/rulesHelper.js |
Rule trigger/action evaluation |
server/cronMigrationManager.js |
Cron-based migration jobs |