Working with Distributed Data Structures

user guidesddsdata-structuressharedmap

Working with Distributed Data Structures

Distributed Data Structures (DDSes) are the foundation of Fluid Framework applications. They provide collaborative data types that automatically synchronize changes across all connected clients while handling conflicts and maintaining consistency.

What are Distributed Data Structures?

DDSes are special data types that:

  • Synchronize automatically across all clients
  • Handle conflicts when multiple users edit simultaneously
  • Maintain consistency through operational transformation
  • Work offline and sync when reconnected
  • Provide familiar APIs similar to native JavaScript data structures

Core DDS Types

SharedMap

A collaborative key-value store similar to JavaScript Map:

import { SharedMap } from "@fluidframework/map";

// In your container schema
const containerSchema = {
    initialObjects: {
        myMap: SharedMap,
    },
};

// Usage
const myMap = container.initialObjects.myMap;

// Set values
myMap.set("username", "alice");
myMap.set("score", 100);

// Get values
const username = myMap.get("username"); // "alice"
const score = myMap.get("score");       // 100

// Listen for changes
myMap.on("valueChanged", (changed) => {
    console.log(`Key "${changed.key}" changed to: ${myMap.get(changed.key)}`);
});

// Check if key exists
if (myMap.has("username")) {
    // Key exists
}

// Delete keys
myMap.delete("score");

// Iterate over entries
for (const [key, value] of myMap) {
    console.log(`${key}: ${value}`);
}

SharedString

A collaborative string for text editing:

import { SharedString } from "@fluidframework/sequence";

const containerSchema = {
    initialObjects: {
        text: SharedString,
    },
};

const text = container.initialObjects.text;

// Insert text at position
text.insertText(0, "Hello, ");
text.insertText(7, "World!");

// Get the full text
console.log(text.getText()); // "Hello, World!"

// Insert at specific position
text.insertText(5, " beautiful");
console.log(text.getText()); // "Hello beautiful, World!"

// Remove text
text.removeText(5, 10); // Remove " beautiful"
console.log(text.getText()); // "Hello, World!"

// Listen for changes
text.on("sequenceDelta", (delta) => {
    console.log("Text changed:", delta);
});

// Get text in range
const substring = text.getText(0, 5); // "Hello"

// Format text (if using rich text features)
text.annotateRange(0, 5, { bold: true });

SharedNumberSequence

A collaborative array of numbers:

import { SharedNumberSequence } from "@fluidframework/sequence";

const containerSchema = {
    initialObjects: {
        numbers: SharedNumberSequence,
    },
};

const numbers = container.initialObjects.numbers;

// Insert numbers
numbers.insert(0, [1, 2, 3]);
numbers.insert(3, [4, 5]);

// Get items
console.log(numbers.getItems(0, 5)); // [1, 2, 3, 4, 5]

// Remove items
numbers.remove(1, 2); // Remove 2 items starting at index 1

// Listen for changes
numbers.on("sequenceDelta", (delta) => {
    console.log("Sequence changed:", delta);
});

SharedObjectSequence

A collaborative array that can hold any serializable objects:

import { SharedObjectSequence } from "@fluidframework/sequence";

const containerSchema = {
    initialObjects: {
        tasks: SharedObjectSequence,
    },
};

const tasks = container.initialObjects.tasks;

// Insert objects
tasks.insert(0, [
    { id: 1, title: "Buy groceries", completed: false },
    { id: 2, title: "Walk the dog", completed: true }
]);

// Get items
const allTasks = tasks.getItems(0, tasks.getLength());

// Update an item (create new object, don't mutate)
const taskToUpdate = tasks.getItems(0, 1)[0];
const updatedTask = { ...taskToUpdate, completed: true };
tasks.remove(0, 1);
tasks.insert(0, [updatedTask]);

// Listen for changes
tasks.on("sequenceDelta", (delta) => {
    // Update UI when tasks change
    renderTasks();
});

Working with Nested Structures

Maps of Maps

const containerSchema = {
    initialObjects: {
        users: SharedMap, // Map of user ID to user data maps
    },
};

const users = container.initialObjects.users;

// Create a new user
const userId = "user123";
const userData = new SharedMap();
userData.set("name", "Alice");
userData.set("email", "alice@example.com");

users.set(userId, userData.handle);

// Access nested data
const userHandle = users.get(userId);
if (userHandle) {
    const userData = await userHandle.get();
    const userName = userData.get("name");
}

Complex Data Modeling

// Model a collaborative document
const containerSchema = {
    initialObjects: {
        document: SharedMap,
        content: SharedString,
        comments: SharedObjectSequence,
        metadata: SharedMap,
    },
};

// Document structure
const document = container.initialObjects.document;
const content = container.initialObjects.content;
const comments = container.initialObjects.comments;
const metadata = container.initialObjects.metadata;

// Set document metadata
metadata.set("title", "My Document");
metadata.set("created", new Date().toISOString());
metadata.set("author", "alice");

// Add content
content.insertText(0, "# My Document\n\nThis is the content...");

// Add comments linked to text positions
comments.insert(0, [{
    id: "comment1",
    position: { start: 0, end: 5 },
    text: "Great title!",
    author: "bob",
    timestamp: new Date().toISOString()
}]);

Best Practices

Performance Optimization

Performance Tips

Follow these patterns for optimal performance with large datasets and frequent updates.

// Batch operations when possible
const map = container.initialObjects.myMap;

// Instead of multiple individual sets:
// map.set("a", 1);
// map.set("b", 2);
// map.set("c", 3);

// Batch them:
map.set("a", 1);
map.set("b", 2);
map.set("c", 3);

// For sequences, use batch inserts
const sequence = container.initialObjects.mySequence;
sequence.insert(0, [item1, item2, item3]); // Better than multiple inserts

Event Handling

// Listen for specific changes
myMap.on("valueChanged", (changed, local) => {
    if (!local) {
        // Change came from another client
        console.log(`Remote change: ${changed.key} = ${myMap.get(changed.key)}`);
    }
});

// Listen for all operations on sequences
mySequence.on("sequenceDelta", (delta, target) => {
    for (const op of delta.deltaSegments) {
        if (op.segment.cachedLength > 0) {
            console.log("Items inserted:", op.segment.items);
        }
    }
});

// Cleanup listeners when component unmounts
const removeListener = myMap.on("valueChanged", handler);
// Later...
removeListener();

Error Handling

try {
    // DDS operations can fail in certain scenarios
    myMap.set("key", "value");
} catch (error) {
    console.error("Failed to set value:", error);
    // Handle the error appropriately
}

// Check container connection status
if (container.connectionState === ConnectionState.Connected) {
    // Safe to perform operations
    myMap.set("key", "value");
} else {
    // Queue operations or show offline state
    console.log("Container is disconnected");
}

Data Validation

// Validate data before setting
function setUserData(userId: string, userData: any) {
    if (!userId || typeof userId !== "string") {
        throw new Error("Invalid user ID");
    }

    if (!userData.name || !userData.email) {
        throw new Error("Missing required user data");
    }

    users.set(userId, userData);
}

// Use TypeScript for better type safety
interface UserData {
    name: string;
    email: string;
    joinDate: string;
}

const typedMap = myMap as SharedMap<UserData>;

Advanced Patterns

Conditional Updates

// Update only if current value matches expectation
function incrementCounter(expectedValue: number) {
    const current = counterMap.get("count");
    if (current === expectedValue) {
        counterMap.set("count", current + 1);
        return true;
    }
    return false; // Someone else modified it
}

Versioning Data

// Include version numbers for conflict resolution
interface VersionedData {
    value: any;
    version: number;
    lastModified: string;
}

function updateWithVersion(key: string, newValue: any) {
    const current = myMap.get(key) as VersionedData || { version: 0 };
    const updated: VersionedData = {
        value: newValue,
        version: current.version + 1,
        lastModified: new Date().toISOString()
    };
    myMap.set(key, updated);
}

Creating Custom DDS

For specialized use cases, you can create custom distributed data structures:

// This is an advanced topic - see the API documentation
// for details on implementing custom DDSes

Common Patterns

Presence Tracking

// Track online users
const presence = container.initialObjects.presence;
const userId = getCurrentUserId();

// Join
presence.set(userId, {
    name: "Alice",
    joinedAt: Date.now(),
    cursor: { x: 0, y: 0 }
});

// Update cursor position
function updateCursor(x: number, y: number) {
    const current = presence.get(userId);
    if (current) {
        presence.set(userId, { ...current, cursor: { x, y } });
    }
}

// Leave (cleanup)
window.addEventListener("beforeunload", () => {
    presence.delete(userId);
});

Undo/Redo System

// Simple command pattern for undo/redo
interface Command {
    execute(): void;
    undo(): void;
}

class SetValueCommand implements Command {
    constructor(
        private map: SharedMap,
        private key: string,
        private newValue: any,
        private oldValue: any
    ) {}

    execute() {
        this.map.set(this.key, this.newValue);
    }

    undo() {
        if (this.oldValue !== undefined) {
            this.map.set(this.key, this.oldValue);
        } else {
            this.map.delete(this.key);
        }
    }
}

// Usage
const history: Command[] = [];
let historyIndex = -1;

function executeCommand(command: Command) {
    command.execute();
    history.length = historyIndex + 1; // Clear forward history
    history.push(command);
    historyIndex++;
}

function undo() {
    if (historyIndex >= 0) {
        history[historyIndex].undo();
        historyIndex--;
    }
}

function redo() {
    if (historyIndex < history.length - 1) {
        historyIndex++;
        history[historyIndex].execute();
    }
}

Testing DDS Code

// Unit testing with Fluid Framework test utilities
import { MockFluidDataStoreRuntime } from "@fluidframework/test-runtime-utils";

describe("SharedMap tests", () => {
    let map: SharedMap;

    beforeEach(() => {
        const runtime = new MockFluidDataStoreRuntime();
        map = new SharedMap("map", runtime, SharedMapFactory.Attributes);
    });

    it("should set and get values", () => {
        map.set("key", "value");
        expect(map.get("key")).toBe("value");
    });

    it("should emit events on changes", () => {
        const listener = jest.fn();
        map.on("valueChanged", listener);

        map.set("key", "value");

        expect(listener).toHaveBeenCalledWith(
            expect.objectContaining({ key: "key" }),
            true // local change
        );
    });
});

Next Steps


DDS Comparison

DDS TypeUse CaseJavaScript EquivalentKey Features
SharedMapKey-value storageMapFast lookups, event-driven
SharedStringText editingStringOperational transform, rich text
SharedNumberSequenceNumeric arraysArray<number>Efficient for numbers
SharedObjectSequenceObject arraysArray<object>General-purpose arrays

Choose the right DDS based on your data access patterns and collaboration requirements.

Last updated: September 17, 2024