Skip to content

Commit f3e2743

Browse files
committed
feat(core): support declarative remote configs for instance IPC (#9546)
- Removed the singleton-only restriction from `remote` config in `src/core/Base.mjs`. - Updated `initRemote` to pre-generate proxy functions directly onto the `this.remote` object for non-singletons, resolving `this.remoteId` dynamically at call time. - Implemented robust intent-driven JSDoc comments to document the Instance-to-Instance Handshake pattern. - Refactored `Neo.data.Pipeline` to define its IPC methods declaratively via `static config = { remote: { data: ['create', 'read', 'update'] } }`. - Replaced the procedural `generateRemote` hacks in Pipeline methods with clean, pre-bound `this.remote.data[operation]()` calls.
1 parent 2b9b44b commit f3e2743

3 files changed

Lines changed: 86 additions & 58 deletions

File tree

src/core/Base.mjs

Lines changed: 63 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -453,24 +453,6 @@ class Base {
453453
return value
454454
}
455455

456-
/**
457-
* Triggered before the remote config gets changed
458-
* @param {Object|null} value
459-
* @param {Object|null} oldValue
460-
* @returns {Object|null}
461-
* @protected
462-
*/
463-
beforeSetRemote(value, oldValue) {
464-
let me = this;
465-
466-
// Only allow remote access for singletons or main thread addons
467-
if (value && !me.singleton && !me.isMainThreadAddon) {
468-
throw new Error('Remote method access is only functional for Singleton classes ' + me.className)
469-
}
470-
471-
return value
472-
}
473-
474456
/**
475457
* @param {String} fn The name of a function to find in the passed scope object.
476458
* @param {Object} originName The name of the method inside the originScope.
@@ -644,27 +626,76 @@ class Base {
644626
* @protected
645627
*/
646628
async initRemote() {
647-
let {className, remote} = this,
629+
let me = this,
630+
{className, remote} = me,
648631
{currentWorker} = Neo;
649632

650633
if (!Neo.config.isMiddleware && !Neo.config.unitTestMode) {
651-
if (Neo.workerId !== 'main' && currentWorker.isSharedWorker) {
652-
if (remote.main) {
653-
currentWorker.remotesToRegister.push({className, methods: remote.main})
634+
// SetupClass applies `singleton` to the instance prototype if configured.
635+
if (me.singleton === true) {
636+
// Singleton Routing (Namespace-Driven)
637+
if (Neo.workerId !== 'main' && currentWorker.isSharedWorker) {
638+
if (remote.main) {
639+
currentWorker.remotesToRegister.push({className, methods: remote.main})
640+
}
641+
642+
if (!currentWorker.isConnected) {
643+
await new Promise(resolve => {
644+
currentWorker.on('connected', () => resolve(), me, {once: true})
645+
})
646+
}
647+
} else if (Neo.workerId === 'service') {
648+
if (remote.app) {
649+
currentWorker.remotesToRegister.push({className, methods: remote.app})
650+
}
654651
}
655652

656-
if (!currentWorker.isConnected) {
657-
await new Promise(resolve => {
658-
currentWorker.on('connected', () => resolve(), this, {once: true})
653+
await Base.promiseRemotes(className, remote)
654+
} else {
655+
// Instance-to-Instance Routing (ID-Driven)
656+
// Unlike Singletons which broadcast their existence globally via 'registerRemote',
657+
// instances dynamically build a `me.remote` object containing pre-bound proxy functions.
658+
// This establishes a localized IPC channel for cross-thread architecture (e.g. data.Pipeline).
659+
let remoteObj = {};
660+
661+
Object.entries(remote).forEach(([worker, methods]) => {
662+
remoteObj[worker] = {};
663+
664+
methods.forEach(method => {
665+
remoteObj[worker][method] = (data, buffer) => {
666+
let origin = Neo.workerId === 'main' ? Neo.worker.Manager : Neo.currentWorker,
667+
opts = {
668+
action : 'remoteMethod',
669+
data,
670+
destination : worker,
671+
remoteClassName: className,
672+
remoteMethod : method
673+
};
674+
675+
// The destination ID is resolved at execution time. This accommodates
676+
// the "Handshake" pattern where `me.remoteId` is populated asynchronously
677+
// after the target instance is created in the remote thread.
678+
if (me.remoteId) {
679+
opts.remoteId = me.remoteId
680+
} else if (data?.remoteId) {
681+
opts.remoteId = data.remoteId
682+
}
683+
684+
if (worker === 'main' && data?.windowId) {
685+
opts.destination = data.windowId
686+
}
687+
688+
if (origin.isSharedWorker) {
689+
origin.assignPort(data, opts)
690+
}
691+
692+
return origin.promiseMessage(opts.destination, opts, buffer)
693+
}
659694
})
660-
}
661-
} else if (Neo.workerId === 'service') {
662-
if (remote.app) {
663-
currentWorker.remotesToRegister.push({className, methods: remote.app})
664-
}
665-
}
695+
});
666696

667-
await Base.promiseRemotes(className, remote)
697+
me.remote = remoteObj
698+
}
668699
}
669700
}
670701

src/data/Pipeline.mjs

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,13 @@ class Pipeline extends Base {
6161
* @reactive
6262
*/
6363
parser_: null,
64+
/**
65+
* @member {Object} remote
66+
* @protected
67+
*/
68+
remote: {
69+
data: ['create', 'read', 'update']
70+
},
6471
/**
6572
* The ID of the corresponding Pipeline instance in the remote worker.
6673
* @member {String|null} remoteId=null
@@ -278,13 +285,7 @@ class Pipeline extends Base {
278285
if (me.isDestroyed) return null;
279286

280287
try {
281-
let remoteRead = Neo.currentWorker.generateRemote({
282-
origin : 'data',
283-
className: me.className,
284-
id : me.remoteId
285-
}, 'read');
286-
287-
const response = await remoteRead(params);
288+
const response = await me.remote.data.read(params);
288289

289290
if (response === null && attempt <= maxRemoteRetries) {
290291
// Potential remote instance loss or silent failure
@@ -359,13 +360,7 @@ class Pipeline extends Base {
359360

360361
if (me.isDestroyed) return null;
361362

362-
let remoteMethod = Neo.currentWorker.generateRemote({
363-
origin : 'data',
364-
className: me.className,
365-
id : me.remoteId
366-
}, operation);
367-
368-
return await remoteMethod(params);
363+
return await me.remote.data[operation](params);
369364
} else {
370365
let rawData;
371366

src/worker/mixin/RemoteMethodAccess.mjs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,20 +37,27 @@ import Base from '../../core/Base.mjs';
3737
* the original method is synchronous.
3838
*
3939
* **Routing Modes:**
40-
* RMA supports two distinct routing modes for executing remote methods:
40+
* RMA supports two distinct routing modes for executing remote methods, orchestrated by `Neo.core.Base.initRemote`:
4141
*
4242
* 1. **Singleton Routing (Namespace-Driven):**
43-
* Historically, RMA was used exclusively for singletons. Remote access is resolved via static namespaces
44-
* (e.g., `Neo.ns('Neo.main.addon.LocalStorage')`). The calling thread must know the full class name.
43+
* Historically, RMA was used exclusively for singletons. When a singleton class defines a `remote` config,
44+
* the framework broadcasts a `registerRemote` message. The target threads generate proxy functions and attach
45+
* them directly to the static namespace (e.g., `Neo.main.addon.LocalStorage.readLocalStorageItem`).
46+
* This mode supports an `interceptRemotes` config. Calls arriving before a singleton is `isReady`
47+
* can be intercepted and queued until the singleton is fully functional.
4548
*
4649
* 2. **Instance-to-Instance Routing (ID-Driven):**
4750
* RMA can also route messages to specific class instances across worker boundaries using `remoteId`.
4851
* Because each worker has an isolated memory space and its own `Neo.manager.Instance` registry, an ID
4952
* (like 'pipeline-1') only refers to an object within its local thread.
53+
* For non-singleton instances, `core.Base.initRemote` automatically generates proxy functions and attaches
54+
* them to a `this.remote` object on the instance itself (e.g., `this.remote.data.read()`), rather than
55+
* broadcasting globally.
56+
*
5057
* To establish an instance-to-instance connection, instances must perform a **Handshake**:
5158
* - Thread A creates an instance in Thread B (via `worker.createInstance()`), passing its own local ID in the config.
5259
* - Thread B instantiates the object, registers it locally, and returns its new local ID to Thread A.
53-
* - Now, both instances can use `generateRemote` by providing `id` alongside `origin` (the destination worker).
60+
* - Now, both instances can call their local `this.remote` proxies, which dynamically resolve the destination ID at execution time.
5461
* RMA intercepts messages with a `remoteId` and resolves them dynamically via `Neo.manager.Instance.get(remoteId)`.
5562
*
5663
* **Architectural Note:**
@@ -81,14 +88,9 @@ import Base from '../../core/Base.mjs';
8188
*
8289
* @example
8390
* // 3. Instance-to-Instance Usage
84-
* // App Worker Pipeline generating a proxy to call `read()` on its Data Worker counterpart
85-
* let remoteRead = this.generateRemote({
86-
* origin : 'data', // Target worker
87-
* className: this.className,
88-
* id : this.remoteId // The local ID of the instance in the Data Worker
89-
* }, 'read');
90-
*
91-
* let response = await remoteRead({ page: 1 });
91+
* // App Worker Pipeline calling `read()` on its Data Worker counterpart.
92+
* // The proxy was pre-generated by `core.Base.initRemote` onto `this.remote`.
93+
* let response = await this.remote.data.read({ page: 1 });
9294
*
9395
* @class Neo.worker.mixin.RemoteMethodAccess
9496
* @extends Neo.core.Base

0 commit comments

Comments
 (0)