runInNewWorkspace Service
The runInNewWorkspace
service provides workspace isolation by executing functions in completely fresh workspace contexts. It's essentially a convenient wrapper around Workspace.run()
that creates a new, isolated environment for function execution without affecting the current workspace state.
Interface
this.runInNewWorkspace(fn)
fn
(AsyncFunction
): The function to execute in a new workspace context- Returns: Promise resolving to the return value of the function
Function Signature
await this.runInNewWorkspace(async function() {
// Function executes in a fresh workspace context
// `this` refers to the new workspace instance
// Has access to all services: this.database, this.sendMail, etc.
return result;
});
Description
The runInNewWorkspace
service provides complete workspace isolation by:
- Creates fresh workspace contexts with completely isolated state and resources
- Preserves service availability ensuring the new workspace has access to all services
- Manages context lifecycle with automatic cleanup when the function completes
- Enables safe concurrent operations by preventing state conflicts between contexts
- Isolates database connections and other shared resources per workspace
- Handles errors gracefully with proper resource cleanup even on failures
The service is particularly useful for:
- Background task execution that shouldn't affect the current request context
- Database locking scenarios where operations need separate connection pools
- Email sending or other I/O operations that can run independently
- Parallel processing where multiple operations need isolated state
- User notification systems that process data independently of user actions
- Testing scenarios where clean state isolation is required
Key Features
Complete Isolation
- Each function execution gets a fresh workspace instance
- No shared state between the current workspace and the new one
- Separate database connections and resource pools
- Independent service instances
Resource Management
- Automatic resource cleanup when function completes
- Proper error isolation - failures don't affect calling context
- Memory management with context destruction after execution
Service Availability
- All services are available in the new workspace context
- Database, email, configuration, and custom services work normally
- Service instances are fresh but fully configured
Examples
Background User Notifications
// Send notifications without blocking the main request
export default {
async success({ id }) {
// Trigger notifications in background without affecting response
this.notifyUsers({ commentId: id, currentUserId: await this.user.id });
return this.renderRedirect({ target: '_top' });
},
async notifyUsers({ commentId, currentUserId }) {
await this.runInNewWorkspace(async function() {
// Runs in isolated context - won't affect main request
const comment = await this.database.comments.where({ id: commentId }).first();
if (!comment) return;
const users = await this.database.users.where({
idNe: currentUserId,
role: 'admin'
}).all();
for (const user of users) {
await user.notify(({ line }) => {
line(`New comment: ${comment.body}`);
});
}
});
}
};
Email Sending in Background
// Send welcome email without blocking sign-in flow
export default {
async signInUser(email) {
const user = await this.database.users.where({ email }).first();
// Send welcome email in background
this.runInNewWorkspace(({ sendMail }) => sendMail({
to: email,
subject: 'Welcome!',
text({ line }) {
line('Welcome to our platform!');
}
}));
// Continue with main flow immediately
return this.renderRedirect({ url: '/dashboard' });
}
};
Database Locking with Isolation
// Prevent database lock conflicts in tests
test('database.lock with workspace isolation', () => Workspace.run(async function() {
await Promise.all([
// Lock in current workspace
this.database.lock(async () => {
await new Promise(resolve => setTimeout(resolve, 100));
}),
// Lock in separate workspace - won't conflict
this.runInNewWorkspace(async function() {
await this.database.lock(async () => {
await new Promise(resolve => setTimeout(resolve, 100));
});
})
]);
}));
Multi-User Processing
// Process subscriber notifications independently
export default {
async notifySubscribers() {
const { id: postId, _url: baseUrl } = this.params;
await this.runInNewWorkspace(async function() {
const post = await this.database.posts.where({ id: postId }).first();
if (!post) return;
const { title, slug } = post;
const url = new URL(`/${slug}`, baseUrl);
// Process subscribers in batches
let page = 1;
while (true) {
const users = await this.database.users
.where({ subscriptions: { tier: ['free', 'paid'] } })
.paginate(page, 100)
.all();
if (users.length === 0) break;
for (const user of users) {
await user.notify(({ line }) => {
line(`New post: "${title}"`);
line(`Read at: ${url}`);
});
}
page++;
}
});
}
};
Parallel Data Processing
// Process multiple data streams concurrently
export default {
async processUserData(userIds) {
// Process each user in isolated workspace to prevent conflicts
const results = await Promise.all(
userIds.map(userId =>
this.runInNewWorkspace(async function() {
const user = await this.database.users.where({ id: userId }).first();
// Complex processing that might conflict if shared
await this.runCommand('generate-user-report', { userId });
await this.runCommand('update-user-metrics', { userId });
return { userId, processed: true };
})
)
);
return results;
}
};
Testing with Clean State
// Each test gets completely isolated database state
import { Workspace } from 'pinstripe';
test('user creation', () => Workspace.run(async function() {
// Main test workspace
const user1 = await this.database.users.insert({ name: 'User 1' });
// Isolated test - won't see user1
await this.runInNewWorkspace(async function() {
const count = await this.database.users.count(); // 0
const user2 = await this.database.users.insert({ name: 'User 2' });
// user2 won't be visible in main workspace
});
// Back in main workspace - only sees user1
const mainCount = await this.database.users.count(); // 1
}));
Implementation Details
The service is implemented as a simple wrapper around the Workspace.run()
method:
// packages/pinstripe/lib/services/run_in_new_workspace.js
import { Workspace } from '../workspace.js';
const runInNewWorkspace = fn => Workspace.run(fn);
export default {
meta(){
this.addToClient(); // Available in browser contexts
},
create(){
return runInNewWorkspace;
}
};
Underlying Workspace.run() Process
- Import Resolution: Ensures all modules are loaded with
importAll()
- Context Creation: Creates a new
Context
instance for resource management - Workspace Instantiation: Creates a new workspace instance with the context
- Function Execution: Calls the provided function with the workspace as
this
- Resource Cleanup: Automatically destroys the context and cleans up resources
Context Lifecycle
// Simplified internal flow
async run(fn) {
await importAll();
return await Context.new().run(async context => {
const workspace = this.new(context);
try {
return await fn.call(workspace, workspace);
} finally {
// Context.run() handles cleanup automatically
await context.destroy();
}
});
}
Use Cases
Background Processing
- Email delivery without blocking user responses
- User notifications that can be processed asynchronously
- Data exports or report generation
- Image processing or file uploads
Concurrency Control
- Database locking scenarios where separate connections prevent conflicts
- Multi-tenant processing where each tenant needs isolated context
- Parallel operations that might interfere with shared state
- Resource-intensive operations that need dedicated connections
Testing Scenarios
- Test isolation ensuring tests don't affect each other
- Database state management with clean slate for each test case
- Service mocking where different tests need different service configurations
- Integration testing with controlled environment setup
Development Tools
- REPL experimentation where you want to test without affecting current state
- Database migrations that need isolated transaction contexts
- Development utilities that manipulate data independently
Performance Considerations
Resource Overhead
- Each workspace creates new service instances and database connections
- Context creation has overhead - use judiciously for truly isolated operations
- Consider connection pool limits when running many concurrent workspaces
Memory Management
- Workspaces are automatically cleaned up after function completion
- Long-running functions should be mindful of memory usage
- Large data processing should consider streaming or batching
Database Connections
- Each workspace gets separate database connections
- Connection pools may limit concurrent workspace count
- Database locks work independently across workspaces
Related Services
runCommand
- Executes commands in forked contexts (lighter isolation)runBackgroundJob
- Executes background jobs using workspace isolation internallydatabase
- Benefits from workspace isolation for connection managementbot
- Uses workspace isolation for scheduled job execution
Best Practices
Use for True Isolation Needs
Only use runInNewWorkspace
when you need complete isolation:
// Good: Background processing that shouldn't affect request
await this.runInNewWorkspace(async function() {
await this.sendMail({ to: user.email, subject: 'Welcome!' });
});
// Overkill: Simple data access that doesn't need isolation
const user = await this.database.users.where({ id }).first();
Error Handling
Wrap calls in try-catch when you need to handle failures:
try {
await this.runInNewWorkspace(async function() {
await this.processLargeDataset();
});
} catch (error) {
console.error('Background processing failed:', error);
// Handle gracefully without affecting main flow
}
Resource Awareness
Be mindful of resource usage with concurrent workspaces:
// Good: Process in controlled batches
const batches = chunk(userIds, 10);
for (const batch of batches) {
await Promise.all(batch.map(userId =>
this.runInNewWorkspace(async function() {
await this.processUser(userId);
})
));
}
// Problematic: May exhaust connection pool
await Promise.all(allUserIds.map(userId =>
this.runInNewWorkspace(async function() {
await this.processUser(userId);
})
));
Context Awareness
Remember that workspaces are completely isolated:
// Variables from parent context are not automatically available
const currentUser = await this.user;
await this.runInNewWorkspace(async function() {
// currentUser is not available here
// Must re-fetch or pass as parameter
const user = await this.database.users.where({ id: currentUser.id }).first();
});
Client-Side Availability
The service is marked with addToClient()
, making it available in browser environments for client-side workspace isolation needs.