Putting it together
The same shared notes list as the .NET demo, running against a separate Node.js server
instead. Click the button, the server picks the text and everyone currently viewing this
page sees it appear, live, without refreshing. Open this page in a second tab to check it:
add a note in one, watch it appear in the other, even though only one tab clicked anything.
This Result block below connects to componyx.com/nodebackend/dataserver,
a Node server running as its own /nodebackend application on this site, not this
site's own .NET backend.
Server
// services/notes-controller.js
const { DataResponse, DataRules } = require('../bindary-data');
const UPDATE_NOTES_ID = 'notes';
const SAMPLE_NOTES =
[
'Bindary is neat',
'Hello from the docs',
'This is a shared note',
'Testing the live update',
'Notes stay short',
'Pushed, not polled',
'Nice to see you here',
'Works across tabs',
'Straight from the server',
'No refresh needed'
];
class NotesController
{
#notes = [];
#nextId = 1;
#dataUpdateManager;
#server;
constructor(dataUpdateManager, server)
{
this.#dataUpdateManager = dataUpdateManager;
this.#server = server;
}
registerActions()
{
this.#dataUpdateManager.addUpdateAction(
UPDATE_NOTES_ID, this.#sendNotes.bind(this));
}
// called once, when the demo section first renders
async subscribe(client, request)
{
this.#dataUpdateManager.subscribe(UPDATE_NOTES_ID, client);
await this.#sendNotes(client);
}
// called from the "Add note" button. The client never supplies the
// text, the server always picks it, so there's nothing here for a
// request built by hand, dev console or otherwise, to actually control.
async addNote(client, request)
{
const text = SAMPLE_NOTES[Math.floor(Math.random() * SAMPLE_NOTES.length)];
this.#notes.push({ id: this.#nextId++, text });
if (this.#notes.length > 10)
this.#notes.shift(); // cap growth
// ack, so dataSendAsync resolves
const ack = new DataResponse(request.callbackId);
await this.#server.send(client, ack, request.xhrRequestId);
// push to every subscribed client, including this one
await this.#dataUpdateManager.update(UPDATE_NOTES_ID);
}
async #sendNotes(client)
{
const response = new DataResponse();
const rule = new DataRules();
rule.update = true;
rule.updateKey = 'id';
rule.path = 'demo.nodeBackendDemo.notes';
response.data.appData = { notes: this.#notes };
response.data.appDataRules = { notes: rule };
await this.#server.send(client, response);
}
}
module.exports = { NotesController };
subscribe/addNote are camelCase here, where the .NET version
uses Subscribe/AddNote. The two dataSendAsync calls in
the client script below have to match exactly, the dispatch is a case-sensitive string
lookup against the method name, both sides drift out of sync otherwise.
The server picks the text, not the client. Nothing sent in the request is ever read for
this, so there's no way, dev console included, to make this list say anything the server
didn't already approve.
Registered once, alongside the rest of the server's startup wiring:
const notesController = new NotesController(dataUpdateManager, server);
controllers['Notes.Controller'] = notesController;
notesController.registerActions();
Client
ctrl.subscribeNodeNotes = async () =>
{
await $bindary.dataSendAsync(null, 'Notes.Controller.subscribe');
};
ctrl.addNodeNote = async () =>
{
await $bindary.dataSendAsync(null, 'Notes.Controller.addNote');
};
Same shape as the .NET demo's client code, because it's the same API either way, this
assumes $bindary.dataServerURL already points at this Node server, set once,
normally, the same way any Node.js-backed site configures it.
Named subscribeNodeNotes/addNodeNote rather than
subscribeNotes/addNote deliberately. Docs pages share one
controller, bindary-docs.js, the .NET Backend page's demo already claims those
two names, reusing them here would silently overwrite one or the other depending on load
order. Not something a normal Node.js site would ever need to think about, this only
matters because both demos share one controller file on this particular reference site.
Running this demo alongside the .NET one
This reference site's own default connection already belongs to the .NET backend, that's
what the rest of the site talks to. Getting this one page to talk to the Node server
instead, without breaking every other page, needs some extra glue specific to running two
demos side by side, not something a Node.js-backed site would ever need on its own:
ctrl.setNodeDataServerURL = () =>
{
$bindary.disconnect();
$bindary.relativeDataServerURL = false;
$bindary.dataServerURL = 'componyx.com/nodebackend/dataserver';
$bindary.onUnload.once(ctrl.resetDataServerURL);
};
ctrl.resetDataServerURL = () =>
{
$bindary.disconnect();
$bindary.relativeDataServerURL = true;
$bindary.dataServerURL =
getLocationOrigin().replace(/http(s)*:\/\//, "") + '/DataServer';
};
On this reference site, the real subscribeNodeNotes, in
bindary-docs.js, actually calls ctrl.setNodeDataServerURL(); as
its first line, that's what makes this specific page's demo work. Left out of the sample
above deliberately, a Node.js-backed site copying this pattern for its own use wouldn't add
that line at all, it only exists here because of the dual-demo setup, not because Bindary
needs it.
$bindary.disconnect() isn't optional here. Reassigning dataServerURL
on a connection that's already open, which it is, the site's own backend connects on first
page load, does nothing by itself: sending on an open WebSocket just uses the existing
socket and never looks at dataServerURL again. Only once the connection is
actually closed does the next send reconnect and pick up the new address.
No scheme prefix on the Node URL, not https://, not wss://.
Bindary adds that itself based on secure, prepending it onto whatever's already
there without checking for a duplicate first. Include one here and the connection ends up
pointed at something like wss://https://..., broken.
relativeDataServerURL = false because the address above is written out in full,
host and /nodebackend application path both, rather than the site's own
/DataServer endpoint that resetDataServerURL restores.
$bindary.onUnload.once(...) is what puts the connection back once this section
is no longer showing, without it, navigating away would leave the whole site still pointed
at the Node server. Every route that reassigns dataServerURL needs to restore
it on its own way out, this isn't automatic.
<div b-post-render="subscribeNodeNotes">
<ul>
<li b-repeat="demo.nodeBackendDemo.notes" b-value="text"></li>
</ul>
<a ui-type="Button" ui-text="Add note"
ui-command="$bindary.controller.addNodeNote"></a>
</div>
demo.nodeBackendDemo.notes has to already exist client-side before this
renders. b-repeat resolves its data-key the moment this markup renders, before
subscribeNodeNotes even runs, let alone before its response comes back, so there's
nothing that would create it in time on its own. It's seeded as an empty array in
ctrl.load, in bindary-docs.js, the same shared function that seeds
every other reference page's demo data on this site, demo.backendDemo.notes
included.
Result
Every push here sends the whole list, all ten notes, not just the new one.
updateKey: 'id' is what makes that safe: a matching id gets updated in place, a
new id gets appended, so resending the full list on every push doesn't duplicate anything
already there. path is why this lands at
demo.nodeBackendDemo.notes specifically, rather than merging directly onto
appData's own root.
Sending everything every time is a deliberate simplification for this demo, not the only
way to use this. Sending only what's genuinely new, tracking per-subscriber what a client
has already seen, means less to send as a list grows, with updateKey then
acting as a safety net against an accidental duplicate rather than doing the main work.
Worth reaching for once "resend everything" stops being cheap enough.
updateKey is matched against the property name as it arrives client-side. Here
that's simply 'id', the same as it's written server-side, no casing
translation to get wrong the way the .NET version requires getting right. Get it wrong on
either side regardless and every comparison quietly reads undefined on both
sides, which counts as a match, so the merge always updates whichever item happens to be
first, no error, nothing obviously broken, just the wrong item overwritten every time.