Skip to content

Commit 1863bc8

Browse files
authored
commands: add OptableCommands command-queue addon (#323)
1 parent 3b031c2 commit 1863bc8

4 files changed

Lines changed: 112 additions & 0 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1273,6 +1273,18 @@ if (!SkipTargetingForBots()) {
12731273

12741274
Matching is substring-based and case-insensitive, covering generic crawlers, headless browsers, HTTP clients and Google's non-search agents. It is deliberately broad and user-agent only — a cost-saving filter, not a fraud signal. For the full match list, see the [bot detection addon README](lib/addons/botDetection.md).
12751275

1276+
## Command queue
1277+
1278+
The command queue addon lets a page interact with a wrapper loaded via an async script tag before the script has arrived, in the style of `googletag.cmd` and `pbjs.que`. The page queues functions on a plain-array stub; the wrapper replaces the stub with an instance, which drains the queue and executes later pushes immediately.
1279+
1280+
```typescript
1281+
import { OptableCommands } from "@optable/web-sdk/lib/dist/addons/commands";
1282+
1283+
window.optable.cmd = new OptableCommands(window.optable.cmd || []);
1284+
```
1285+
1286+
For the page-side stub and behaviour details, see the [command queue addon README](lib/addons/commands.md).
1287+
12761288
## Demo Pages
12771289

12781290
The demo pages are working examples of both `identify` and `targeting` APIs, as well as an integration with the [Google Ad Manager 360](https://admanager.google.com/home/) ad server, enabling the targeting of ads served by GAM360 to audiences activated in the [Optable](https://optable.co/) DCN.

lib/addons/commands.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# Command Queue Addon
2+
3+
A command queue for wrappers loaded via an async script tag, in the style of `googletag.cmd` and `pbjs.que`. It lets a page interact with the wrapper before the script has loaded.
4+
5+
## Usage
6+
7+
The page defines a plain-array stub and queues calls against it:
8+
9+
```html
10+
<script>
11+
window.optable = window.optable || { cmd: [] };
12+
window.optable.cmd.push(() => {
13+
// Runs once the wrapper has loaded.
14+
});
15+
</script>
16+
<script async src="https://.../wrapper.js"></script>
17+
```
18+
19+
During initialization the wrapper swaps the stub for an instance:
20+
21+
```js
22+
import { OptableCommands } from "@optable/web-sdk/lib/dist/addons/commands";
23+
24+
window.optable.cmd = new OptableCommands(window.optable.cmd || []);
25+
```
26+
27+
The constructor drains everything queued while the script was loading. After the swap, `push()` executes its argument immediately and returns its value.
28+
29+
Non-function entries in the pre-load queue are ignored, a missing or non-array queue is tolerated, and a queued function that throws is logged to the console without stopping the rest of the queue — so a page that clobbers the stub or queues a broken function cannot break wrapper initialization.

lib/addons/commands.test.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { OptableCommands } from "./commands";
2+
3+
describe("OptableCommands", () => {
4+
it("executes functions queued before construction, in order", () => {
5+
const calls: number[] = [];
6+
new OptableCommands([() => calls.push(1), () => calls.push(2)]);
7+
expect(calls).toEqual([1, 2]);
8+
});
9+
10+
it("ignores non-function entries in the queue", () => {
11+
const fn = jest.fn();
12+
expect(() => new OptableCommands([null, "x", 42, fn])).not.toThrow();
13+
expect(fn).toHaveBeenCalledTimes(1);
14+
});
15+
16+
it("logs a throwing queued function and continues draining", () => {
17+
const spy = jest.spyOn(console, "error").mockImplementation(() => {});
18+
const after = jest.fn();
19+
const boom = new Error("boom");
20+
expect(
21+
() =>
22+
new OptableCommands([
23+
() => {
24+
throw boom;
25+
},
26+
after,
27+
])
28+
).not.toThrow();
29+
expect(after).toHaveBeenCalledTimes(1);
30+
expect(spy).toHaveBeenCalledWith(boom);
31+
spy.mockRestore();
32+
});
33+
34+
it("tolerates a missing or non-array queue", () => {
35+
expect(() => new OptableCommands()).not.toThrow();
36+
expect(() => new OptableCommands(undefined)).not.toThrow();
37+
expect(() => new OptableCommands({} as unknown)).not.toThrow();
38+
});
39+
40+
it("executes pushed functions immediately and returns their value", () => {
41+
const cmd = new OptableCommands([]);
42+
const fn = jest.fn(() => "done");
43+
expect(cmd.push(fn)).toBe("done");
44+
expect(fn).toHaveBeenCalledTimes(1);
45+
});
46+
});

lib/addons/commands.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Command queue for async script-tag wrappers, in the style of googletag.cmd
2+
// and pbjs.que. Pages push functions onto a plain-array stub before the
3+
// wrapper script loads; the wrapper replaces the stub with an instance, which
4+
// drains the queue and executes later pushes immediately.
5+
class OptableCommands {
6+
constructor(cmds?: unknown) {
7+
if (Array.isArray(cmds)) {
8+
cmds.forEach((cmd) => {
9+
if (typeof cmd !== "function") return;
10+
try {
11+
cmd();
12+
} catch (e) {
13+
console.error(e); // eslint-disable-line no-console
14+
}
15+
});
16+
}
17+
}
18+
19+
push<T>(cmd: () => T): T {
20+
return cmd();
21+
}
22+
}
23+
24+
export { OptableCommands };
25+
export default OptableCommands;

0 commit comments

Comments
 (0)