@on-the-ground/proxxy-js 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +90 -0
- package/dist/index.cjs +72 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.mjs +70 -0
- package/package.json +54 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 on-the-ground
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# @on-the-ground/proxxy-js
|
|
2
|
+
|
|
3
|
+
Async proxies that shard an object across N independently-constructed instances and dispatch
|
|
4
|
+
method calls onto per-shard [`@on-the-ground/daemonizer`](https://github.com/on-the-ground/daemonizer)
|
|
5
|
+
`Daemon` queues โ a JS/TS counterpart to [proxxy](https://github.com/on-the-ground/proxxy) (JVM).
|
|
6
|
+
|
|
7
|
+
## ๐ Directory Structure
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
src/
|
|
11
|
+
โโโ index.ts # Entry point
|
|
12
|
+
โโโ proxxy.ts # startProxy
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## โ
Features
|
|
16
|
+
|
|
17
|
+
- Wraps a `constructor` โ not a live instance โ and builds one independent instance per shard (via `new constructor()`), so shards never share mutable state
|
|
18
|
+
- A caller-supplied `keyExtractor` routes each call to a shard; calls routed to the same shard are strictly ordered, calls on different shards run concurrently
|
|
19
|
+
- Every call returns `Promise<Awaited<R>>`, propagating the method's result or thrown error straight back to the caller โ proxxy never swallows or redirects errors, that's the caller's call
|
|
20
|
+
- Cancellation via `AbortSignal`; `close()` returns a `Promise<void>` that resolves once every shard has fully drained
|
|
21
|
+
- Only methods are proxied โ plain property access (get and set) throws, since a single field has no coherent value once state is sharded
|
|
22
|
+
- Proxied methods must use regular method syntax (`method() {}`), not arrow-function class fields (`method = () => {}`) โ see [Method syntax](#method-syntax) below
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
```ts
|
|
27
|
+
import { startProxy } from "@on-the-ground/proxxy-js";
|
|
28
|
+
|
|
29
|
+
class Account {
|
|
30
|
+
private balance = 0;
|
|
31
|
+
// first arg is only used for routing below, the method itself doesn't need it
|
|
32
|
+
async deposit(_accountId: string, amount: number) {
|
|
33
|
+
return (this.balance += amount);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const controller = new AbortController();
|
|
38
|
+
const { proxy, close } = startProxy(Account, controller.signal, {
|
|
39
|
+
partitionCount: 4,
|
|
40
|
+
// calls for the same accountId must land on the same shard to observe each other
|
|
41
|
+
keyExtractor: (_method, args) => hashOf(args[0] as string),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await proxy.deposit("acct-1", 100); // 100
|
|
45
|
+
await proxy.deposit("acct-1", 50); // 150 โ same shard, ordered after the first call
|
|
46
|
+
await proxy.deposit("acct-2", 10); // runs concurrently with acct-1's calls, different shard
|
|
47
|
+
|
|
48
|
+
await close();
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Wrapping a single object with no sharding at all is just the default: omit `partitionCount`
|
|
52
|
+
(defaults to `1`) and `keyExtractor` (defaults to always routing to shard 0).
|
|
53
|
+
|
|
54
|
+
## Method syntax
|
|
55
|
+
|
|
56
|
+
Define proxied methods with regular method syntax:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
class Account {
|
|
60
|
+
async deposit(id: string, amount: number) { /* ... */ } // โ
visible to startProxy
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Not as arrow-function class fields:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
class Account {
|
|
68
|
+
deposit = async (id: string, amount: number) => { /* ... */ }; // โ not recognized
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
`startProxy` tells methods apart from plain properties by checking `constructor.prototype`,
|
|
73
|
+
without constructing a throwaway instance just to inspect its shape. Regular methods live on
|
|
74
|
+
the prototype; arrow-function class fields are assigned per-instance in the constructor, so
|
|
75
|
+
they're invisible until an instance already exists. This costs nothing in practice โ proxxy
|
|
76
|
+
always invokes methods via `fn.apply(instance, args)`, so the usual reason to reach for an
|
|
77
|
+
arrow field (auto-bound `this`) doesn't apply here.
|
|
78
|
+
|
|
79
|
+
## ๐ง Build
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
yarn build
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## ๐งช Testing
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
yarn test # vitest
|
|
89
|
+
yarn typecheck # tsc --noEmit, covers src/ and test/
|
|
90
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var daemonizer = require('@on-the-ground/daemonizer');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Wraps `partitionCount` independent instances (one per shard, built via `constructor`) behind a
|
|
7
|
+
* single proxy. Method calls are routed to a shard by `keyExtractor`; calls on the same shard
|
|
8
|
+
* are strictly ordered, calls on different shards run concurrently. Every call returns a
|
|
9
|
+
* Promise that resolves with the method's result or rejects with whatever it threw.
|
|
10
|
+
*
|
|
11
|
+
* `constructor`'s methods must be defined with regular method syntax (`method() {}`), not as
|
|
12
|
+
* arrow-function class fields (`method = () => {}`) โ only the former is visible on
|
|
13
|
+
* `constructor.prototype`, which is how proxied methods are told apart from plain properties
|
|
14
|
+
* without needing a throwaway instance. This isn't a functional loss here: every call is
|
|
15
|
+
* dispatched via `fn.apply(instance, args)` regardless of method style, so arrow fields'
|
|
16
|
+
* usual benefit (auto-bound `this`) is moot inside proxxy.
|
|
17
|
+
*/
|
|
18
|
+
function startProxy(constructor, signal, options = {}) {
|
|
19
|
+
const partitionCount = options.partitionCount ?? 1;
|
|
20
|
+
const keyExtractor = options.keyExtractor ?? (() => 0);
|
|
21
|
+
const router = new daemonizer.PartitionedDaemon(() => {
|
|
22
|
+
// Built once per partition, not per call โ calls routed to this shard must observe
|
|
23
|
+
// each other's effects on the same instance, not start over from scratch every time.
|
|
24
|
+
const instance = new constructor();
|
|
25
|
+
return new daemonizer.Daemon(signal, async (_ctx, call) => {
|
|
26
|
+
const fn = instance[call.method];
|
|
27
|
+
if (typeof fn !== "function") {
|
|
28
|
+
call.reject(new TypeError(`'${String(call.method)}' is not a method`));
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
call.resolve(await fn.apply(instance, call.args));
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
call.reject(e);
|
|
36
|
+
}
|
|
37
|
+
}, options.bufferSizePerPartition);
|
|
38
|
+
}, partitionCount, (call) => keyExtractor(call.method, call.args));
|
|
39
|
+
const dispatch = (method, args) => new Promise((resolve, reject) => {
|
|
40
|
+
router.pushEvent({ method, args, resolve, reject }).then((accepted) => {
|
|
41
|
+
if (!accepted)
|
|
42
|
+
reject(new Error(`proxxy: cannot dispatch '${String(method)}', proxy is closed`));
|
|
43
|
+
}, reject);
|
|
44
|
+
});
|
|
45
|
+
const proxy = new Proxy({}, {
|
|
46
|
+
get(_target, method) {
|
|
47
|
+
// State lives per-shard now, so there's no single coherent value to hand back for a
|
|
48
|
+
// plain field read โ only methods (routed and queued per shard) are supported. The
|
|
49
|
+
// shape comes straight from the class prototype, so no throwaway instance is needed
|
|
50
|
+
// just to answer "is this a method". This only sees conventional prototype methods โ
|
|
51
|
+
// arrow-function class fields live on the instance, not the prototype, and won't be
|
|
52
|
+
// recognized here.
|
|
53
|
+
const shape = constructor.prototype[method];
|
|
54
|
+
if (typeof shape !== "function") {
|
|
55
|
+
throw new TypeError(`proxxy: '${String(method)}' is not a method โ direct property access isn't supported on a sharded proxy`);
|
|
56
|
+
}
|
|
57
|
+
return (...args) => dispatch(method, args);
|
|
58
|
+
},
|
|
59
|
+
set(_target, prop) {
|
|
60
|
+
// Without a `set` trap, an unhandled writing would silently succeed against the
|
|
61
|
+
// internal dummy target instead of touching any real shard โ worse than a no-op,
|
|
62
|
+
// since it looks like it worked. Fail loudly instead, same as get().
|
|
63
|
+
throw new TypeError(`proxxy: '${String(prop)}' cannot be set โ direct property access isn't supported on a sharded proxy`);
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
return {
|
|
67
|
+
proxy,
|
|
68
|
+
close: () => router.close(),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
exports.startProxy = startProxy;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
type KeyExtractor = (method: string | symbol, args: unknown[]) => number;
|
|
2
|
+
type Proxied<T> = {
|
|
3
|
+
[K in keyof T]: T[K] extends (...args: infer A) => infer R ? (...args: A) => Promise<Awaited<R>> : never;
|
|
4
|
+
};
|
|
5
|
+
type ProxyHandle<T> = {
|
|
6
|
+
proxy: T;
|
|
7
|
+
close: () => Promise<void>;
|
|
8
|
+
};
|
|
9
|
+
type ProxyOptions = {
|
|
10
|
+
/** Number of independent shards, each backed by its own instance of `constructor`. Defaults to 1 (no sharding). */
|
|
11
|
+
partitionCount?: number;
|
|
12
|
+
/**
|
|
13
|
+
* Routes a call to a shard by hashing a key derived from it (e.g. an entity id in `args`).
|
|
14
|
+
* Calls that must observe each other's effects (same underlying entity) need to map to the
|
|
15
|
+
* same shard โ otherwise they land on independently constructed, mutually invisible instances.
|
|
16
|
+
* Defaults to always routing to shard 0, which is only correct when `partitionCount` is 1.
|
|
17
|
+
*/
|
|
18
|
+
keyExtractor?: KeyExtractor;
|
|
19
|
+
/** Queue capacity per shard. Defaults to the underlying daemon's default (10). */
|
|
20
|
+
bufferSizePerPartition?: number;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Wraps `partitionCount` independent instances (one per shard, built via `constructor`) behind a
|
|
24
|
+
* single proxy. Method calls are routed to a shard by `keyExtractor`; calls on the same shard
|
|
25
|
+
* are strictly ordered, calls on different shards run concurrently. Every call returns a
|
|
26
|
+
* Promise that resolves with the method's result or rejects with whatever it threw.
|
|
27
|
+
*
|
|
28
|
+
* `constructor`'s methods must be defined with regular method syntax (`method() {}`), not as
|
|
29
|
+
* arrow-function class fields (`method = () => {}`) โ only the former is visible on
|
|
30
|
+
* `constructor.prototype`, which is how proxied methods are told apart from plain properties
|
|
31
|
+
* without needing a throwaway instance. This isn't a functional loss here: every call is
|
|
32
|
+
* dispatched via `fn.apply(instance, args)` regardless of method style, so arrow fields'
|
|
33
|
+
* usual benefit (auto-bound `this`) is moot inside proxxy.
|
|
34
|
+
*/
|
|
35
|
+
declare function startProxy<T extends object>(constructor: new () => T, signal: AbortSignal, options?: ProxyOptions): ProxyHandle<Proxied<T>>;
|
|
36
|
+
|
|
37
|
+
export { startProxy };
|
|
38
|
+
export type { KeyExtractor, Proxied, ProxyHandle, ProxyOptions };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { PartitionedDaemon, Daemon } from '@on-the-ground/daemonizer';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Wraps `partitionCount` independent instances (one per shard, built via `constructor`) behind a
|
|
5
|
+
* single proxy. Method calls are routed to a shard by `keyExtractor`; calls on the same shard
|
|
6
|
+
* are strictly ordered, calls on different shards run concurrently. Every call returns a
|
|
7
|
+
* Promise that resolves with the method's result or rejects with whatever it threw.
|
|
8
|
+
*
|
|
9
|
+
* `constructor`'s methods must be defined with regular method syntax (`method() {}`), not as
|
|
10
|
+
* arrow-function class fields (`method = () => {}`) โ only the former is visible on
|
|
11
|
+
* `constructor.prototype`, which is how proxied methods are told apart from plain properties
|
|
12
|
+
* without needing a throwaway instance. This isn't a functional loss here: every call is
|
|
13
|
+
* dispatched via `fn.apply(instance, args)` regardless of method style, so arrow fields'
|
|
14
|
+
* usual benefit (auto-bound `this`) is moot inside proxxy.
|
|
15
|
+
*/
|
|
16
|
+
function startProxy(constructor, signal, options = {}) {
|
|
17
|
+
const partitionCount = options.partitionCount ?? 1;
|
|
18
|
+
const keyExtractor = options.keyExtractor ?? (() => 0);
|
|
19
|
+
const router = new PartitionedDaemon(() => {
|
|
20
|
+
// Built once per partition, not per call โ calls routed to this shard must observe
|
|
21
|
+
// each other's effects on the same instance, not start over from scratch every time.
|
|
22
|
+
const instance = new constructor();
|
|
23
|
+
return new Daemon(signal, async (_ctx, call) => {
|
|
24
|
+
const fn = instance[call.method];
|
|
25
|
+
if (typeof fn !== "function") {
|
|
26
|
+
call.reject(new TypeError(`'${String(call.method)}' is not a method`));
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
call.resolve(await fn.apply(instance, call.args));
|
|
31
|
+
}
|
|
32
|
+
catch (e) {
|
|
33
|
+
call.reject(e);
|
|
34
|
+
}
|
|
35
|
+
}, options.bufferSizePerPartition);
|
|
36
|
+
}, partitionCount, (call) => keyExtractor(call.method, call.args));
|
|
37
|
+
const dispatch = (method, args) => new Promise((resolve, reject) => {
|
|
38
|
+
router.pushEvent({ method, args, resolve, reject }).then((accepted) => {
|
|
39
|
+
if (!accepted)
|
|
40
|
+
reject(new Error(`proxxy: cannot dispatch '${String(method)}', proxy is closed`));
|
|
41
|
+
}, reject);
|
|
42
|
+
});
|
|
43
|
+
const proxy = new Proxy({}, {
|
|
44
|
+
get(_target, method) {
|
|
45
|
+
// State lives per-shard now, so there's no single coherent value to hand back for a
|
|
46
|
+
// plain field read โ only methods (routed and queued per shard) are supported. The
|
|
47
|
+
// shape comes straight from the class prototype, so no throwaway instance is needed
|
|
48
|
+
// just to answer "is this a method". This only sees conventional prototype methods โ
|
|
49
|
+
// arrow-function class fields live on the instance, not the prototype, and won't be
|
|
50
|
+
// recognized here.
|
|
51
|
+
const shape = constructor.prototype[method];
|
|
52
|
+
if (typeof shape !== "function") {
|
|
53
|
+
throw new TypeError(`proxxy: '${String(method)}' is not a method โ direct property access isn't supported on a sharded proxy`);
|
|
54
|
+
}
|
|
55
|
+
return (...args) => dispatch(method, args);
|
|
56
|
+
},
|
|
57
|
+
set(_target, prop) {
|
|
58
|
+
// Without a `set` trap, an unhandled writing would silently succeed against the
|
|
59
|
+
// internal dummy target instead of touching any real shard โ worse than a no-op,
|
|
60
|
+
// since it looks like it worked. Fail loudly instead, same as get().
|
|
61
|
+
throw new TypeError(`proxxy: '${String(prop)}' cannot be set โ direct property access isn't supported on a sharded proxy`);
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
proxy,
|
|
66
|
+
close: () => router.close(),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export { startProxy };
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@on-the-ground/proxxy-js",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Async proxies that route method calls through partitioned daemon queues, with per-key ordering and cross-key concurrency.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"import": "./dist/index.mjs",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"main": "./dist/index.cjs",
|
|
17
|
+
"module": "./dist/index.mjs",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"files": [
|
|
20
|
+
"./dist/index.d.ts",
|
|
21
|
+
"./dist/index.cjs",
|
|
22
|
+
"./dist/index.mjs",
|
|
23
|
+
"./LICENSE"
|
|
24
|
+
],
|
|
25
|
+
"scripts": {
|
|
26
|
+
"clean": "rm -rf dist",
|
|
27
|
+
"build": "yarn clean && rollup -c",
|
|
28
|
+
"test": "vitest run",
|
|
29
|
+
"typecheck": "tsc --noEmit -p tsconfig.test.json",
|
|
30
|
+
"prepublishOnly": "yarn typecheck && yarn test && yarn build"
|
|
31
|
+
},
|
|
32
|
+
"keywords": [
|
|
33
|
+
"proxy",
|
|
34
|
+
"daemon",
|
|
35
|
+
"async",
|
|
36
|
+
"partitioned",
|
|
37
|
+
"actor",
|
|
38
|
+
"queue"
|
|
39
|
+
],
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@on-the-ground/daemonizer": "^0.2.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@rollup/plugin-typescript": "^12.1.4",
|
|
45
|
+
"rollup": "^4.46.0",
|
|
46
|
+
"rollup-plugin-dts": "^6.2.1",
|
|
47
|
+
"typescript": "^5.8.3",
|
|
48
|
+
"vitest": "^3.2.4"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "https://github.com/on-the-ground/proxxy-js"
|
|
53
|
+
}
|
|
54
|
+
}
|