Core Concepts
The Data API separates large binary data from your synchronized model state, providing efficient and secure file sharing capabilities.
- How It Works
- Security Model
Data flow overview
Copy
Ask AI
// 1. Upload data and get a handle
const dataHandle = await session.data.store(arrayBuffer);
// 2. Share the lightweight handle through events
this.publish("game", "asset-uploaded", { handle: dataHandle });
// 3. Other users fetch data using the handle
const data = await session.data.fetch(dataHandle);
// 4. Use the data (create images, audio, etc.)
const blob = new Blob([data], { type: "image/jpeg" });
const imageUrl = URL.createObjectURL(blob);
Benefits: Efficient network usage, automatic caching, end-to-end encryption, and cleaner model state.
Basic File Upload and Sharing
- Complete Example
- Audio Sharing
Image sharing application
Copy
Ask AI
class ImageSharingModel extends Multisynq.Model {
init(options, persisted) {
this.images = new Map();
this.nextImageId = 1;
// Restore from persistence
if (persisted) {
this.restoreFromPersistence(persisted);
}
this.subscribe("images", "upload", this.handleImageUpload);
this.subscribe("images", "delete", this.handleImageDelete);
}
handleImageUpload(data) {
const imageId = this.nextImageId++;
const imageInfo = {
id: imageId,
name: data.fileName,
type: data.fileType,
size: data.fileSize,
handle: data.handle,
uploader: data.userId,
uploadTime: this.now(),
tags: data.tags || [],
description: data.description || ""
};
this.images.set(imageId, imageInfo);
// Notify all users
this.publish("images", "image-added", imageInfo);
// Persist the session data
this.persistSession(this.saveForPersistence);
}
handleImageDelete(data) {
const image = this.images.get(data.imageId);
// Only uploader can delete
if (image && image.uploader === data.userId) {
this.images.delete(data.imageId);
this.publish("images", "image-deleted", {
imageId: data.imageId,
fileName: image.name
});
this.persistSession(this.saveForPersistence);
}
}
saveForPersistence() {
// Convert handles to persistable IDs
const imageData = {};
for (const [id, image] of this.images) {
imageData[id] = {
...image,
handleId: Multisynq.Data.toId(image.handle) // Convert to string
};
delete imageData[id].handle; // Remove non-serializable handle
}
return {
images: imageData,
nextImageId: this.nextImageId
};
}
restoreFromPersistence(saved) {
this.nextImageId = saved.nextImageId || 1;
// Restore images and recreate handles
for (const [id, imageData] of Object.entries(saved.images || {})) {
const image = {
...imageData,
handle: Multisynq.Data.fromId(imageData.handleId) // Recreate handle
};
delete imageData.handleId; // Remove the ID string
this.images.set(parseInt(id), image);
}
}
}
ImageSharingModel.register("ImageSharingModel");
class ImageSharingView extends Multisynq.View {
init() {
this.model = this.wellKnownModel("ImageSharingModel");
this.userId = this.viewId;
this.imageElements = new Map();
this.setupUI();
this.setupFileHandlers();
this.displayExistingImages();
// Subscribe to events
this.subscribe("images", "image-added", this.onImageAdded);
this.subscribe("images", "image-deleted", this.onImageDeleted);
}
setupUI() {
document.body.innerHTML = `
<div id="app">
<header>
<h1>Image Sharing Gallery</h1>
<div class="upload-area" id="upload-area">
<p>Drop images here or click to upload</p>
<input type="file" id="file-input" accept="image/*" multiple style="display: none;">
</div>
</header>
<main>
<div id="image-gallery" class="gallery"></div>
</main>
<div id="status" class="status"></div>
</div>
`;
// Style the application
const style = document.createElement('style');
style.textContent = `
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: Arial, sans-serif; background: #f5f5f5; }
#app { max-width: 1200px; margin: 0 auto; padding: 20px; }
header { margin-bottom: 30px; }
h1 { color: #333; margin-bottom: 20px; }
.upload-area {
border: 3px dashed #ccc; border-radius: 10px; padding: 40px;
text-align: center; cursor: pointer; transition: all 0.3s;
background: white;
}
.upload-area:hover { border-color: #007bff; background: #f8f9fa; }
.upload-area.dragover { border-color: #007bff; background: #e7f3ff; }
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 20px; }
.image-card {
background: white; border-radius: 10px; overflow: hidden;
box-shadow: 0 2px 10px rgba(0,0,0,0.1); transition: transform 0.2s;
}
.image-card:hover { transform: translateY(-5px); }
.image-container { width: 100%; height: 200px; overflow: hidden; }
.image-container img { width: 100%; height: 100%; object-fit: cover; }
.image-info { padding: 15px; }
.image-name { font-weight: bold; margin-bottom: 5px; }
.image-meta { color: #666; font-size: 12px; margin-bottom: 10px; }
.image-actions { display: flex; gap: 10px; }
.btn { padding: 8px 16px; border: none; border-radius: 5px; cursor: pointer; }
.btn-delete { background: #dc3545; color: white; }
.btn-download { background: #007bff; color: white; }
.status { position: fixed; bottom: 20px; right: 20px; padding: 10px 20px;
background: #333; color: white; border-radius: 5px; display: none; }
`;
document.head.appendChild(style);
}
setupFileHandlers() {
const uploadArea = document.getElementById('upload-area');
const fileInput = document.getElementById('file-input');
// Click to upload
uploadArea.addEventListener('click', () => fileInput.click());
// File input change
fileInput.addEventListener('change', (e) => {
this.handleFiles(Array.from(e.target.files));
fileInput.value = ''; // Reset for repeated uploads
});
// Drag and drop
uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
uploadArea.classList.add('dragover');
});
uploadArea.addEventListener('dragleave', () => {
uploadArea.classList.remove('dragover');
});
uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
uploadArea.classList.remove('dragover');
const files = Array.from(e.dataTransfer.files).filter(file =>
file.type.startsWith('image/')
);
if (files.length > 0) {
this.handleFiles(files);
} else {
this.showStatus('Please drop image files only', 'error');
}
});
}
async handleFiles(files) {
for (const file of files) {
await this.uploadFile(file);
}
}
async uploadFile(file) {
try {
this.showStatus(`Reading ${file.name}...`);
// Read file data
const arrayBuffer = await this.readFileAsArrayBuffer(file);
this.showStatus(`Uploading ${file.name} (${this.formatFileSize(arrayBuffer.byteLength)})...`);
// Store data and get handle
const handle = await this.session.data.store(arrayBuffer);
// Publish upload event
this.publish("images", "upload", {
fileName: file.name,
fileType: file.type,
fileSize: arrayBuffer.byteLength,
handle: handle,
userId: this.userId,
tags: [],
description: ""
});
this.showStatus(`${file.name} uploaded successfully!`, 'success');
} catch (error) {
console.error('Upload failed:', error);
this.showStatus(`Failed to upload ${file.name}: ${error.message}`, 'error');
}
}
readFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(new Error('Failed to read file'));
reader.readAsArrayBuffer(file);
});
}
displayExistingImages() {
// Display images that are already in the model
for (const [id, image] of this.model.images) {
this.onImageAdded(image);
}
}
async onImageAdded(imageInfo) {
try {
this.showStatus(`Loading ${imageInfo.name}...`);
// Fetch the image data
const data = await this.session.data.fetch(imageInfo.handle);
// Create blob and image URL
const blob = new Blob([data], { type: imageInfo.type });
const imageUrl = URL.createObjectURL(blob);
// Create image card
this.createImageCard(imageInfo, imageUrl);
this.showStatus(`${imageInfo.name} loaded`, 'success');
} catch (error) {
console.error('Failed to load image:', error);
this.showStatus(`Failed to load ${imageInfo.name}`, 'error');
}
}
createImageCard(imageInfo, imageUrl) {
const gallery = document.getElementById('image-gallery');
const card = document.createElement('div');
card.className = 'image-card';
card.id = `image-${imageInfo.id}`;
const uploadDate = new Date(imageInfo.uploadTime).toLocaleDateString();
const isOwner = imageInfo.uploader === this.userId;
card.innerHTML = `
<div class="image-container">
<img src="${imageUrl}" alt="${imageInfo.name}" loading="lazy">
</div>
<div class="image-info">
<div class="image-name">${imageInfo.name}</div>
<div class="image-meta">
${this.formatFileSize(imageInfo.size)} • ${uploadDate}
${imageInfo.description ? `<br>${imageInfo.description}` : ''}
</div>
<div class="image-actions">
<button class="btn btn-download" onclick="this.saveImage('${imageUrl}', '${imageInfo.name}')">
Download
</button>
${isOwner ? `
<button class="btn btn-delete" onclick="this.deleteImage(${imageInfo.id})">
Delete
</button>
` : ''}
</div>
</div>
`;
gallery.appendChild(card);
this.imageElements.set(imageInfo.id, { card, url: imageUrl });
}
onImageDeleted(data) {
const element = this.imageElements.get(data.imageId);
if (element) {
// Clean up blob URL
URL.revokeObjectURL(element.url);
// Remove from DOM
element.card.remove();
// Remove from tracking
this.imageElements.delete(data.imageId);
this.showStatus(`${data.fileName} deleted`, 'info');
}
}
deleteImage(imageId) {
if (confirm('Are you sure you want to delete this image?')) {
this.publish("images", "delete", {
imageId: imageId,
userId: this.userId
});
}
}
saveImage(url, filename) {
const a = document.createElement('a');
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
}
formatFileSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}
showStatus(message, type = 'info') {
const status = document.getElementById('status');
status.textContent = message;
status.className = `status ${type}`;
status.style.display = 'block';
// Auto-hide after 3 seconds
setTimeout(() => {
status.style.display = 'none';
}, 3000);
console.log(`[${type.toUpperCase()}] ${message}`);
}
}
ImageSharingView.register("ImageSharingView");
// Launch the application
document.addEventListener('DOMContentLoaded', () => {
Multisynq.Session.join({
apiKey: "your_api_key_here", // Get from multisynq.io/coder
appId: "com.example.image-sharing",
name: Multisynq.App.autoSession(),
password: Multisynq.App.autoPassword(),
model: ImageSharingModel,
view: ImageSharingView,
tps: 0 // No regular model updates needed
});
});
Persistence Integration
Data handles can be persisted and restored across sessions using
Data.toId()
and Data.fromId()
.- Persistent Data Handles
- Cross-Session Sharing
Converting handles for storage
Copy
Ask AI
class PersistentDataModel extends Multisynq.Model {
init(options, persisted) {
this.userDocuments = new Map();
this.sharedAssets = new Map();
// Restore from persistence
if (persisted) {
this.restoreFromPersistence(persisted);
}
this.subscribe("docs", "save", this.saveDocument);
this.subscribe("assets", "share", this.shareAsset);
}
saveDocument(data) {
const docInfo = {
id: data.docId,
name: data.fileName,
type: data.fileType,
size: data.fileSize,
handle: data.handle,
owner: data.userId,
lastModified: this.now(),
version: 1
};
this.userDocuments.set(data.docId, docInfo);
// Automatically persist when documents are saved
this.persistSession(this.saveForPersistence);
this.publish("docs", "document-saved", docInfo);
}
shareAsset(data) {
// Create shareable handle for cross-session sharing
const asset = this.userDocuments.get(data.docId);
if (asset && asset.owner === data.userId) {
this.sharedAssets.set(data.shareId, {
shareId: data.shareId,
docId: data.docId,
name: asset.name,
handle: asset.handle,
sharedBy: data.userId,
shareTime: this.now(),
permissions: data.permissions || ["read"]
});
this.persistSession(this.saveForPersistence);
this.publish("assets", "asset-shared", {
shareId: data.shareId,
name: asset.name
});
}
}
saveForPersistence() {
// Convert handles to persistable string IDs
const persistableData = {
documents: {},
sharedAssets: {}
};
// Save documents
for (const [id, doc] of this.userDocuments) {
persistableData.documents[id] = {
...doc,
handleId: Multisynq.Data.toId(doc.handle) // Convert to string
};
delete persistableData.documents[id].handle;
}
// Save shared assets
for (const [id, asset] of this.sharedAssets) {
persistableData.sharedAssets[id] = {
...asset,
handleId: Multisynq.Data.toId(asset.handle)
};
delete persistableData.sharedAssets[id].handle;
}
return persistableData;
}
restoreFromPersistence(saved) {
// Restore documents
for (const [id, docData] of Object.entries(saved.documents || {})) {
const doc = {
...docData,
handle: Multisynq.Data.fromId(docData.handleId) // Recreate handle
};
delete doc.handleId;
this.userDocuments.set(id, doc);
}
// Restore shared assets
for (const [id, assetData] of Object.entries(saved.sharedAssets || {})) {
const asset = {
...assetData,
handle: Multisynq.Data.fromId(assetData.handleId)
};
delete asset.handleId;
this.sharedAssets.set(id, asset);
}
}
}
PersistentDataModel.register("PersistentDataModel");
Best Practices
🏗️ Model-View Separation
Keep data operations in the view
Copy
Ask AI
// ✅ Good - Views handle data operations
class GameView extends Multisynq.View {
async uploadAsset(file) {
// View handles file I/O
const data = await this.readFile(file);
const handle = await this.session.data.store(data);
// Notify model with handle
this.publish("assets", "uploaded", { handle });
}
}
class GameModel extends Multisynq.Model {
handleAssetUpload(data) {
// Model stores handle metadata
this.assets.push({
id: this.nextId++,
handle: data.handle,
uploadTime: this.now()
});
}
}
🔒 Security Considerations
Handle data securely
Copy
Ask AI
// ✅ Validate and sanitize
class SecureDataView extends Multisynq.View {
async uploadFile(file) {
// Validate file type
if (!this.isAllowedType(file.type)) {
throw new Error('File type not allowed');
}
// Check file size
if (file.size > 10 * 1024 * 1024) { // 10MB limit
throw new Error('File too large');
}
// Sanitize filename
const safeName = this.sanitizeFilename(file.name);
const data = await this.readFileAsArrayBuffer(file);
const handle = await this.session.data.store(data);
this.publish("files", "upload", {
handle,
fileName: safeName,
fileSize: data.byteLength
});
}
isAllowedType(mimeType) {
const allowed = [
'image/jpeg', 'image/png', 'image/gif',
'audio/mpeg', 'audio/wav',
'text/plain', 'application/pdf'
];
return allowed.includes(mimeType);
}
sanitizeFilename(filename) {
return filename
.replace(/[^a-zA-Z0-9.-]/g, '_')
.substring(0, 255);
}
}
⚡ Performance Optimization
Optimize data handling
Copy
Ask AI
// ✅ Efficient data management
class OptimizedDataView extends Multisynq.View {
init() {
this.imageCache = new Map();
this.preloadQueue = [];
// Pre-load critical assets
this.preloadCriticalAssets();
}
async loadImage(imageHandle, priority = 'normal') {
// Check cache first
if (this.imageCache.has(imageHandle)) {
return this.imageCache.get(imageHandle);
}
// Queue for background loading
if (priority === 'low') {
this.preloadQueue.push(imageHandle);
this.processPreloadQueue();
return null;
}
// Load immediately
const data = await this.session.data.fetch(imageHandle);
const blob = new Blob([data]);
const url = URL.createObjectURL(blob);
// Cache the result
this.imageCache.set(imageHandle, url);
return url;
}
async processPreloadQueue() {
if (this.preloadQueue.length === 0) return;
// Process one item every 100ms to avoid blocking
const handle = this.preloadQueue.shift();
await this.loadImage(handle, 'normal');
setTimeout(() => this.processPreloadQueue(), 100);
}
// Clean up when view is destroyed
destroy() {
// Revoke all blob URLs to free memory
for (const url of this.imageCache.values()) {
URL.revokeObjectURL(url);
}
this.imageCache.clear();
}
}
📱 Offline Handling
Handle network issues gracefully
Copy
Ask AI
// ✅ Robust offline support
class OfflineDataView extends Multisynq.View {
init() {
this.uploadQueue = [];
this.isOnline = navigator.onLine;
window.addEventListener('online', () => {
this.isOnline = true;
this.processUploadQueue();
});
window.addEventListener('offline', () => {
this.isOnline = false;
});
}
async uploadFile(file) {
const fileData = await this.readFileAsArrayBuffer(file);
if (this.isOnline && this.session) {
return this.doUpload(file, fileData);
} else {
// Queue for later upload
this.uploadQueue.push({ file, fileData });
this.showMessage('Queued for upload when online');
}
}
async processUploadQueue() {
while (this.uploadQueue.length > 0 && this.isOnline && this.session) {
const { file, fileData } = this.uploadQueue.shift();
try {
await this.doUpload(file, fileData);
this.showMessage(`Uploaded ${file.name}`);
} catch (error) {
// Re-queue on failure
this.uploadQueue.unshift({ file, fileData });
console.error('Upload failed, re-queued:', error);
break;
}
}
}
async doUpload(file, fileData) {
const handle = await this.session.data.store(fileData);
this.publish("files", "upload", {
handle,
fileName: file.name,
fileType: file.type,
fileSize: fileData.byteLength
});
return handle;
}
}
Common Patterns
📄 Document Collaboration
📄 Document Collaboration
Copy
Ask AI
// Real-time document sharing with version control
class DocumentModel extends Multisynq.Model {
init() {
this.documents = new Map();
this.versions = new Map();
this.subscribe("docs", "create", this.createDocument);
this.subscribe("docs", "update", this.updateDocument);
}
createDocument(data) {
const doc = {
id: data.docId,
name: data.name,
handle: data.handle,
owner: data.userId,
created: this.now(),
currentVersion: 1,
collaborators: [data.userId]
};
this.documents.set(data.docId, doc);
this.versions.set(`${data.docId}_v1`, {
docId: data.docId,
version: 1,
handle: data.handle,
timestamp: this.now(),
author: data.userId
});
this.publish("docs", "document-created", doc);
}
updateDocument(data) {
const doc = this.documents.get(data.docId);
if (doc && doc.collaborators.includes(data.userId)) {
doc.currentVersion++;
doc.lastModified = this.now();
this.versions.set(`${data.docId}_v${doc.currentVersion}`, {
docId: data.docId,
version: doc.currentVersion,
handle: data.newHandle,
timestamp: this.now(),
author: data.userId,
changes: data.changes
});
this.publish("docs", "document-updated", {
docId: data.docId,
version: doc.currentVersion,
author: data.userId
});
}
}
}
🎵 Media Library
🎵 Media Library
Copy
Ask AI
// Music/podcast sharing platform
class MediaLibraryModel extends Multisynq.Model {
init() {
this.tracks = new Map();
this.playlists = new Map();
this.nowPlaying = null;
this.subscribe("media", "upload-track", this.addTrack);
this.subscribe("media", "create-playlist", this.createPlaylist);
this.subscribe("media", "play", this.playTrack);
}
addTrack(data) {
const track = {
id: data.trackId,
title: data.title,
artist: data.artist,
duration: data.duration,
handle: data.handle,
uploader: data.userId,
uploadTime: this.now(),
genre: data.genre || "Unknown",
playCount: 0
};
this.tracks.set(data.trackId, track);
this.publish("media", "track-added", track);
}
createPlaylist(data) {
const playlist = {
id: data.playlistId,
name: data.name,
creator: data.userId,
tracks: [],
isPublic: data.isPublic || false,
created: this.now()
};
this.playlists.set(data.playlistId, playlist);
this.publish("media", "playlist-created", playlist);
}
playTrack(data) {
const track = this.tracks.get(data.trackId);
if (track) {
track.playCount++;
this.nowPlaying = {
trackId: data.trackId,
startedBy: data.userId,
startTime: this.now()
};
this.publish("media", "now-playing", this.nowPlaying);
}
}
}
API Reference
📤 session.data.store()
📤 session.data.store()
Upload data and get a handleParameters:
Copy
Ask AI
// Basic usage
const handle = await session.data.store(arrayBuffer);
// With options
const handle = await session.data.store(arrayBuffer, {
keep: true, // Keep original ArrayBuffer (default: false)
shareable: true // Create cross-session handle (default: false)
});
// Example
const fileData = await file.arrayBuffer();
const handle = await session.data.store(fileData, { shareable: true });
arrayBuffer
: ArrayBuffer containing the data to storeoptions.keep
: Boolean - preserve original ArrayBuffer (transferred by default for efficiency)options.shareable
: Boolean - create handle that works across sessions
📥 session.data.fetch()
📥 session.data.fetch()
Download data using a handleParameters:
Copy
Ask AI
// Fetch data
const arrayBuffer = await session.data.fetch(handle);
// Use the data
const blob = new Blob([arrayBuffer], { type: 'image/jpeg' });
const url = URL.createObjectURL(blob);
handle
: Data handle returned fromstore()
🔗 Data.toId() / Data.fromId()
🔗 Data.toId() / Data.fromId()
Convert handles for persistencetoId(handle): Convert handle to persistable string
fromId(id): Recreate handle from string ID
Copy
Ask AI
// Convert handle to string for persistence
const handleId = Multisynq.Data.toId(handle);
// Recreate handle from string
const recreatedHandle = Multisynq.Data.fromId(handleId);
// Persistence example
class PersistentModel extends Multisynq.Model {
saveData() {
return {
imageHandleId: Multisynq.Data.toId(this.imageHandle)
};
}
restoreData(saved) {
this.imageHandle = Multisynq.Data.fromId(saved.imageHandleId);
}
}
Next Steps
Persistence
Learn how to persist data handles across sessions
Writing a Multisynq View
Master view patterns for handling user uploads
Multi-user Chat
See data sharing in action with file attachments
Simple Animation
Apply data techniques to animated content
The Data API enables powerful file sharing capabilities while maintaining security and performance. Use it for media content, documents, and any large binary data that would be inefficient to send through the event system. Remember to handle data operations in views and store only lightweight handles in your models.