Konubinix' opinionated web of thoughts

Offline First

Fleeting

often using crdt

yjs

discovering

in the command line, with deno

import { Doc } from "https://cdn.skypack.dev/yjs@^13.5.0"
import { WebsocketProvider } "https://cdn.skypack.dev/y-websocket@^1.3.0"

const doc = new Doc();
const ymap = doc.getMap("my-map")

ymap.observe(ymapEvent => {
    ymapEvent.target === ymap // => true

    // Find out what changed:
    // Option 1: A set of keys that changed
    ymapEvent.keysChanged // => Set
    // Option 2: Compute the differences
    ymapEvent.changes.keys // => Map

    // Sample code.
    ymapEvent.changes.keys.forEach((change, key) => {
        if (change.action === 'add') {
            console.log(`Property "${key}" was added. Initial value: "${ymap.get(key)}".`);
        } else if (change.action === 'update') {
            console.log(`Property "${key}" was updated. New value: "${ymap.get(key)}". Previous value: "${change.oldValue}".`);
        } else if (change.action === 'delete') {
            console.log(`Property "${key}" was deleted. New value: undefined. Previous value: "${change.oldValue}".`);
        }
    })
});

const wsProvider = new WebsocketProvider(
    'ws://192.168.2.14:9905', 'my-roomname',
    doc,
    //    { WebSocketPolyfill: ws }
)
wsProvider.on('status', event => {
    console.log(event.status) ;
})

wsProvider.on('sync', event => {
    console.log('sync', event) ;
    console.log("ymap", ymap.toJSON() )
    ymap.set("i", "chose")
    ymap.delete("g")
    console.log("ymap", ymap.toJSON() )

})

deno run –allow-all /home/sam/test/next/a.js

Yjs was already imported. This breaks constructor checks and will lead to issues! - https://github.com/yjs/yjs/issues/438         │E/IMGSRV  ( 1619): :0: DoKickTA: SGXKickTA() failed with error 9
Yjs was already imported. Importing different versions of Yjs often leads to issues.

This log will arise, because yjs is imported once and also as a dependency of y-websocket. Using a bundler may help solve this.

with python and pycrdt and pycrdt_websocket

import asyncio
import logging

from httpx_ws import aconnect_ws
from pycrdt import Doc, Map
from pycrdt_websocket import WebsocketProvider
from pycrdt_websocket.websocket import HttpxWebsocket

logging.basicConfig(level=logging.DEBUG)


def callback(event):
    print(event.keys)
    print(ymap)
    loop = asyncio.get_event_loop()
    loop.create_task(ch())


async def ch():
    print("ch")
    if "i" in ymap:
        del ymap["i"]


ydoc = Doc()
ymap = ydoc.get("my-map", type=Map)
ymap.observe(callback)


async def do():
    # ymap["key"] = "biduoe"
    ymap["g"] = "a"


async def client():
    room_name = "my-roomname"
    async with (
            aconnect_ws(f"http://192.168.2.14:9905/{room_name}") as websocket,
            WebsocketProvider(ydoc, HttpxWebsocket(websocket, room_name)),
    ):
        loop = asyncio.get_event_loop()
        loop.create_task(do())

        await asyncio.Future()  # run forever


asyncio.run(client())

in a browser with bun

I need a temporary project

{ echo ; echo ; } | bun init
bun install yjs
bun install y-websocket
bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit

package name (bun): entry point (index.ts):
Done! A package.json file was saved in the current directory.
 + index.ts
 + tsconfig.json (for editor auto-complete)
 + README.md

To get started, run:
  bun run index.ts
bun add v1.1.38 (bf2f153f)
  🔍 Resolving [1/1]   🔍 yjs [2/2]   🔍 lib0 [3/3]   🔍 isomorphic.js [4/3]   🔍 isomorphic.js [4/3]   🔒 Saving lockfile... 
installed yjs@13.6.27

3 packages installed [936.00ms]
bun add v1.1.38 (bf2f153f)
  🔍 Resolving [1/1]   🔍 y-websocket [2/2]   🔍 y-protocols [3/2]   🔒 Saving lockfile... 
installed y-websocket@3.0.0

2 packages installed [762.00ms]

Then, I can load and populate window

export { Doc } from "yjs"
export { WebsocketProvider } from "y-websocket"

Once put in index.ts I can run

bun build --target browser index.ts > yjs.js

Then, I can load yjs.js in the browser and run the same code that I used with deno.

<html>
  <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script type="module" src="./yjs.js"></script>
    <script type="module">
      import { Doc, WebsocketProvider } from './yjs.js'

      const doc = new Doc();
      const ymap = doc.getMap("my-map")

      ymap.observe(ymapEvent => {
          ymapEvent.target === ymap // => true

          // Find out what changed:
          // Option 1: A set of keys that changed
          ymapEvent.keysChanged // => Set
          // Option 2: Compute the differences
          ymapEvent.changes.keys // => Map

          // Sample code.
          ymapEvent.changes.keys.forEach((change, key) => {
              if (change.action === 'add') {
                  console.log(`Property "${key}" was added. Initial value: "${ymap.get(key)}".`);
              } else if (change.action === 'update') {
                  console.log(`Property "${key}" was updated. New value: "${ymap.get(key)}". Previous value: "${change.oldValue}".`);
              } else if (change.action === 'delete') {
                  console.log(`Property "${key}" was deleted. New value: undefined. Previous value: "${change.oldValue}".`);
              }
          })
      });

      const wsProvider = new WebsocketProvider(
          'ws://192.168.2.14:9905', 'my-roomname',
          doc,
          //    { WebSocketPolyfill: ws }
      )
      wsProvider.on('status', event => {
          console.log(event.status) ;
      })

      wsProvider.on('sync', event => {
          console.log('sync', event) ;
          console.log("ymap", ymap.toJSON() )
          ymap.set("i", "chose")
          ymap.delete("g")
          console.log("ymap", ymap.toJSON() )

      })

      // I can export the Doc so that it will be used in the dom
      window.doc = doc

    </script>
    <script src="https://hyperscript.org/js/_hyperscript_w9y.min.js"></script>
  </head>
  <body>
    <div _="def update(event)
            set my innerHTML to event.target.get('i')
            end

            init
            doc.getMap('my-map').observe(update)
            ">test</div>
  </body>
</html>

I can see the code working in the browser

gundb

GUN documentation

GUN is fully decentralized (peer-to-peer or multi-master), meaning that changes are not controlled by a centralized server. A server can be just another peer in the network, one that may have more reliable resources than a browser. You save data on one machine, and it will sync it to other peers without needing a complex consensus protocol. It just works.

https://gun.eco/docs/

we recommend you include these dependencies with your app, rather than trusting a public CDN

https://gun.eco/docs/Todo-Dapp

Browsers (and internet firewalls) and even WebRTC, for legacy security reasons, won’t let you directly connect to other machines unless they have a publicly accessible IP address (your localhost might! If you have an IPv6 address and no firewall). To get around this, WebRTC uses public “signaling servers” to coordinate where non-IPv6 peers (like a browser) are, and then attempts to establish a P2P connection if possible.

https://gun.eco/docs/Todo-Dapp

If you want to have meta information about the relationship, simply create an “edge” node that both properties point to instead. Many graph databases do this by default, but because not all data structures require it, gun leaves it to you to specify.

https://gun.eco/docs/Graph-Guide

Am I connected

Am I Connected? (Peer counting) There’s currently no single method provisioned to quickly check whether you’re connected, or to how many peers. However, you can retrieve gun’s backend list of peers, and then filter the list on specific parameters (which might change as Gun is currently reaching an alchemical state of transmutation).

const opt_peers = gun.back(‘opt.peers’); // get peers, as configured in the setup let connectedPeers = _.filter(Object.values(opt_peers), (peer) > { return peer && peer.wire && peer.wire.readyState == 1 && peer.wire.OPEN = 1 && peer.wire.constructor.name = ‘WebSocket’; }); The length of connectedPeers corresponds to the connected peers, which you can now use in your UI.

Reconnecting Here again, due to convoluted reasons which need high priority addressing, after going offline and then hopping back on, GUN doesn’t reliably re-connect to its peers (unless you’d refresh the page/app). In some cases, the peers even get removed from the opt.peers list which we’ve accessed above to count connected peers.

It’s being debated how to approach this most reasonably. In the meantime you can trigger a reconnect by re-adding the peers to GUN’s opt list using the following code:

gun.opt({peers: [‘http://server1.com/gun’, ‘http://server2.com/gun’]}); // re-add peers to GUN-options

gun.get(‘heartbeat’).put(“heartbeat”) // optional: tell GUN to put something small, forcing GUN to establish connection, as GUN is lazy by nature to make it save on data transfer. In the case of counting your peers using the previous example, after reconnecting you’ll see the peer count go back up.

https://gun.eco/docs/API#options

Notes linking here