Skip to content

Commit

Permalink
feat: add ephemeral attribute support (#533)
Browse files Browse the repository at this point in the history
  • Loading branch information
npaton committed Mar 31, 2024
1 parent cbccdb7 commit d5c6969
Show file tree
Hide file tree
Showing 18 changed files with 157 additions and 17 deletions.
18 changes: 18 additions & 0 deletions .changeset/add-ephemeral-attributes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
---
"@empirica/core": minor
---

Add ephemeral attribute support.

This allows you to define attributes that are not persisted to the database, but
are available to the client and server while the server is still running. These
attributes will sync with all players as normal attributes. This is useful for
data that that would be unreasonable to persist to the database due to size or
volatility, but is still useful to share between clients and the server.

For example, you could use this to sync the mouse movements of the players.

```js
player.set("mouse", { x: 123, y: 456 }, { ephemeral: true });
player.get("mouse"); // { x: 123, y: 456 }
```
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
github.com/charmbracelet/lipgloss v0.5.0
github.com/cortesi/moddwatch v0.0.0-20210323234936-df014e95c743
github.com/davecgh/go-spew v1.1.1
github.com/empiricaly/tajriba v1.6.1
github.com/empiricaly/tajriba v1.7.0
github.com/go-playground/validator/v10 v10.11.0
github.com/jpillora/backoff v1.0.0
github.com/json-iterator/go v1.1.12
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/empiricaly/tajriba v1.6.1 h1:AB5zWVvktKMoYGV/Dh5o7ieP+OVisY7u1liiaxqGj54=
github.com/empiricaly/tajriba v1.6.1/go.mod h1:SvDTEUhhlQTQI1uBB8AWWR1cKzbE+zOfpJCm0Adkdyw=
github.com/empiricaly/tajriba v1.7.0 h1:3WdJqOIPrdJmdWZEs7ngH20ghu27sJjqK+KKy54iY4U=
github.com/empiricaly/tajriba v1.7.0/go.mod h1:SvDTEUhhlQTQI1uBB8AWWR1cKzbE+zOfpJCm0Adkdyw=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
Expand Down
6 changes: 3 additions & 3 deletions lib/@empirica/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -129,11 +129,11 @@
}
},
"volta": {
"node": "20.10.0",
"npm": "10.2.3"
"node": "20.12.0",
"npm": "10.5.0"
},
"engines": {
"node": ">= 16.17.0"
"node": ">= 20.12.0"
},
"browserslist": "> 0.5%, last 2 versions, not dead",
"ava": {
Expand Down
1 change: 1 addition & 0 deletions lib/@empirica/core/src/admin/classic/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ interface AttributeEdge {
private: boolean;
protected: boolean;
immutable: boolean;
ephemeral: boolean;
deletedAt?: any;
key: string;
val?: string | null | undefined;
Expand Down
3 changes: 3 additions & 0 deletions lib/@empirica/core/src/admin/classic/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ export function scopeConstructor(input: AddScopeInput) {
export type AttrInput = {
append?: boolean;
immutable?: boolean;
ephemeral?: boolean;
index?: number;
key: string;
nodeID?: string;
Expand All @@ -75,6 +76,7 @@ export function attrs(attrs: AttrInput[]) {
const {
append,
immutable,
ephemeral,
index,
key,
nodeID,
Expand All @@ -86,6 +88,7 @@ export function attrs(attrs: AttrInput[]) {
result.push({
append,
immutable,
ephemeral,
index,
key,
nodeID,
Expand Down
1 change: 1 addition & 0 deletions lib/@empirica/core/src/admin/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ export interface AddScopePayload {
private: boolean;
protected: boolean;
immutable: boolean;
ephemeral: boolean;
key: string;
val?: string | null | undefined;
index?: number | null | undefined;
Expand Down
1 change: 1 addition & 0 deletions lib/@empirica/core/src/admin/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export class Globals extends SharedGlobals {
attrProps.private = ao.private;
attrProps.protected = ao.protected;
attrProps.immutable = ao.immutable;
attrProps.ephemeral = ao.ephemeral;
attrProps.append = ao.append;
attrProps.index = ao.index;
}
Expand Down
1 change: 1 addition & 0 deletions lib/@empirica/core/src/admin/globals_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export function attrPayload(
immutable: false,
private: false,
protected: false,
ephemeral: false,
},
done,
isNew: false,
Expand Down
1 change: 1 addition & 0 deletions lib/@empirica/core/src/admin/runloop_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ function attrib(props: Partial<typeof attribProps> = attribProps) {
immutable: false,
private: false,
protected: false,
ephemeral: false,
};
}

Expand Down
6 changes: 6 additions & 0 deletions lib/@empirica/core/src/shared/attributes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,11 @@ export interface AttributeOptions {
protected: boolean;
/** Immutable creates an Attribute that cannot be updated. */
immutable: boolean;
/** ephemeral indicates the Attribute should not be persisted. Ephemeral
* Attributes are not stored in the database and are only synced to the
* connected clients. An ephemeral Attribute cannot become non-ephemeral and
* vice versa. */
ephemeral: boolean;
/**
* Index, only used if the Attribute is a vector, indicates which index to
* update the value at.
Expand Down Expand Up @@ -349,6 +354,7 @@ export class Attribute {
attrProps.private = ao.private;
attrProps.protected = ao.protected;
attrProps.immutable = ao.immutable;
attrProps.ephemeral = ao.ephemeral;
attrProps.append = ao.append;
attrProps.index = ao.index;
}
Expand Down
1 change: 1 addition & 0 deletions lib/@empirica/core/src/shared/globals_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export function attrPayload(
immutable: false,
private: false,
protected: false,
ephemeral: false,
},
done,
isNew: false,
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,13 @@
"e2e-tests",
"lib/@empirica/*"
],
"volta": {
"node": "20.12.0",
"npm": "10.5.0"
},
"engines": {
"node": ">= 20.12.0"
},
"repository": {
"type": "git",
"url": "git+https://github.com/empiricaly/empirica.git"
Expand Down
4 changes: 2 additions & 2 deletions tests/stress/experiment/client/src/Game.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ class Scope {
return this.scope?.get(key);
}

set(key, value) {
this.scope?.set(key, value);
set(key, value, options) {
this.scope?.set(key, value, options);
}

get game() {
Expand Down
66 changes: 62 additions & 4 deletions tests/stress/tests/attributes.spec.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
// @ts-check
/// <reference path="./index.d.ts" />

const { test } = require("@playwright/test");
import { Context } from "./context";
const { test, expect } = require("@playwright/test");
import { adminNewBatch, quickGame } from "./admin";
import { Context } from "./context";
import {
playerStart,
submitStage,
waitGameFinished,
waitNextStage,
} from "./player";
import { sleep } from "./utils";
import { randomString, sleep } from "./utils";

// At the moment, we use the same empirica server for all tests, so we need to
// run them serially. This will change when we have a dedicated server for eac XLh
Expand Down Expand Up @@ -107,3 +106,62 @@ test("attribute as bool, correct equality check", async ({ browser }) => {

await ctx.close();
});

test("attribute persistent or ephemeral", async ({ browser }) => {
const ctx = new Context(browser);

const playerCount = 2;
const roundCount = 1;
const stageCount = 2;

ctx.logMatching(/keya/);
ctx.logMatching(/keyb/);

await ctx.start();
await ctx.addPlayers(playerCount);
ctx.players[0].logWS();
ctx.players[1].logWS();

await ctx.applyAdmin(
adminNewBatch({
treatmentConfig: quickGame(playerCount, roundCount, stageCount),
})
);

await ctx.applyPlayers(playerStart);

// Baseline, normal keya is saved
await ctx.players[0].set("game", "keya", "123");
await ctx.expectPlayers("game", "keya", "123");

// Ephemeral keyb is NOT saved
const randstr = randomString(12);
const key = `key-${randstr}`;
const val = `val-${randstr}`;
await ctx.players[0].set("game", key, val, { ephemeral: true });
await ctx.expectPlayers("game", key, val);

// Next stage
await ctx.applyPlayers(submitStage);
await ctx.applyPlayers(waitNextStage);

// Check both keys are available to all players
await ctx.expectPlayers("game", "keya", "123");
await ctx.expectPlayers("game", key, val);

// Wait a bit for the tajriba file to be written
await sleep(1200);

// keya should exist
expect(await ctx.tajContains("keya"), "keya exists").toBeTruthy();

// ephemeral key and value should NOT exist
expect(await ctx.tajContains(key), "ephemeral key exists").toBeFalsy();
expect(await ctx.tajContains(val), "ephemeral value exists").toBeFalsy();

// Finish game
await ctx.applyPlayers(submitStage);
await ctx.applyPlayers(waitGameFinished);

await ctx.close();
});
30 changes: 30 additions & 0 deletions tests/stress/tests/context.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { exec } from "child_process";
import { Admin } from "./admin";
import { Player } from "./player";
import path from "path";

export class Context {
constructor(browser) {
Expand Down Expand Up @@ -89,6 +91,34 @@ export class Context {
await player.expect(kind, key, value);
}
}

// Execute a command and return the output
async exec(cmd) {
return new Promise((resolve, reject) => {
let proc;
proc = exec(cmd, (error, stdout, stderr) => {
if (error) {
reject(error);
}
resolve({ stdout, stderr, exitCode: proc.exitCode });
});
});
}

// Check if the tajriba file contains a string
async tajContains(string) {
const tajfile = path.resolve(
__dirname,
"../experiment/.empirica/local/tajriba.json"
);

try {
await this.exec(`cat ${tajfile} | grep ${string}`);
return true;
} catch (e) {
return false;
}
}
}

export function applyAll(actors, step) {
Expand Down
14 changes: 9 additions & 5 deletions tests/stress/tests/player.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,18 +91,22 @@ export class Player extends Actor {
);
}

async set(kind, key, value) {
async set(kind, key, value, options) {
const subKind = getSubKind(kind);

await this.page.evaluate(
({ kind, subKind, key, value }) => {
({ kind, subKind, key, value, options }) => {
if (subKind) {
window["empirica_test_collector"]["player"][subKind].set(key, value);
window["empirica_test_collector"]["player"][subKind].set(
key,
value,
options
);
} else {
window["empirica_test_collector"][kind].set(key, value);
window["empirica_test_collector"][kind].set(key, value, options);
}
},
{ kind, subKind, key, value }
{ kind, subKind, key, value, options }
);
}

Expand Down
8 changes: 8 additions & 0 deletions tests/stress/tests/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ import { exec as cpexec } from "child_process";
export async function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export function randomString(length) {
const chars = "0123456789abcdefghijklmnopqrstuvwxyz";
let result = "";
for (let i = length; i > 0; --i) {
result += chars[Math.floor(Math.random() * chars.length)];
}
return result;
}

export function exec(cmd, wd) {
return new Promise((resolve, reject) => {
Expand Down

0 comments on commit d5c6969

Please sign in to comment.