🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

yaob

Package Overview
Dependencies
Maintainers
6
Versions
18
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

yaob - npm Package Compare versions

Comparing version
0.3.10
to
0.3.11
+372
src/BridgeState.js
/* global setTimeout */
// @flow
import type { BridgeOptions, SendMessage } from './bridge.js'
import { type ObjectTable, packData, packThrow, unpackData } from './data.js'
import { bridgifyClass, getInstanceMagic } from './magic.js'
import { close, emit, update } from './manage.js'
import {
type ValueCache,
diffObject,
dirtyValue,
makeProxy,
packObject,
updateObjectProps
} from './objects.js'
import type {
CallMessage,
ChangeMessage,
CreateMessage,
EventMessage,
Message,
ReturnMessage
} from './protocol.js'
export class BridgeState implements ObjectTable {
// Options:
+hideProperties: string[]
+sendMessage: SendMessage
+throttleMs: number
// Objects:
+proxies: { [objectId: number]: Object }
+objects: { [localId: number]: Object }
+caches: { [localId: number]: ValueCache }
// Outgoing method calls:
nextCallId: number
pendingCalls: {
[callId: number]: { name: string, resolve: Function, reject: Function }
}
// Pending message:
dirty: { [localId: number]: { cache: ValueCache, object: Object } }
message: Message
// Update scheduling:
closed: boolean
lastUpdate: number
sendPending: boolean
constructor(opts: BridgeOptions) {
const { hideProperties = [], sendMessage, throttleMs = 0 } = opts
// Options:
this.hideProperties = hideProperties
this.sendMessage = sendMessage
this.throttleMs = throttleMs
// Objects:
this.proxies = {}
this.objects = {}
this.caches = {}
// Outgoing method calls:
this.nextCallId = 0
this.pendingCalls = {}
// Pending message:
this.dirty = {}
this.message = {}
// Update scheduling:
this.lastUpdate = 0
this.sendPending = false
}
/**
* Close the bridge, so it will no longer send messages.
* This also closes all proxies created by the bridge and rejects
* all pending calls.
*/
close(error: Error): void {
for (const callId in this.pendingCalls) {
const call = this.pendingCalls[Number(callId)]
call.reject(error)
}
for (const objectId in this.proxies) {
close(this.proxies[Number(objectId)])
}
this.closed = true
}
/**
* Grabs an object by its proxy id.
*/
getObject(packedId: number): Object | void {
return packedId < 0 ? this.proxies[-packedId] : this.objects[packedId]
}
/**
* Returns an object's id relative to this bridge.
* The id is positive for objects created on this side of the bridge,
* and negative for proxy objects reflecting things on the other side.
*/
getPackedId(o: Object): number | null {
const magic = getInstanceMagic(o)
if (magic.closed) return null
if (magic.remoteId != null && this.proxies[magic.remoteId] != null) {
return -magic.remoteId
}
if (this.objects[magic.localId] == null) {
// Add unknown objects to the bridge:
this.objects[magic.localId] = o
const { cache, create } = packObject(this, o)
this.caches[magic.localId] = cache
magic.bridges.push(this)
this.emitCreate(create, o)
}
return magic.localId
}
/**
* Marks an object as needing changes.
*/
markDirty(localId: number, name?: string): void {
const cache = this.caches[localId]
if (name != null && name in cache) cache[name] = dirtyValue
this.dirty[localId] = { cache, object: this.objects[localId] }
this.wakeup()
}
/**
* Marks an object as being deleted.
*/
emitClose(localId: number): void {
delete this.objects[localId]
delete this.caches[localId]
if (this.message.closed == null) this.message.closed = []
this.message.closed.push(localId)
this.wakeup()
}
/**
* Attaches an object to this bridge, sending a creation message.
*/
emitCreate(create: CreateMessage, o: Object): void {
if (this.message.created == null) this.message.created = []
this.message.created.push(create)
// this.wakeup() not needed, since this is part of data packing.
}
/**
* Enqueues a proxy call message.
*/
emitCall(remoteId: number, name: string, args: mixed): Promise<mixed> {
const callId = this.nextCallId++
const message: CallMessage = {
callId,
remoteId,
name,
...packData(this, args)
}
if (this.message.calls == null) this.message.calls = []
this.message.calls.push(message)
this.wakeup()
return new Promise((resolve, reject) => {
this.pendingCalls[callId] = { name, resolve, reject }
})
}
/**
* Enqueues an event message.
*/
emitEvent(localId: number, name: string, payload: mixed): void {
const message: EventMessage = {
localId,
name,
...packData(this, payload)
}
if (this.message.events == null) this.message.events = []
this.message.events.push(message)
this.wakeup()
}
/**
* Enqueues a function return message.
*/
emitReturn(callId: number, fail: boolean, value: mixed): void {
const message: ReturnMessage = {
callId,
...(fail ? packThrow(this, value) : packData(this, value))
}
if (this.message.returns == null) this.message.returns = []
this.message.returns.push(message)
this.wakeup()
}
/**
* Handles an incoming message,
* updating state and triggering side-effects as needed.
*/
handleMessage(message: Message): void {
// ----------------------------------------
// Phase 1: Get our proxies up to date.
// ----------------------------------------
// Handle newly-created objects:
if (message.created) {
// Pass 1: Create proxies for the new objects:
for (const create of message.created) {
this.proxies[create.localId] = makeProxy(this, create)
}
// Pass 2: Fill in the values:
for (const create of message.created) {
updateObjectProps(this, this.proxies[create.localId], create.props)
}
}
// Handle updated objects:
if (message.changed) {
// Pass 1: Update all the proxies:
for (const change of message.changed) {
const { localId, props } = change
const o = this.proxies[localId]
if (o == null) {
throw new RangeError(`Invalid localId ${localId}`)
}
updateObjectProps(this, o, props)
}
// Pass 2: Fire the callbacks:
for (const change of message.changed) {
update(this.proxies[change.localId])
}
}
// ----------------------------------------
// Phase 2: Handle events & method calls
// ----------------------------------------
// Handle events:
if (message.events) {
for (const event of message.events) {
const { localId, name } = event
const o = localId === 0 ? this : this.proxies[localId]
if (o == null) continue
try {
emit(o, name, unpackData(this, event, name))
} catch (e) {
emit(o, 'error', e) // Payload unpacking problem
}
}
}
// Handle method calls:
if (message.calls) {
for (const call of message.calls) {
const { callId, remoteId, name } = call
try {
const o = this.objects[remoteId]
if (o == null) {
throw new TypeError(
`Cannot call method '${name}' of closed proxy (remote)`
)
}
if (typeof o[name] !== 'function') {
throw new TypeError(`'${name}' is not a function`)
}
const args = unpackData(this, call, `${name}.arguments`)
Promise.resolve(o[name].apply(o, args)).then(
value => this.emitReturn(callId, false, value),
e => this.emitReturn(callId, true, e)
)
} catch (e) {
this.emitReturn(callId, true, e)
}
}
}
// Handle method returns:
if (message.returns) {
for (const ret of message.returns) {
const { callId } = ret
const pendingCall = this.pendingCalls[callId]
if (pendingCall == null) {
throw new RangeError(`Invalid callId ${callId}`)
}
try {
pendingCall.resolve(
unpackData(this, ret, `${pendingCall.name}.return`)
)
} catch (e) {
pendingCall.reject(e)
} finally {
delete this.pendingCalls[callId]
}
}
}
// ----------------------------------------
// Phase 3: Clean up closed objects
// ----------------------------------------
if (message.closed) {
for (const localId of message.closed) {
const o = this.proxies[localId]
if (o == null) return
delete this.proxies[localId]
close(o)
}
}
}
/**
* Sends the current message.
*/
sendNow(): void {
if (this.closed) return
// Build change messages:
for (const id in this.dirty) {
const localId = Number(id)
const { object, cache } = this.dirty[localId]
const { dirty, props } = diffObject(this, object, cache)
if (dirty) {
const message: ChangeMessage = { localId, props }
if (this.message.changed == null) this.message.changed = []
this.message.changed.push(message)
}
}
const message = this.message
this.dirty = {}
this.message = {}
if (
message.calls != null ||
message.changed != null ||
message.closed != null ||
message.created != null ||
message.events != null ||
message.returns != null
) {
this.sendMessage(message)
}
}
/**
* Something has changed, so prepare to send the pending message:
*/
wakeup(): void {
if (this.sendPending) return
this.sendPending = true
const task = (): void => {
this.sendPending = false
this.lastUpdate = Date.now()
this.sendNow()
}
// We really do want `setTimeout` here, even if the delay is 0,
// since promises and other micro tasks should fire first.
const delay = this.lastUpdate + this.throttleMs - Date.now()
setTimeout(task, delay < 0 ? 0 : delay)
}
}
bridgifyClass(BridgeState)
+4
-0
# Changelog
## 0.3.11 (2023-07-27)
- fixed: Do not send blank messages over the bridge.
## 0.3.10 (2023-01-24)

@@ -4,0 +8,0 @@

+4
-5

@@ -1141,3 +1141,6 @@ 'use strict';

this.message = {};
this.sendMessage(message);
if (message.calls != null || message.changed != null || message.closed != null || message.created != null || message.events != null || message.returns != null) {
this.sendMessage(message);
}
}

@@ -1173,6 +1176,2 @@ /**

/**
* The bridge sends messages using this function.
*/
/**
* An object bridge.

@@ -1179,0 +1178,0 @@ */

@@ -1137,3 +1137,6 @@ import { base64 } from 'rfc4648';

this.message = {};
this.sendMessage(message);
if (message.calls != null || message.changed != null || message.closed != null || message.created != null || message.events != null || message.returns != null) {
this.sendMessage(message);
}
}

@@ -1169,6 +1172,2 @@ /**

/**
* The bridge sends messages using this function.
*/
/**
* An object bridge.

@@ -1175,0 +1174,0 @@ */

{
"name": "yaob",
"version": "0.3.10",
"version": "0.3.11",
"description": "Bridges an object-oriented API across a messaging layer",

@@ -5,0 +5,0 @@ "repository": {

// @flow
import { BridgeState } from './BridgeState.js'
import { packData, unpackData } from './data.js'
import { addListener } from './manage.js'
import { type Message } from './protocol.js'
import { BridgeState } from './state.js'

@@ -8,0 +8,0 @@ /**

@@ -32,3 +32,3 @@ // @flow

_close() {
_close(): void {
close(this)

@@ -44,3 +44,3 @@ }

_update(name?: $Keys<Props>) {
_update(name?: $Keys<Props>): void {
update(this, name)

@@ -47,0 +47,0 @@ }

@@ -8,3 +8,3 @@ // @flow

import type { BridgeState } from './state.js'
import type { BridgeState } from './BridgeState.js'

@@ -73,3 +73,6 @@ // An object is bridgeable if it has this key:

*/
function addMagic(o: Object, magic: ClassMagic | ObjectMagic | SharedMagic) {
function addMagic(
o: Object,
magic: ClassMagic | ObjectMagic | SharedMagic
): void {
if (Object.prototype.hasOwnProperty.call(o, MAGIC_KEY)) {

@@ -147,3 +150,3 @@ Object.assign(o[MAGIC_KEY], magic)

namespace?: string
) {
): void {
if (namespace == null) namespace = ''

@@ -150,0 +153,0 @@ else namespace += '.'

@@ -149,3 +149,3 @@ // @flow

emitError: boolean
) {
): void {
try {

@@ -156,3 +156,3 @@ const out = f(payload)

if (emitError && out != null && typeof out.then === 'function') {
out.then(undefined, e => emit(o, 'error', e))
out.then(undefined, (e: any) => emit(o, 'error', e))
}

@@ -159,0 +159,0 @@ } catch (e) {

@@ -8,2 +8,3 @@ // @flow

import type { BridgeState } from './BridgeState.js'
import { packData, packThrow, unpackData } from './data.js'

@@ -17,3 +18,2 @@ import {

import type { CreateMessage, PackedProps } from './protocol.js'
import type { BridgeState } from './state.js'

@@ -162,3 +162,3 @@ export type ValueCache = { [name: string]: mixed }

function makeProxyMethod(state: BridgeState, magic: ProxyMagic, name: string) {
return function method(...args) {
return function method(...args: any[]): Promise<mixed> {
if (magic.closed) {

@@ -165,0 +165,0 @@ return Promise.reject(

/* global setTimeout */
// @flow
import type { BridgeOptions, SendMessage } from './bridge.js'
import { type ObjectTable, packData, packThrow, unpackData } from './data.js'
import { bridgifyClass, getInstanceMagic } from './magic.js'
import { close, emit, update } from './manage.js'
import {
type ValueCache,
diffObject,
dirtyValue,
makeProxy,
packObject,
updateObjectProps
} from './objects.js'
import type {
CallMessage,
ChangeMessage,
CreateMessage,
EventMessage,
Message,
ReturnMessage
} from './protocol.js'
export class BridgeState implements ObjectTable {
// Options:
+hideProperties: string[]
+sendMessage: SendMessage
+throttleMs: number
// Objects:
+proxies: { [objectId: number]: Object }
+objects: { [localId: number]: Object }
+caches: { [localId: number]: ValueCache }
// Outgoing method calls:
nextCallId: number
pendingCalls: {
[callId: number]: { name: string, resolve: Function, reject: Function }
}
// Pending message:
dirty: { [localId: number]: { cache: ValueCache, object: Object } }
message: Message
// Update scheduling:
closed: boolean
lastUpdate: number
sendPending: boolean
constructor(opts: BridgeOptions) {
const { hideProperties = [], sendMessage, throttleMs = 0 } = opts
// Options:
this.hideProperties = hideProperties
this.sendMessage = sendMessage
this.throttleMs = throttleMs
// Objects:
this.proxies = {}
this.objects = {}
this.caches = {}
// Outgoing method calls:
this.nextCallId = 0
this.pendingCalls = {}
// Pending message:
this.dirty = {}
this.message = {}
// Update scheduling:
this.lastUpdate = 0
this.sendPending = false
}
/**
* Close the bridge, so it will no longer send messages.
* This also closes all proxies created by the bridge and rejects
* all pending calls.
*/
close(error: Error) {
for (const callId in this.pendingCalls) {
const call = this.pendingCalls[Number(callId)]
call.reject(error)
}
for (const objectId in this.proxies) {
close(this.proxies[Number(objectId)])
}
this.closed = true
}
/**
* Grabs an object by its proxy id.
*/
getObject(packedId: number): Object | void {
return packedId < 0 ? this.proxies[-packedId] : this.objects[packedId]
}
/**
* Returns an object's id relative to this bridge.
* The id is positive for objects created on this side of the bridge,
* and negative for proxy objects reflecting things on the other side.
*/
getPackedId(o: Object): number | null {
const magic = getInstanceMagic(o)
if (magic.closed) return null
if (magic.remoteId != null && this.proxies[magic.remoteId] != null) {
return -magic.remoteId
}
if (this.objects[magic.localId] == null) {
// Add unknown objects to the bridge:
this.objects[magic.localId] = o
const { cache, create } = packObject(this, o)
this.caches[magic.localId] = cache
magic.bridges.push(this)
this.emitCreate(create, o)
}
return magic.localId
}
/**
* Marks an object as needing changes.
*/
markDirty(localId: number, name?: string): void {
const cache = this.caches[localId]
if (name != null && name in cache) cache[name] = dirtyValue
this.dirty[localId] = { cache, object: this.objects[localId] }
this.wakeup()
}
/**
* Marks an object as being deleted.
*/
emitClose(localId: number): void {
delete this.objects[localId]
delete this.caches[localId]
if (this.message.closed == null) this.message.closed = []
this.message.closed.push(localId)
this.wakeup()
}
/**
* Attaches an object to this bridge, sending a creation message.
*/
emitCreate(create: CreateMessage, o: Object): void {
if (this.message.created == null) this.message.created = []
this.message.created.push(create)
// this.wakeup() not needed, since this is part of data packing.
}
/**
* Enqueues a proxy call message.
*/
emitCall(remoteId: number, name: string, args: mixed): Promise<mixed> {
const callId = this.nextCallId++
const message: CallMessage = {
callId,
remoteId,
name,
...packData(this, args)
}
if (this.message.calls == null) this.message.calls = []
this.message.calls.push(message)
this.wakeup()
return new Promise((resolve, reject) => {
this.pendingCalls[callId] = { name, resolve, reject }
})
}
/**
* Enqueues an event message.
*/
emitEvent(localId: number, name: string, payload: mixed): void {
const message: EventMessage = {
localId,
name,
...packData(this, payload)
}
if (this.message.events == null) this.message.events = []
this.message.events.push(message)
this.wakeup()
}
/**
* Enqueues a function return message.
*/
emitReturn(callId: number, fail: boolean, value: mixed): void {
const message: ReturnMessage = {
callId,
...(fail ? packThrow(this, value) : packData(this, value))
}
if (this.message.returns == null) this.message.returns = []
this.message.returns.push(message)
this.wakeup()
}
/**
* Handles an incoming message,
* updating state and triggering side-effects as needed.
*/
handleMessage(message: Message): void {
// ----------------------------------------
// Phase 1: Get our proxies up to date.
// ----------------------------------------
// Handle newly-created objects:
if (message.created) {
// Pass 1: Create proxies for the new objects:
for (const create of message.created) {
this.proxies[create.localId] = makeProxy(this, create)
}
// Pass 2: Fill in the values:
for (const create of message.created) {
updateObjectProps(this, this.proxies[create.localId], create.props)
}
}
// Handle updated objects:
if (message.changed) {
// Pass 1: Update all the proxies:
for (const change of message.changed) {
const { localId, props } = change
const o = this.proxies[localId]
if (o == null) {
throw new RangeError(`Invalid localId ${localId}`)
}
updateObjectProps(this, o, props)
}
// Pass 2: Fire the callbacks:
for (const change of message.changed) {
update(this.proxies[change.localId])
}
}
// ----------------------------------------
// Phase 2: Handle events & method calls
// ----------------------------------------
// Handle events:
if (message.events) {
for (const event of message.events) {
const { localId, name } = event
const o = localId === 0 ? this : this.proxies[localId]
if (o == null) continue
try {
emit(o, name, unpackData(this, event, name))
} catch (e) {
emit(o, 'error', e) // Payload unpacking problem
}
}
}
// Handle method calls:
if (message.calls) {
for (const call of message.calls) {
const { callId, remoteId, name } = call
try {
const o = this.objects[remoteId]
if (o == null) {
throw new TypeError(
`Cannot call method '${name}' of closed proxy (remote)`
)
}
if (typeof o[name] !== 'function') {
throw new TypeError(`'${name}' is not a function`)
}
const args = unpackData(this, call, `${name}.arguments`)
Promise.resolve(o[name].apply(o, args)).then(
value => this.emitReturn(callId, false, value),
e => this.emitReturn(callId, true, e)
)
} catch (e) {
this.emitReturn(callId, true, e)
}
}
}
// Handle method returns:
if (message.returns) {
for (const ret of message.returns) {
const { callId } = ret
const pendingCall = this.pendingCalls[callId]
if (pendingCall == null) {
throw new RangeError(`Invalid callId ${callId}`)
}
try {
pendingCall.resolve(
unpackData(this, ret, `${pendingCall.name}.return`)
)
} catch (e) {
pendingCall.reject(e)
} finally {
delete this.pendingCalls[callId]
}
}
}
// ----------------------------------------
// Phase 3: Clean up closed objects
// ----------------------------------------
if (message.closed) {
for (const localId of message.closed) {
const o = this.proxies[localId]
if (o == null) return
delete this.proxies[localId]
close(o)
}
}
}
/**
* Sends the current message.
*/
sendNow(): void {
if (this.closed) return
// Build change messages:
for (const id in this.dirty) {
const localId = Number(id)
const { object, cache } = this.dirty[localId]
const { dirty, props } = diffObject(this, object, cache)
if (dirty) {
const message: ChangeMessage = { localId, props }
if (this.message.changed == null) this.message.changed = []
this.message.changed.push(message)
}
}
const message = this.message
this.dirty = {}
this.message = {}
this.sendMessage(message)
}
/**
* Something has changed, so prepare to send the pending message:
*/
wakeup(): void {
if (this.sendPending) return
this.sendPending = true
const task = () => {
this.sendPending = false
this.lastUpdate = Date.now()
this.sendNow()
}
// We really do want `setTimeout` here, even if the delay is 0,
// since promises and other micro tasks should fire first.
const delay = this.lastUpdate + this.throttleMs - Date.now()
setTimeout(task, delay < 0 ? 0 : delay)
}
}
bridgifyClass(BridgeState)

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display