Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Check

on:
pull_request:
push:
branches:
- main

jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci

- name: Run test
run: npm run test
82 changes: 80 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,92 @@ During SDK testing, it’s often useful to simulate real-world conditions or ins
- Replay server messages for automated or regression testing.
- Monitor raw protocol-level activity to catch subtle bugs or race conditions.

🚀 Getting Started
### 🚀 Getting Started

Install npm dependency:

```bash
npm i -D @ably-labs/local-proxy
npm install --save-dev @ably-labs/local-proxy
```

### 🧪 Basic Usage

```ts
import { createInterceptingProxy } from '@ably-labs/local-proxy';

// 1. Create and start the proxy
const proxy = createInterceptingProxy({
// options here realtimeHost, restHost
});
await proxy.start();

// 2. Configure your SDK to use proxy.options
const client = new Ably.Realtime({
...proxy.options
// aditonal option
});

// 3. Observe an outgoing HTTP request
const request = await proxy.observeNextRequest(req =>
req.url.includes('/channels')
);

// 4. Observe an incoming protocol message
proxy.observeNextIncomingProtocolMessage(msg =>
msg.action === 15 // Presence message
)

// 5. Inject a fake message into the client
proxy.injectProtocolMessage(client.connection.id, {
action: 9, // Example: SYNC
channel: 'room:test',
connectionId: client.connection.id,
msgSerial: 0,
connectionSerial: -1,
data: { custom: 'data' },
});
```

### 🧩 Replace a Server Message

You can also simulate faulty server responses:

```ts
proxy.replaceNextIncomingProtocolMessage(
{
action: 9, // Fake SYNC
channel: 'room:test',
data: [],
},
msg => msg.action === 9 // Replace only SYNC messages
);
```


### 🔌 Drop or Pause a Connection

```ts
// Drop connection by ID (force disconnect)
proxy.dropConnection(client.connection.id);

// Pause and resume connection manually
const resume = proxy.pauseConnection();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pauseConnection is not in the InterceptingProxy interface. should it be pauseAllConnections?
that being said we should still have a way to pause connection by an id. wdyt?

// simulate a pause...
setTimeout(() => resume(), 5000);
```


### 🔧 Register Middleware

For more advanced use cases, register middlewares to continuously inspect or modify traffic:

```ts
const unregister = proxy.registerRestMiddleware(req => {
if (req.url.includes('/channels')) {
console.log('Intercepted REST request:', req);
// You can modify headers, body, or response here
}
});
// Later...
unregister();
```
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@ably-labs/local-proxy",
"version": "0.0.6",
"description": "Unified Test Suite for Chat SDKs. An executable npm package designed to provide a consistent testing environment for different Ably Chat SDKs",
"version": "0.1.0",
"description": "Local proxy for SDK testing",
"scripts": {
"build": "tsc",
"prepare": "npm run build",
Expand Down
36 changes: 36 additions & 0 deletions src/async-queue.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { CompletableDeferred } from './completable-deferred';

type AsyncTask = () => Promise<void>;

export class AsyncQueue {
private readonly queue: AsyncTask[] = [];
private processing: boolean = false;

enqueue(task: AsyncTask): Promise<void> {
const deferredValue = CompletableDeferred<void>();
this.queue.push(async () => {
try {
await task();
} finally {
deferredValue.complete();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're not catching an error here and I believe it will lead to undandled promise rejections at the line below where we call this.processNext() (as that will throw an error when executin task).
is it intentional here?
also not handling an error prevents us from passing the error to the deferredValue - caller of the async-queue can't know if it failed

}
});
this.processNext();
return deferredValue.get()
}

private async processNext() {
if (this.processing || this.queue.length === 0) return;

this.processing = true;

const task = this.queue.shift();

try {
await task!!();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
await task!!();
await task!();

should it be just one exclamation mark?

} finally {
this.processing = false;
this.processNext();
}
}
}
22 changes: 22 additions & 0 deletions src/completable-deferred.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
type CompletionHandler<T = void> = (value: T) => void

class DefaultCompletableDeferred<T = void> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would like to have a description for this primitive as it's not immediately obvious what it does.
was it by any chance copied from somewhere? maybe can include a link to source too

private _completeWith!: CompletionHandler<T>;
private value: T | undefined
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it look like value is never set? is it still needed?


private valuePromise = new Promise<T>(resolve => {
this._completeWith = resolve;
});

public complete(value: T): void {
this._completeWith(value);
}

public async get(): Promise<T> {
return this.value ?? this.valuePromise;
}
}

export function CompletableDeferred<T>() {
return new DefaultCompletableDeferred<T>()
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './proxy.js';
Loading