﻿{"id":835554,"date":"2026-02-18T21:20:26","date_gmt":"2026-02-18T21:20:26","guid":{"rendered":"https:\/\/gridnet.org\/wpp\/?p=835554"},"modified":"2026-02-20T07:57:40","modified_gmt":"2026-02-20T07:57:40","slug":"building-real-things-files-messages-and-webrtc-swarms","status":"publish","type":"post","link":"https:\/\/mag.gridnet.org\/index.php\/2026\/02\/18\/building-real-things-files-messages-and-webrtc-swarms\/","title":{"rendered":"Building Real Things \u2014 Files, Messages, and WebRTC Swarms"},"content":{"rendered":"<h2>Introduction: What &#8220;Building Real Things&#8221; Means on a Decentralized OS<\/h2>\n<p>In the previous articles of this series, you learned the fundamentals: how GRIDNET OS UI dApps are structured, how <code>CWindow<\/code> gives you an isolated Shadow DOM sandbox, how <code>CVMContext<\/code> connects your JavaScript to a blockchain-backed virtual machine, and how GridScript commands drive state transitions on the decentralized ledger. You built a &#8220;Hello Blockchain&#8221; dApp. You explored the CVMContext API in depth.<\/p>\n<p>Now comes the question that separates toy demos from real software: <strong>what can you actually build?<\/strong><\/p>\n<p>The answer is: quite a lot. GRIDNET OS provides three foundational capabilities that, combined, enable an entire class of applications that would traditionally require servers, databases, cloud storage, and messaging infrastructure:<\/p>\n<ol>\n<li><strong>A Decentralized File System<\/strong> \u2014 create directories, read and write files, navigate paths, commit changes to the blockchain. No Amazon S3. No Firebase. Your files live on the chain.<\/li>\n<li><strong>Peer-to-Peer Messaging<\/strong> \u2014 the built-in Messenger dApp demonstrates direct, encrypted, zero-server communication between browsers using WebRTC data channels. No WhatsApp backend. No Signal server.<\/li>\n<li><strong>WebRTC Swarms<\/strong> \u2014 create multi-peer mesh networks where browsers connect directly to each other for real-time data exchange, audio, video, and screen sharing. The signaling server (a GRIDNET full node) brokers the initial connection; after that, traffic flows peer-to-peer.<\/li>\n<\/ol>\n<p>This article is your comprehensive reference for all three. Every method signature is taken directly from the source code. Every example is tested against the actual implementation. By the end, you will be able to build a file manager, a messenger, or a collaborative real-time application \u2014 from scratch, on a decentralized OS, with no server of your own.<\/p>\n<h2>Part I: The Decentralized File System<\/h2>\n<figure><img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/building-real-things-filesystem-arch.svg\" alt=\"Decentralized File System request flow diagram\" style=\"width:100%; max-width:900px;\" \/><figcaption>The request flow: your dApp calls CFileSystem methods, which construct CDFSMsg datagrams, which CVMContext sends over WebSocket to the full node, which reads\/writes the blockchain state.<\/figcaption><\/figure>\n<h3>Architecture Overview<\/h3>\n<p>The decentralized file system in GRIDNET OS is not a POSIX file system. It is a <strong>blockchain-backed hierarchical data store<\/strong> with directory navigation, file creation, file reading, and atomic commits. Think of it as a version-controlled file system where every commit is a blockchain transaction \u2014 immutable, timestamped, and cryptographically secured.<\/p>\n<p>The file system is accessed through the <code>CFileSystem<\/code> singleton class, defined in <code>\/lib\/FileSystem.js<\/code>. You obtain it via:<\/p>\n<pre>\nconst fs = CVMContext.getInstance().getFileSystem;\n<\/pre>\n<p>Every file system operation follows the same pattern:<\/p>\n<ol>\n<li>You call a method on <code>CFileSystem<\/code> (e.g., <code>doCD()<\/code>, <code>doLS()<\/code>, <code>doNewFile()<\/code>).<\/li>\n<li>The method constructs a <code>CDFSMsg<\/code> \u2014 a DFS (Decentralized File System) message with the appropriate command type.<\/li>\n<li><code>CVMContext.sendDFSMsg()<\/code> serializes and sends the message over the WebSocket connection to the full node.<\/li>\n<li>The full node processes the command against the blockchain state.<\/li>\n<li>A response arrives asynchronously via the DFS message listener system.<\/li>\n<li>You receive results in your registered callback.<\/li>\n<\/ol>\n<p>The return value of every <code>CFileSystem<\/code> method is a <code>COperationStatus<\/code> object \u2014 not the file data itself. The actual data arrives asynchronously through listeners. This is the critical mental model: <strong>all DFS operations are fire-and-forget commands with async responses.<\/strong><\/p>\n<h3>Thread IDs: The Routing Key<\/h3>\n<p>Every file system operation takes an optional <code>threadID<\/code> parameter. This parameter determines <strong>which decentralized thread<\/strong> processes the command. GRIDNET OS uses two primary threads:<\/p>\n<ul>\n<li><code>'data'<\/code> \u2014 The <strong>data thread<\/strong>, used for file read\/write operations. This is the default for most <code>CFileSystem<\/code> methods.<\/li>\n<li><code>'system'<\/code> \u2014 The <strong>system thread<\/strong>, used for commit operations and state-changing transactions.<\/li>\n<\/ul>\n<p>The symbolic thread names (<code>'data'<\/code>, <code>'system'<\/code>) are resolved to actual <code>ArrayBuffer<\/code> thread identifiers by <code>CTools.getInstance().genericThreadNameToID(threadID)<\/code>. You can also pass the raw <code>ArrayBuffer<\/code> thread ID directly \u2014 for example, from <code>this.getThreadID<\/code> in your <code>CWindow<\/code> subclass, which returns the thread ID assigned to your dApp&#8217;s window.<\/p>\n<p>The File Manager dApp demonstrates this pattern throughout: it consistently passes <code>this.getThreadID<\/code> as the thread ID parameter, ensuring all operations are scoped to that window&#8217;s thread context:<\/p>\n<pre>\n\/\/ FileManager.js pattern \u2014 every operation uses the window's thread ID\nCVMContext.getInstance().getFileSystem.doCD(path, true, false, false, this.getThreadID);\nCVMContext.getInstance().getFileSystem.doLS(this.getThreadID);\nCVMContext.getInstance().getFileSystem.doNewFile(fileName, content, false, this.getThreadID);\nCVMContext.getInstance().getFileSystem.doGetFile(filePath, false, this.getThreadID);\nCVMContext.getInstance().getFileSystem.doNewDir(dirName, false, this.getThreadID);\n<\/pre>\n<h3>API Reference: CFileSystem Methods<\/h3>\n<h4><code>doCD(path, doLS, breakCommit, dontThrow, threadID)<\/code> \u2014 Change Directory<\/h4>\n<p>Navigates to a directory path on the decentralized file system.<\/p>\n<pre>\n\/**\n * @param {string}      path        \u2014 Directory path to navigate to (e.g., '\/', '\/myApp\/data')\n * @param {boolean}     doLS        \u2014 If true, performs an atomic LS after CD (default: true)\n * @param {boolean}     breakCommit \u2014 If true, breaks any pending commit lock (default: false)\n * @param {boolean}     dontThrow   \u2014 If true, suppresses fatal errors for missing dirs (default: false)\n * @param {string|ArrayBuffer} threadID \u2014 Thread to process on (default: 'data')\n * @returns {COperationStatus}      \u2014 Contains getReqID and getIsSuccess\n *\/\nconst fs = CVMContext.getInstance().getFileSystem;\nconst result = fs.doCD('\/myApp\/data', true, false, false, this.getThreadID);\n\nif (result.getIsSuccess) {\n  \/\/ Request sent successfully \u2014 await async response via DFS listener\n  this.addNetworkRequestID(result.getReqID);\n}\n<\/pre>\n<p>The <code>doLS<\/code> parameter is a powerful optimization: when <code>true<\/code>, the full node performs the directory change and listing as a <strong>single atomic operation<\/strong>, saving a round trip. The File Manager uses <code>doCD(path, true, ...)<\/code> almost exclusively \u2014 navigating and listing in one shot.<\/p>\n<p>The <code>dontThrow<\/code> parameter is important for defensive programming: normally, attempting to <code>cd<\/code> into a non-existent directory causes a fatal error on the VM. Setting <code>dontThrow = true<\/code> converts this into a soft error returned via <code>COperationResult<\/code>.<\/p>\n<p>Internally, <code>doCD<\/code> constructs a <code>CDFSMsg<\/code> with command type <code>eDFSCmdType.enterDirLS<\/code> (or <code>eDFSCmdType.enterDir<\/code> if <code>doLS<\/code> is false), sets the path as UTF-8 encoded data, and sends it via <code>CVMContext.sendDFSMsg()<\/code>.<\/p>\n<h4><code>doLS(threadID, breakCommit)<\/code> \u2014 List Directory<\/h4>\n<p>Lists the contents of the current directory.<\/p>\n<pre>\n\/**\n * @param {string|ArrayBuffer} threadID    \u2014 Thread to process on (default: 'data')\n * @param {boolean}            breakCommit \u2014 Break pending commit lock (default: false)\n * @returns {COperationStatus}\n *\/\nconst result = fs.doLS(this.getThreadID);\nthis.addNetworkRequestID(result.getReqID);\n<\/pre>\n<p>The directory listing arrives as a <code>CDFSMsg<\/code> response through your registered DFS message listener. The response contains a serialized representation of the directory&#8217;s files and subdirectories.<\/p>\n<h4><code>doNewFile(path, content, breakCommit, threadID)<\/code> \u2014 Create\/Write a File<\/h4>\n<p>Creates a new file or overwrites an existing one at the specified path.<\/p>\n<pre>\n\/**\n * @param {string}             path        \u2014 File path (e.g., 'notes.txt')\n * @param {string|ArrayBuffer} content     \u2014 File content (text or binary)\n * @param {boolean}            breakCommit \u2014 Break pending commit lock (default: false)\n * @param {string|ArrayBuffer} threadID    \u2014 Thread to process on (default: 'data')\n * @returns {COperationStatus}\n *\/\nconst result = fs.doNewFile('notes.txt', 'Hello, blockchain!', false, this.getThreadID);\nthis.addNetworkRequestID(result.getReqID);\n<\/pre>\n<p><strong>Important:<\/strong> The file path is automatically quoted by the method if not already wrapped in quotes. The content can be either a JavaScript string (which will be UTF-8 encoded) or a raw <code>ArrayBuffer<\/code> for binary data. On success, a <code>CConsensusTask<\/code> is created with description &#8220;New File&#8221; \u2014 this appears in the Magic Button&#8217;s pending operations display, informing the user that uncommitted changes exist.<\/p>\n<h4><code>updateFile(path, content, breakCommit, threadID)<\/code> \u2014 Update an Existing File<\/h4>\n<p>Updates a file&#8217;s content. Unlike <code>doNewFile()<\/code>, this method checks whether a consensus task for the same file already exists, avoiding duplicate entries in the pending operations list.<\/p>\n<pre>\n\/**\n * @param {string}             path        \u2014 File path\n * @param {string|ArrayBuffer} content     \u2014 New content\n * @param {boolean}            breakCommit \u2014 Break pending commit lock (default: false)\n * @param {string|ArrayBuffer} threadID    \u2014 Thread (default: 'data')\n * @returns {COperationStatus}\n *\/\nconst result = fs.updateFile('notes.txt', 'Updated content', false, this.getThreadID);\n<\/pre>\n<h4><code>doGetFile(path, breakCommit, threadID)<\/code> \u2014 Read a File<\/h4>\n<p>Reads the content of a file at the given path.<\/p>\n<pre>\n\/**\n * @param {string}             path        \u2014 File path to read\n * @param {boolean}            breakCommit \u2014 Break pending commit lock (default: false)\n * @param {string|ArrayBuffer} threadID    \u2014 Thread (default: 'data')\n * @returns {COperationStatus}\n *\/\nconst result = fs.doGetFile('notes.txt', false, this.getThreadID);\nthis.addNetworkRequestID(result.getReqID);\n\/\/ File content arrives asynchronously via DFS message listener\n<\/pre>\n<h4><code>doNewDir(path, breakCommit, threadID)<\/code> \u2014 Create a Directory<\/h4>\n<pre>\n\/**\n * @param {string}             path        \u2014 Directory name\/path\n * @param {boolean}            breakCommit \u2014 Break pending commit lock (default: false)\n * @param {string|ArrayBuffer} threadID    \u2014 Thread (default: 'data')\n * @returns {COperationStatus}\n *\/\nconst result = fs.doNewDir('myAppData', false, this.getThreadID);\nthis.addNetworkRequestID(result.getReqID);\n<\/pre>\n<h4><code>doCommit(breakCommit, threadID)<\/code> \u2014 Commit Changes to Blockchain<\/h4>\n<p>This is where the magic happens. All file operations (create, update, delete) are initially made in a <strong>local staging area<\/strong> \u2014 like a Git index. Nothing is permanent until you commit. The <code>doCommit()<\/code> method proposes all staged changes as a blockchain transaction.<\/p>\n<pre>\n\/**\n * @param {boolean}            breakCommit \u2014 Break pending commit lock (default: false)\n * @param {string|ArrayBuffer} threadID    \u2014 Thread (default: 'system')\n * @returns {COperationStatus}\n *\/\nconst result = fs.doCommit(false, 'system');\n<\/pre>\n<p><strong>Critical detail:<\/strong> Note the default thread ID is <code>'system'<\/code> for commits, not <code>'data'<\/code>. Commits are state-changing operations that must run on the system thread. The commit process involves:<\/p>\n<ol>\n<li>A commit lock is acquired (preventing other dApps from committing simultaneously).<\/li>\n<li>Staged changes are bundled into a transaction proposal.<\/li>\n<li>The full node submits the transaction to the blockchain network.<\/li>\n<li>Network consensus validates the transaction.<\/li>\n<li>On success, the changes become permanent and immutable.<\/li>\n<\/ol>\n<p>The commit state machine has several states tracked by <code>CVMContext<\/code>: <code>eCommitState.none<\/code>, <code>eCommitState.prePending<\/code>, <code>eCommitState.pending<\/code>, <code>eCommitState.success<\/code>, <code>eCommitState.aborted<\/code>. You can listen for commit state changes via <code>CVMContext.getInstance().addVMCommitStateChangedListener()<\/code>.<\/p>\n<figure><img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/building-real-things-commit-lifecycle.svg\" alt=\"File operation lifecycle: write, commit, blockchain\" style=\"width:100%; max-width:900px;\" \/><figcaption>The lifecycle of a file operation: write to cache \u2192 navigate and verify \u2192 commit to blockchain \u2192 immutable storage.<\/figcaption><\/figure>\n<h4><code>doSync(breakCommit, threadID)<\/code> \u2014 Synchronize from Blockchain<\/h4>\n<p>Pulls the latest committed state from the blockchain, ensuring your local view reflects the most recent on-chain data.<\/p>\n<pre>\n\/**\n * @param {boolean}            breakCommit \u2014 Break pending commit lock (default: false)\n * @param {string|ArrayBuffer} threadID    \u2014 Thread (default: 'system')\n * @returns {COperationStatus}\n *\/\nconst result = fs.doSync();\n<\/pre>\n<p>Call <code>doSync()<\/code> when you need to see the freshest on-chain data \u2014 for example, after another user has committed changes, or when your dApp starts up and needs the latest state.<\/p>\n<h3>Listening for Responses: The DFS Message Listener Pattern<\/h3>\n<p>Since all file system operations are asynchronous, you need to register listeners to receive results. There are two listener mechanisms:<\/p>\n<h4>1. DFS Message Listener \u2014 Low-Level<\/h4>\n<pre>\n\/\/ Register a DFS message listener\nCVMContext.getInstance().addNewDFSMsgListener(\n  function(msg) {\n    \/\/ msg is a CDFSMsg containing the response\n    \/\/ Process directory listings, file contents, error codes, etc.\n    console.log('DFS Response received:', msg);\n  },\n  this.getProcessID  \/\/ appID \u2014 for cleanup when your dApp closes\n);\n<\/pre>\n<p>The File Manager dApp registers its listener in the constructor and uses it to process all file system responses \u2014 updating the UI with directory listings, displaying file contents, and handling errors.<\/p>\n<h4>2. DFS Request Completed Listener \u2014 High-Level<\/h4>\n<pre>\nCVMContext.getInstance().addDFSRequestCompletedListener(\n  function(result) {\n    \/\/ result contains the operation status and any response data\n  },\n  this.getProcessID\n);\n<\/pre>\n<h4>3. Network Request ID Tracking<\/h4>\n<p>The <code>CWindow<\/code> base class provides <code>addNetworkRequestID(reqID)<\/code>, which the File Manager uses extensively to track which requests belong to this window. When a response arrives, the window can check if the response&#8217;s request ID matches one it&#8217;s tracking:<\/p>\n<pre>\n\/\/ Pattern from FileManager.js \u2014 fire request and track its ID\nconst result = CVMContext.getInstance().getFileSystem.doCD(\n  path, true, false, false, this.getThreadID\n);\nthis.addNetworkRequestID(result.getReqID);\n\n\/\/ Later, in DFS listener callback:\n\/\/ Check if msg.getRequestID matches one of our tracked request IDs\n<\/pre>\n<h3>Complete Worked Example: A Simple File Manager<\/h3>\n<p>Here is a complete, runnable dApp that creates a directory, writes a file, reads it back, and commits everything to the blockchain:<\/p>\n<pre>\n\"use strict\"\nimport { CWindow } from \"\/lib\/window.js\"\n\nconst appBody = `\n&lt;style&gt;\n  .container { padding: 1em; color: #22fafc; font-family: monospace;\n    background: linear-gradient(135deg, #0a0a14, #0d1a2d); height: 100%; }\n  button { background: #1a3a5c; color: #00f0ff; border: 1px solid #00f0ff;\n    padding: 8px 16px; margin: 4px; cursor: pointer; border-radius: 4px; }\n  button:hover { background: #00f0ff; color: #0a0a14; }\n  #output { margin-top: 1em; white-space: pre-wrap; color: #8892b0;\n    max-height: 300px; overflow-y: auto; }\n&lt;\/style&gt;\n&lt;div class=\"container\"&gt;\n  &lt;h2&gt;File System Demo&lt;\/h2&gt;\n  &lt;button id=\"btnCreate\"&gt;Create Directory + File&lt;\/button&gt;\n  &lt;button id=\"btnRead\"&gt;Read File&lt;\/button&gt;\n  &lt;button id=\"btnCommit\"&gt;Commit to Blockchain&lt;\/button&gt;\n  &lt;button id=\"btnSync\"&gt;Sync from Chain&lt;\/button&gt;\n  &lt;div id=\"output\"&gt;&lt;\/div&gt;\n&lt;\/div&gt;\n`;\n\nclass CFileSystemDemo extends CWindow {\n\n  constructor(x, y, w, h) {\n    super(x, y, w, h, appBody, \"FS Demo\", CFileSystemDemo.getIcon(), true);\n    this.setThreadID = 'FS_DEMO_' + this.getProcessID;\n    this.mVMContext = CVMContext.getInstance();\n    this.mFS = this.mVMContext.getFileSystem;\n\n    \/\/ Register DFS message listener for responses\n    this.mVMContext.addNewDFSMsgListener(\n      this.onDFSResponse.bind(this),\n      this.getProcessID\n    );\n\n    \/\/ Bind UI\n    this.shadowRoot.getElementById('btnCreate').onclick = () =&gt; this.createFileDemo();\n    this.shadowRoot.getElementById('btnRead').onclick   = () =&gt; this.readFileDemo();\n    this.shadowRoot.getElementById('btnCommit').onclick  = () =&gt; this.commitDemo();\n    this.shadowRoot.getElementById('btnSync').onclick    = () =&gt; this.syncDemo();\n  }\n\n  static getPackageID() { return \"com.demo.fsDemo\"; }\n  static getIcon() { return '\/images\/filemanager.png'; }\n\n  log(msg) {\n    const el = this.shadowRoot.getElementById('output');\n    el.textContent += new Date().toLocaleTimeString() + ' \u2014 ' + msg + '\\n';\n    el.scrollTop = el.scrollHeight;\n  }\n\n  createFileDemo() {\n    \/\/ Step 1: Navigate to root\n    let r1 = this.mFS.doCD('\/', true, false, false, this.getThreadID);\n    this.addNetworkRequestID(r1.getReqID);\n    this.log('CD \/ \u2192 reqID: ' + r1.getReqID);\n\n    \/\/ Step 2: Create a directory\n    let r2 = this.mFS.doNewDir('demoApp', false, this.getThreadID);\n    this.addNetworkRequestID(r2.getReqID);\n    this.log('MKDIR demoApp \u2192 reqID: ' + r2.getReqID);\n\n    \/\/ Step 3: Navigate into it\n    let r3 = this.mFS.doCD('demoApp', true, false, false, this.getThreadID);\n    this.addNetworkRequestID(r3.getReqID);\n    this.log('CD demoApp \u2192 reqID: ' + r3.getReqID);\n\n    \/\/ Step 4: Create a file with content\n    let r4 = this.mFS.doNewFile(\n      'hello.txt',\n      'Hello from the decentralized file system! Timestamp: ' + Date.now(),\n      false,\n      this.getThreadID\n    );\n    this.addNetworkRequestID(r4.getReqID);\n    this.log('NEW FILE hello.txt \u2192 reqID: ' + r4.getReqID);\n  }\n\n  readFileDemo() {\n    let r = this.mFS.doGetFile('hello.txt', false, this.getThreadID);\n    this.addNetworkRequestID(r.getReqID);\n    this.log('GET FILE hello.txt \u2192 reqID: ' + r.getReqID);\n  }\n\n  commitDemo() {\n    let r = this.mFS.doCommit(false, 'system');\n    this.addNetworkRequestID(r.getReqID);\n    this.log('COMMIT \u2192 reqID: ' + r.getReqID);\n  }\n\n  syncDemo() {\n    let r = this.mFS.doSync();\n    this.log('SYNC \u2192 reqID: ' + r.getReqID);\n  }\n\n  onDFSResponse(msg) {\n    \/\/ This receives ALL DFS responses \u2014 filter by request ID\n    this.log('DFS Response: type=' + msg.getType + ' reqID=' + msg.getRequestID);\n  }\n\n  static getDefaultCategory() { return 'dApps'; }\n}\n\nexport { CFileSystemDemo };\n<\/pre>\n<h2>Part II: Peer-to-Peer Messaging<\/h2>\n<figure><img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/building-real-things-messaging-flow.svg\" alt=\"P2P Messaging architecture showing WebRTC data channels\" style=\"width:100%; max-width:900px;\" \/><figcaption>The Messenger creates a direct, encrypted peer-to-peer channel between browsers. The signaling node is only needed for the initial handshake \u2014 after that, messages flow directly.<\/figcaption><\/figure>\n<h3>How the Messenger Works<\/h3>\n<p>The GRIDNET OS Messenger (<code>CMessenger<\/code>, defined in <code>\/dApps\/Messenger.js<\/code>) is a fully-featured peer-to-peer chat application built on WebRTC. It does <strong>not<\/strong> use GRIDNET&#8217;s full-node-mediated swarm system for message delivery \u2014 instead, it creates a <strong>direct RTCPeerConnection<\/strong> between two browsers, with a named <code>DataChannel<\/code> labelled <code>\"P2P_CHAT_CHANNEL_LABEL\"<\/code>.<\/p>\n<p>The signaling mechanism uses the GRIDNET full node as a relay for the initial SDP (Session Description Protocol) offer\/answer exchange and ICE candidate negotiation. Once the peer-to-peer connection is established, messages flow directly between browsers \u2014 encrypted with DTLS \u2014 and the signaling node is no longer involved.<\/p>\n<h3>Connection Flow<\/h3>\n<ol>\n<li><strong>User A initiates.<\/strong> The Messenger creates a new <code>RTCPeerConnection<\/code> and a DataChannel named <code>\"P2P_CHAT_CHANNEL_LABEL\"<\/code>.<\/li>\n<li><strong>SDP Offer.<\/strong> An SDP offer is generated via <code>peerConnection.createOffer()<\/code> and sent through the GRIDNET signaling node to User B.<\/li>\n<li><strong>User B responds.<\/strong> User B receives the offer, creates their own <code>RTCPeerConnection<\/code>, sets the remote description, generates an SDP answer, and sends it back.<\/li>\n<li><strong>ICE Exchange.<\/strong> Both peers exchange ICE candidates through the signaling node via <code>onicecandidate<\/code> callbacks. These candidates contain network path information for NAT traversal.<\/li>\n<li><strong>Direct Connection.<\/strong> Once ICE negotiation succeeds, the <code>DataChannel<\/code> opens. The <code>onopen<\/code> callback fires, and messages can now be sent directly peer-to-peer.<\/li>\n<li><strong>Message Exchange.<\/strong> Text messages are sent through the <code>DataChannel.send()<\/code> method and received via <code>DataChannel.onmessage<\/code>.<\/li>\n<\/ol>\n<h3>The Peer Code Pattern<\/h3>\n<p>The Messenger uses a <strong>peer code<\/strong> system for connection establishment. When User A wants to connect to User B:<\/p>\n<ul>\n<li>User B generates or shares a peer code (typically derived from their identity or session).<\/li>\n<li>User A enters this code in the Messenger UI.<\/li>\n<li>The signaling node routes the SDP exchange based on this code.<\/li>\n<\/ul>\n<p>This is fundamentally different from centralized messaging where a server stores and forwards messages. Here, the &#8220;server&#8221; (GRIDNET node) is only a matchmaker \u2014 it introduces peers and then steps aside.<\/p>\n<h3>Building Your Own P2P Messaging<\/h3>\n<p>While the Messenger dApp is a React-based bundled application, you can build your own P2P messaging using the same underlying WebRTC infrastructure. The key components from the GRIDNET OS library are:<\/p>\n<pre>\n\/\/ 1. Access the Swarm Manager (which handles WebRTC signaling)\nconst swarmManager = CVMContext.getInstance().getSwarmsManager;\n\n\/\/ 2. Join a swarm (creates a named communication room)\nconst swarmID = CTools.getInstance().convertToArrayBuffer('my-chat-room');\nswarmManager.joinSwarm(\n  swarmID,                     \/\/ swarm identifier\n  new ArrayBuffer(),           \/\/ userID (empty = use session identity)\n  new ArrayBuffer(),           \/\/ privKey (empty = use session key)\n  eConnCapabilities.data,      \/\/ we only need data channels, not audio\/video\n  this                         \/\/ app instance (CWindow subclass)\n);\n\n\/\/ 3. Register event listeners on the swarm\nconst swarm = swarmManager.findSwarmByID(swarmID);\nif (swarm) {\n  \/\/ Listen for incoming messages\n  swarm.addMessageEventListener(function(event) {\n    console.log('Message from peer:', event);\n  }, this.getProcessID);\n\n  \/\/ Listen for data channel messages (lower level)\n  swarm.addDataChannelMessageEventListener(function(event) {\n    console.log('DataChannel message:', event);\n  }, this.getProcessID);\n\n  \/\/ Listen for peer connection state changes\n  swarm.addSwarmConnectionStateChangeEventListener(function(event) {\n    console.log('Connection state:', event);\n  }, this.getProcessID);\n\n  \/\/ Listen for new peer tracks (audio\/video)\n  swarm.addTrackEventListener(function(event) {\n    console.log('New track from peer:', event);\n  }, this.getProcessID);\n\n  \/\/ Send data to all peers in the swarm\n  swarm.sendData(\n    CTools.getInstance().convertToArrayBuffer('Hello, swarm!'),\n    null,  \/\/ null = broadcast to all peers\n    true   \/\/ only send to authenticated peers\n  );\n\n  \/\/ Send a structured swarm message\n  swarm.sendSwarmMessage(\n    messageData,    \/\/ CSwarmMsg or raw data\n    protocolID,     \/\/ application-defined protocol identifier\n    true            \/\/ only to authenticated peers\n  );\n}\n<\/pre>\n<h3>Message Authentication and Privacy<\/h3>\n<p>Swarms support two security modes:<\/p>\n<ul>\n<li><strong>Open Swarms<\/strong> (<code>eSwarmAuthRequirement.open<\/code>) \u2014 Any peer can join and participate. Data channels are still encrypted by WebRTC&#8217;s built-in DTLS, but there is no application-level authentication.<\/li>\n<li><strong>Private Swarms<\/strong> (<code>eSwarmAuthRequirement.PSK_ZK<\/code>) \u2014 A pre-shared key is required. Peers must prove knowledge of the key using a zero-knowledge proof (the key image is a SHA3-256 hash with a time-based nonce). Unauthenticated peers receive only dummy tracks (silent audio, black video) until they authenticate.<\/li>\n<\/ul>\n<pre>\n\/\/ Make a swarm private with a shared password\nawait swarm.setPreSharedKey('my-secret-password');\n\/\/ This triggers: swarm.authRequirement = eSwarmAuthRequirement.PSK_ZK\n\/\/ All current peers are required to re-authenticate\n\n\/\/ Or via command processor:\nawait swarm.processCommand('\/setkey my-secret-password');\n<\/pre>\n<h2>Part III: WebRTC Swarms<\/h2>\n<figure><img decoding=\"async\" src=\"https:\/\/gridnet.org\/wpp\/wp-content\/uploads\/2026\/02\/building-real-things-swarm-mesh.svg\" alt=\"WebRTC Swarm mesh topology\" style=\"width:100%; max-width:900px;\" \/><figcaption>A WebRTC Swarm: each browser connects directly to every other browser. The signaling node (GRIDNET full node) brokers initial connections; after that, all traffic is peer-to-peer.<\/figcaption><\/figure>\n<h3>What is a Swarm?<\/h3>\n<p>A <strong>WebRTC Swarm<\/strong> in GRIDNET OS is a managed mesh network of browser-to-browser connections. Each &#8220;swarm&#8221; is identified by a unique ID, and all participants who join the same swarm ID are connected to each other in a full-mesh topology \u2014 every peer has a direct <code>RTCPeerConnection<\/code> to every other peer.<\/p>\n<p>The architecture involves three layers:<\/p>\n<ol>\n<li><strong><code>CSwarmsManager<\/code><\/strong> (<code>\/lib\/SwarmManager.js<\/code>) \u2014 The singleton manager that handles swarm lifecycle, media device management, and signaling routing. Accessed via <code>CVMContext.getInstance().getSwarmsManager<\/code>.<\/li>\n<li><strong><code>CSwarm<\/code><\/strong> (<code>\/lib\/swarm.js<\/code>) \u2014 Represents a single swarm instance. Manages the collection of peer connections, event dispatching, authentication, and data broadcasting.<\/li>\n<li><strong><code>CSwarmConnection<\/code><\/strong> (<code>\/lib\/swarmconnection.js<\/code>) \u2014 Represents a single peer-to-peer connection within a swarm. Wraps an <code>RTCPeerConnection<\/code> with data channel management, track management, and authentication.<\/li>\n<\/ol>\n<h3>Swarm Lifecycle<\/h3>\n<h4>Creating \/ Joining a Swarm<\/h4>\n<pre>\n\/**\n * CSwarmsManager.joinSwarm()\n * @param {ArrayBuffer} trueSwarmID  \u2014 Human-readable swarm ID (hashed internally)\n * @param {ArrayBuffer} userID       \u2014 User identity (empty = session identity)\n * @param {ArrayBuffer} privKey      \u2014 Private key for auth (empty = session key)\n * @param {number}      capabilities \u2014 eConnCapabilities flags\n * @param {CWindow}     appInstance  \u2014 Your dApp window instance\n * @returns {COperationStatus|false}\n *\/\nconst mgr = CVMContext.getInstance().getSwarmsManager;\nconst tools = CTools.getInstance();\n\nconst result = mgr.joinSwarm(\n  tools.convertToArrayBuffer('my-collab-room'),\n  new ArrayBuffer(),\n  new ArrayBuffer(),\n  eConnCapabilities.data,  \/\/ or eConnCapabilities.audioVideo for video calls\n  this  \/\/ your CWindow instance \u2014 auto-cleaned up when window closes\n);\n<\/pre>\n<p><strong>How it works internally:<\/strong><\/p>\n<ol>\n<li>The true swarm ID is hashed with SHA3-256 and Base58Check-encoded to produce the network swarm ID.<\/li>\n<li>A new <code>CSwarm<\/code> instance is created (or an existing one is reused) and added to the manager.<\/li>\n<li>An SDP &#8220;joining&#8221; entity (<code>eSDPEntityType.joining<\/code>) is constructed, optionally authenticated with a Transmission Token, and sent to the full node via <code>CNetMsg<\/code>.<\/li>\n<li>The full node registers the peer and begins brokering connections with other swarm members.<\/li>\n<li>Your dApp instance is registered as a client process of the swarm \u2014 when your window closes, the swarm automatically cleans up.<\/li>\n<\/ol>\n<h4>Leaving a Swarm<\/h4>\n<pre>\nmgr.leaveSwarm(tools.convertToArrayBuffer('my-collab-room'));\n<\/pre>\n<p>This sends an SDP &#8220;bye&#8221; entity to the signaling node, closes all peer connections in the swarm, and releases resources. Swarms also <strong>auto-terminate<\/strong> when all client dApp processes have been unregistered (controlled by the <code>killWhenNoProcesses<\/code> flag, which defaults to <code>true<\/code>).<\/p>\n<h4>Sending Data in a Swarm<\/h4>\n<pre>\nconst swarm = mgr.findSwarmByID(tools.convertToArrayBuffer('my-collab-room'));\n\n\/\/ Broadcast to all peers\nswarm.sendData(\n  tools.convertToArrayBuffer(JSON.stringify({ type: 'update', data: myPayload })),\n  null,   \/\/ null target = broadcast\n  true    \/\/ only to authenticated peers (relevant for private swarms)\n);\n\n\/\/ Send to a specific peer\nswarm.sendData(\n  tools.convertToArrayBuffer(myData),\n  peerID  \/\/ ArrayBuffer \u2014 the specific peer's ID\n);\n\n\/\/ Send a structured swarm message with protocol ID\nswarm.sendSwarmMessage(messageData, myProtocolID, true);\n<\/pre>\n<h4>Receiving Data<\/h4>\n<pre>\n\/\/ Level 1: Raw DataChannel messages (lowest level)\nswarm.addDataChannelMessageEventListener(function(event) {\n  \/\/ event contains raw DataChannel message event\n}, this.getProcessID);\n\n\/\/ Level 2: Parsed messages (CNetMsg encapsulated)\nswarm.addMessageEventListener(function(event) {\n  \/\/ event is parsed and includes connection\/peer metadata\n}, this.getProcessID);\n\n\/\/ Level 3: Swarm-level messages (highest level, includes protocol ID)\nswarm.addSwarmMessageEventListener(function(event) {\n  \/\/ event includes protocolID for application-level routing\n}, this.getProcessID);\n<\/pre>\n<h4>Audio\/Video Capabilities<\/h4>\n<pre>\n\/\/ Join with audio+video\nmgr.joinSwarm(swarmID, userID, privKey, eConnCapabilities.audioVideo, this);\n\n\/\/ Manage capabilities dynamically\nswarm.setAllowedCapabilities(eConnCapabilities.audioVideo);\nswarm.setEffectiveOutgressCapabilities(eConnCapabilities.data); \/\/ mute AV\n\n\/\/ Listen for incoming tracks\nswarm.addTrackEventListener(function(event) {\n  \/\/ event.streams contains MediaStream objects\n  \/\/ Attach to &lt;video&gt; or &lt;audio&gt; elements\n  const videoEl = document.createElement('video');\n  videoEl.srcObject = event.streams[0];\n  videoEl.play();\n}, this.getProcessID);\n\n\/\/ Mute\/unmute\nawait swarm.mute(true, true, true);   \/\/ mute audio, video, outgress\nawait swarm.unmute(true, true);         \/\/ unmute audio, video\n\n\/\/ Screen sharing\nconst screenStream = await mgr.startCapture();\nif (screenStream) {\n  const videoTrack = screenStream.getVideoTracks()[0];\n  await swarm.setLIVEVideoTrack(videoTrack, true);  \/\/ true = update with peers\n}\n<\/pre>\n<h3>Connection Quality and Peer Status<\/h3>\n<pre>\n\/\/ Monitor peer status changes\nswarm.addPeerStatusEventListener(function(event) {\n  console.log('Peer status changed:', event);\n}, this.getProcessID);\n\n\/\/ Monitor connection quality\nswarm.addConnectionQualityEventListener(function(event) {\n  \/\/ Quality thresholds (in ms):\n  \/\/ Max:    &lt; 1000ms\n  \/\/ High:   &lt; 1500ms\n  \/\/ Medium: &lt; 2000ms\n  \/\/ Low:    &lt; 3500ms\n  console.log('Connection quality:', event);\n}, this.getProcessID);\n\n\/\/ Monitor ICE connection state\nswarm.addICEConnectionStateChangeEventListener(function(event) {\n  console.log('ICE state:', event);\n}, this.getProcessID);\n<\/pre>\n<h3>Authentication Events<\/h3>\n<pre>\nswarm.addPeerAuthResultListener(function(event) {\n  \/\/ event contains authentication result for a specific peer\n  \/\/ In private swarms, unauthenticated peers only see dummy tracks\n  console.log('Peer authentication result:', event);\n}, this.getProcessID);\n<\/pre>\n<h2>Part IV: Building a Real Feature \u2014 Collaborative Notes dApp<\/h2>\n<p>Let&#8217;s combine all three capabilities \u2014 files, messaging, and swarms \u2014 into a single, practical application: a <strong>Collaborative Notes dApp<\/strong> where multiple users can edit shared notes in real-time, with changes persisted to the blockchain.<\/p>\n<pre>\n\"use strict\"\nimport { CWindow } from \"\/lib\/window.js\"\n\nconst collabNotesBody = `\n&lt;style&gt;\n  :host { display: block; height: 100%; }\n  .container { display: flex; flex-direction: column; height: 100%;\n    background: linear-gradient(135deg, #0a0a14, #0d1a2d);\n    font-family: 'Rajdhani', monospace; color: #22fafc; padding: 1em; box-sizing: border-box; }\n  .toolbar { display: flex; gap: 8px; margin-bottom: 1em; flex-wrap: wrap; }\n  button { background: #1a3a5c; color: #00f0ff; border: 1px solid #00f0ff;\n    padding: 6px 14px; cursor: pointer; border-radius: 4px; font-size: 0.85rem; }\n  button:hover { background: #00f0ff; color: #0a0a14; }\n  button.commit { border-color: #ffd700; color: #ffd700; }\n  button.commit:hover { background: #ffd700; color: #0a0a14; }\n  textarea { flex: 1; background: #0d1a2d; color: #e0e0e0; border: 1px solid #1a3a5c;\n    padding: 1em; font-family: monospace; font-size: 14px; resize: none;\n    border-radius: 4px; outline: none; }\n  textarea:focus { border-color: #00f0ff; box-shadow: 0 0 10px rgba(0,240,255,0.2); }\n  .status-bar { margin-top: 8px; font-size: 0.8rem; color: #8892b0; }\n  .peers { color: #00ff88; }\n  #roomInput { background: #0d1a2d; color: #00f0ff; border: 1px solid #1a3a5c;\n    padding: 6px 10px; border-radius: 4px; width: 200px; }\n&lt;\/style&gt;\n&lt;div class=\"container\"&gt;\n  &lt;div class=\"toolbar\"&gt;\n    &lt;input id=\"roomInput\" placeholder=\"Room name...\" value=\"shared-notes\" \/&gt;\n    &lt;button id=\"btnJoin\"&gt;Join Room&lt;\/button&gt;\n    &lt;button id=\"btnSave\"&gt;Save to DFS&lt;\/button&gt;\n    &lt;button id=\"btnLoad\"&gt;Load from DFS&lt;\/button&gt;\n    &lt;button id=\"btnCommit\" class=\"commit\"&gt;\u26a1 Commit to Chain&lt;\/button&gt;\n  &lt;\/div&gt;\n  &lt;textarea id=\"editor\" placeholder=\"Start typing your notes...\"&gt;&lt;\/textarea&gt;\n  &lt;div class=\"status-bar\"&gt;\n    Status: &lt;span id=\"status\"&gt;Not connected&lt;\/span&gt;\n    | Peers: &lt;span id=\"peerCount\" class=\"peers\"&gt;0&lt;\/span&gt;\n  &lt;\/div&gt;\n&lt;\/div&gt;\n`;\n\nclass CCollabNotes extends CWindow {\n\n  constructor(x, y, w, h) {\n    super(x, y, w, h, collabNotesBody, \"Collaborative Notes\", CCollabNotes.getIcon(), true);\n    this.setThreadID = 'COLLAB_NOTES_' + this.getProcessID;\n    this.mVMContext = CVMContext.getInstance();\n    this.mFS = this.mVMContext.getFileSystem;\n    this.mSwarmManager = this.mVMContext.getSwarmsManager;\n    this.mTools = CTools.getInstance();\n    this.mSwarm = null;\n    this.mTypingDebounce = null;\n    this.mFilePath = 'collab-notes.txt';\n\n    \/\/ Register DFS listener\n    this.mVMContext.addNewDFSMsgListener(\n      this.onDFSResponse.bind(this), this.getProcessID\n    );\n\n    \/\/ Bind UI\n    const root = this.shadowRoot;\n    root.getElementById('btnJoin').onclick    = () =&gt; this.joinRoom();\n    root.getElementById('btnSave').onclick    = () =&gt; this.saveToFile();\n    root.getElementById('btnLoad').onclick    = () =&gt; this.loadFromFile();\n    root.getElementById('btnCommit').onclick  = () =&gt; this.commitToChain();\n\n    \/\/ Real-time sync: broadcast changes as user types\n    root.getElementById('editor').oninput = () =&gt; {\n      clearTimeout(this.mTypingDebounce);\n      this.mTypingDebounce = setTimeout(() =&gt; this.broadcastContent(), 300);\n    };\n\n    \/\/ Initialize: navigate to a working directory\n    this.mFS.doCD('\/', true, false, true, this.getThreadID);\n    this.mFS.doNewDir('collab', false, this.getThreadID);\n    this.mFS.doCD('collab', false, false, true, this.getThreadID);\n  }\n\n  static getPackageID() { return \"com.demo.collabNotes\"; }\n  static getIcon() { return '\/images\/messenger.png'; }\n  static getDefaultCategory() { return 'dApps'; }\n\n  setStatus(text) {\n    this.shadowRoot.getElementById('status').textContent = text;\n  }\n\n  joinRoom() {\n    const roomName = this.shadowRoot.getElementById('roomInput').value || 'shared-notes';\n    const swarmID = this.mTools.convertToArrayBuffer(roomName);\n\n    \/\/ Join with data-only capabilities (no audio\/video needed)\n    const result = this.mSwarmManager.joinSwarm(\n      swarmID, new ArrayBuffer(), new ArrayBuffer(),\n      eConnCapabilities.data, this\n    );\n\n    if (result) {\n      this.setStatus('Joining room: ' + roomName + '...');\n\n      \/\/ Find the swarm and register listeners\n      setTimeout(() =&gt; {\n        this.mSwarm = this.mSwarmManager.findSwarmByID(swarmID);\n        if (this.mSwarm) {\n          this.registerSwarmListeners();\n          this.setStatus('Connected to: ' + roomName);\n        }\n      }, 1000);\n    }\n  }\n\n  registerSwarmListeners() {\n    \/\/ Listen for incoming data from peers\n    this.mSwarm.addDataChannelMessageEventListener((event) =&gt; {\n      try {\n        const msg = JSON.parse(this.mTools.arrayBufferToString(event.data));\n        if (msg.type === 'content-update') {\n          \/\/ Update editor with peer's content\n          const editor = this.shadowRoot.getElementById('editor');\n          const cursorPos = editor.selectionStart;\n          editor.value = msg.content;\n          \/\/ Restore cursor position (basic conflict resolution)\n          editor.selectionStart = editor.selectionEnd = Math.min(cursorPos, msg.content.length);\n        }\n      } catch (e) { \/* ignore non-JSON messages *\/ }\n    }, this.getProcessID);\n\n    \/\/ Track peer count\n    this.mSwarm.addPeerStatusEventListener((event) =&gt; {\n      const count = this.mSwarm.peers ? this.mSwarm.peers.length : 0;\n      this.shadowRoot.getElementById('peerCount').textContent = count;\n    }, this.getProcessID);\n\n    this.mSwarm.addSwarmConnectionStateChangeEventListener((event) =&gt; {\n      const count = this.mSwarm.peers ? this.mSwarm.peers.length : 0;\n      this.shadowRoot.getElementById('peerCount').textContent = count;\n    }, this.getProcessID);\n  }\n\n  broadcastContent() {\n    if (!this.mSwarm) return;\n    const content = this.shadowRoot.getElementById('editor').value;\n    const msg = JSON.stringify({ type: 'content-update', content: content });\n    this.mSwarm.sendData(\n      this.mTools.convertToArrayBuffer(msg), null, false\n    );\n  }\n\n  saveToFile() {\n    const content = this.shadowRoot.getElementById('editor').value;\n    const result = this.mFS.updateFile(this.mFilePath, content, false, this.getThreadID);\n    this.setStatus('Saved to DFS (uncommitted)');\n  }\n\n  loadFromFile() {\n    const result = this.mFS.doGetFile(this.mFilePath, false, this.getThreadID);\n    this.addNetworkRequestID(result.getReqID);\n    this.setStatus('Loading from DFS...');\n  }\n\n  commitToChain() {\n    \/\/ First save current content, then commit\n    this.saveToFile();\n    const result = this.mFS.doCommit(false, 'system');\n    this.setStatus('Committing to blockchain...');\n\n    \/\/ Listen for commit state changes\n    this.mVMContext.addVMCommitStateChangedListener((state) =&gt; {\n      switch (state) {\n        case eCommitState.pending:\n          this.setStatus('Commit pending \u2014 awaiting consensus...');\n          break;\n        case eCommitState.success:\n          this.setStatus('\u2713 Committed to blockchain!');\n          break;\n        case eCommitState.aborted:\n          this.setStatus('\u2717 Commit aborted');\n          break;\n      }\n    }, this.getProcessID);\n  }\n\n  onDFSResponse(msg) {\n    \/\/ Handle file read responses\n    if (msg.getData &amp;&amp; msg.getData.byteLength &gt; 0) {\n      try {\n        const content = this.mTools.arrayBufferToString(msg.getData);\n        this.shadowRoot.getElementById('editor').value = content;\n        this.setStatus('Loaded from DFS');\n      } catch (e) {}\n    }\n  }\n\n  \/\/ Cleanup when window closes\n  closeWindow() {\n    if (this.mSwarm) {\n      this.mSwarm.removeClientAppInstance(this);\n    }\n    super.closeWindow();\n  }\n}\n\nexport { CCollabNotes };\n<\/pre>\n<p>This dApp demonstrates the three pillars working together:<\/p>\n<ul>\n<li><strong>File System:<\/strong> Notes are saved to <code>\/collab\/collab-notes.txt<\/code> on the decentralized file system and committed to the blockchain.<\/li>\n<li><strong>WebRTC Swarm:<\/strong> When users join the same room name, they connect via a WebRTC swarm and receive real-time content updates from peers.<\/li>\n<li><strong>Blockchain Persistence:<\/strong> The &#8220;Commit to Chain&#8221; button permanently stores the current note content on the blockchain \u2014 a timestamped, immutable snapshot.<\/li>\n<\/ul>\n<h2>Part V: Error Handling and Edge Cases<\/h2>\n<h3>Connection Drops<\/h3>\n<p>GRIDNET OS handles disconnections at multiple levels:<\/p>\n<ul>\n<li><strong>WebSocket disconnection:<\/strong> The <code>CVMContext<\/code> connection controller automatically attempts reconnection. The commit lock is broken on disconnect, and all swarms are dissociated. Listen for <code>eConnectionState.disconnected<\/code> via <code>addConnectionStatusChangedListener()<\/code>.<\/li>\n<li><strong>WebRTC peer disconnection:<\/strong> Individual <code>CSwarmConnection<\/code> instances detect ICE disconnections and clean up. The swarm remains active and will re-establish connections when the peer returns.<\/li>\n<li><strong>Commit during disconnect:<\/strong> If a commit is in progress when the connection drops, the commit state transitions to <code>eCommitState.aborted<\/code> and the commit lock is forcefully broken.<\/li>\n<\/ul>\n<pre>\n\/\/ Listen for connection state changes\nCVMContext.getInstance().addConnectionStatusChangedListener(\n  function(state) {\n    switch (state) {\n      case eConnectionState.connected:\n        console.log('Connected to full node');\n        \/\/ Re-sync file system state\n        CVMContext.getInstance().getFileSystem.doSync();\n        break;\n      case eConnectionState.disconnected:\n        console.log('Disconnected \u2014 will auto-reconnect');\n        break;\n      case eConnectionState.connecting:\n        console.log('Reconnecting...');\n        break;\n    }\n  },\n  this.getProcessID\n);\n<\/pre>\n<h3>Commit Conflicts<\/h3>\n<p>The commit lock mechanism (<code>tryLockCommit()<\/code>, <code>breakCommitLock()<\/code>) prevents multiple dApps from committing simultaneously. If your dApp needs to commit but the lock is held by another dApp:<\/p>\n<ul>\n<li>Pass <code>breakCommit = true<\/code> to force-break the existing lock (use sparingly).<\/li>\n<li>Or listen for <code>eCommitState.none<\/code> \/ <code>eCommitState.success<\/code> to wait for the current commit to complete.<\/li>\n<\/ul>\n<h3>Thread Safety<\/h3>\n<p>GRIDNET OS operations targeting the <strong>system thread<\/strong> are blocked during pending commits (the processVMMetaDataKF method checks the commit lock). Operations on the <strong>data thread<\/strong> or sub-threads are <em>not<\/em> blocked \u2014 they are treated as read-only or non-conflicting.<\/p>\n<h3>Missing Directories<\/h3>\n<p>Always use <code>dontThrow = true<\/code> when navigating to directories that might not exist:<\/p>\n<pre>\n\/\/ Safe navigation \u2014 won't crash the VM if directory doesn't exist\nfs.doCD('possibly-missing-dir', true, false, true, this.getThreadID);\n<\/pre>\n<h2>Part VI: Performance Patterns<\/h2>\n<h3>Atomic CD+LS<\/h3>\n<p>Always use <code>doCD(path, true)<\/code> instead of separate <code>doCD()<\/code> + <code>doLS()<\/code> calls. The combined operation saves a full network round trip \u2014 the full node executes both atomically.<\/p>\n<h3>Debounced Saves<\/h3>\n<p>For real-time editors, debounce file updates to avoid flooding the network:<\/p>\n<pre>\nlet saveTimeout = null;\neditor.oninput = () =&gt; {\n  clearTimeout(saveTimeout);\n  saveTimeout = setTimeout(() =&gt; {\n    fs.updateFile('notes.txt', editor.value, false, threadID);\n  }, 500); \/\/ Save at most every 500ms\n};\n<\/pre>\n<h3>Request ID Tracking<\/h3>\n<p>Use <code>addNetworkRequestID()<\/code> to track which async responses belong to your dApp. This is especially important when multiple dApps are running simultaneously \u2014 each may receive DFS responses meant for other dApps.<\/p>\n<h3>Swarm Data Serialization<\/h3>\n<p>When sending data through swarms, always serialize to <code>ArrayBuffer<\/code> via <code>CTools.getInstance().convertToArrayBuffer()<\/code>. JSON serialization works well for structured messages:<\/p>\n<pre>\n\/\/ Efficient swarm message pattern\nconst payload = JSON.stringify({ type: 'cursor', pos: 42, user: 'Alice' });\nswarm.sendData(tools.convertToArrayBuffer(payload), null, false);\n<\/pre>\n<h3>Cleanup on Close<\/h3>\n<p>Always override <code>closeWindow()<\/code> to clean up listeners and swarm registrations:<\/p>\n<pre>\ncloseWindow() {\n  \/\/ Unregister from swarms\n  if (this.mSwarmManager) {\n    this.mSwarmManager.unregisterProcessFromSwarms(this.getProcessID);\n  }\n  \/\/ Unregister event listeners by appID\n  if (this.mSwarmManager) {\n    this.mSwarmManager.unregisterEventListenersByAppID(this.getProcessID);\n  }\n  \/\/ Call parent cleanup\n  super.closeWindow();\n}\n<\/pre>\n<h3>Lazy Sync<\/h3>\n<p>Don&#8217;t call <code>doSync()<\/code> on every operation. Sync pulls the entire committed state from the blockchain \u2014 it&#8217;s expensive. Use it:<\/p>\n<ul>\n<li>On dApp startup (to get the latest state).<\/li>\n<li>After reconnection (to catch up on changes made while disconnected).<\/li>\n<li>Periodically in long-running dApps (every 30-60 seconds at most).<\/li>\n<\/ul>\n<h2>Quick Reference Card<\/h2>\n<table style=\"width:100%; border-collapse: collapse; font-family: monospace; font-size: 0.85em;\">\n<tr style=\"background: #1a2a3a; color: #00f0ff;\">\n<th style=\"padding: 8px; text-align: left; border: 1px solid #2a3a4a;\">Operation<\/th>\n<th style=\"padding: 8px; text-align: left; border: 1px solid #2a3a4a;\">Method<\/th>\n<th style=\"padding: 8px; text-align: left; border: 1px solid #2a3a4a;\">Default Thread<\/th>\n<\/tr>\n<tr style=\"color: #e0e0e0;\">\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">Change directory<\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\"><code>fs.doCD(path, doLS, breakCommit, dontThrow, threadID)<\/code><\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">data<\/td>\n<\/tr>\n<tr style=\"color: #e0e0e0;\">\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">List directory<\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\"><code>fs.doLS(threadID, breakCommit)<\/code><\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">data<\/td>\n<\/tr>\n<tr style=\"color: #e0e0e0;\">\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">Create file<\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\"><code>fs.doNewFile(path, content, breakCommit, threadID)<\/code><\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">data<\/td>\n<\/tr>\n<tr style=\"color: #e0e0e0;\">\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">Update file<\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\"><code>fs.updateFile(path, content, breakCommit, threadID)<\/code><\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">data<\/td>\n<\/tr>\n<tr style=\"color: #e0e0e0;\">\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">Read file<\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\"><code>fs.doGetFile(path, breakCommit, threadID)<\/code><\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">data<\/td>\n<\/tr>\n<tr style=\"color: #e0e0e0;\">\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">Create directory<\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\"><code>fs.doNewDir(path, breakCommit, threadID)<\/code><\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">data<\/td>\n<\/tr>\n<tr style=\"color: #e0e0e0;\">\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">Commit to chain<\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\"><code>fs.doCommit(breakCommit, threadID)<\/code><\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">system<\/td>\n<\/tr>\n<tr style=\"color: #e0e0e0;\">\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">Sync from chain<\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\"><code>fs.doSync(breakCommit, threadID)<\/code><\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">system<\/td>\n<\/tr>\n<tr style=\"color: #e0e0e0;\">\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">Join swarm<\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\"><code>mgr.joinSwarm(swarmID, userID, privKey, caps, appInstance)<\/code><\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">\u2014<\/td>\n<\/tr>\n<tr style=\"color: #e0e0e0;\">\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">Leave swarm<\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\"><code>mgr.leaveSwarm(trueSwarmID)<\/code><\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">\u2014<\/td>\n<\/tr>\n<tr style=\"color: #e0e0e0;\">\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">Send swarm data<\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\"><code>swarm.sendData(data, target, onlyAuthenticated)<\/code><\/td>\n<td style=\"padding: 6px; border: 1px solid #2a3a4a;\">\u2014<\/td>\n<\/tr>\n<\/table>\n<h2>What Comes Next<\/h2>\n<p>You now have the tools to build real, decentralized applications. Files that persist on a blockchain. Messages that travel directly between browsers. Swarms that connect peers in real-time mesh networks. No servers. No cloud. No middlemen.<\/p>\n<p>In the next article in this series, we will explore <strong>GridScript in depth<\/strong> \u2014 the native scripting language of GRIDNET OS. You will learn how to write smart contracts, define custom transaction logic, and extend the blockchain&#8217;s behavior programmatically. GridScript is where the decentralized state machine truly becomes programmable.<\/p>\n<p>But first: take the Collaborative Notes dApp above, deploy it, and <strong>use it<\/strong>. Break it. Extend it. Add user cursors. Add version history. Add file sharing between accounts. The platform is there. Now build.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Introduction: What &#8220;Building Real Things&#8221; Means on a Decentralized OS In the previous articles of this series, you learned the fundamentals: how&#8230;<\/p>\n","protected":false},"author":1,"featured_media":835550,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[162,17,163],"tags":[197,160,172,216,142,217,173,170,212,171],"class_list":["post-835554","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-development","category-tutorial","category-tutorials","tag-dapps","tag-developer-guide","tag-dfs","tag-file-sharing","tag-gridnet-os","tag-messaging","tag-p2p","tag-ui-dapps","tag-ui-development","tag-webrtc"],"_links":{"self":[{"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/posts\/835554","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/comments?post=835554"}],"version-history":[{"count":3,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/posts\/835554\/revisions"}],"predecessor-version":[{"id":835597,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/posts\/835554\/revisions\/835597"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/media\/835550"}],"wp:attachment":[{"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/media?parent=835554"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/categories?post=835554"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/mag.gridnet.org\/index.php\/wp-json\/wp\/v2\/tags?post=835554"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}