Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wip: add support for append #278

Merged
merged 3 commits into from
Apr 15, 2023
Merged
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
71 changes: 71 additions & 0 deletions .changeset/big-cheetahs-behave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
"@empirica/core": minor
---

- Support for vector attributes added

Vector attributes are attributes that are lists of values. Vector attributes
are supported by the `.append()` method. The `.append()` method will append
the given value to the list. If the attribute is not a list, the `.append()`
method will fail. Appends will not override each other if 2 users append to
the same attribute concurrently.

Example:

```js
const game = userGame();

game.append("messages", { text: "hello" });
game.append("messages", { text: "world" });

const messages = game.get("messages"); // [{ text: 'hello' }, { text: 'world' }]
```

Vector attributes can be used to implement chat, message boards, and other
features that require a list of items. Only changes to the vector attribute
will be sent to the other players. This means that if you have a list of
messages, and you append a new message, only the new message will be sent to
the other players. This is useful for reducing the amount of data that needs
to be sent over the network, and improve performance.

A specific index of a vector attribute can updated with the `.set()` method
using the `index` argument.

Example:

```js
const game = userGame();

game.append("messages", { text: "hello" });
game.append("messages", { text: "world" });

game.set("messages", { text: "hi" }, { index: 0 });

const messages = game.get("messages"); // [{ text: 'hi' }, { text: 'world' }]
```

- Attributes can be set in batch

The `.set()` method now accepts an array of attributes to set. This is
useful for setting multiple attributes at once without sending multiple
messages to the server. All attributes will be applied on the server
atomically.

Example:

```js
const game = userGame();

game.set([
{ key: "name", value: "John" },
{ key: "age", value: 30 },
]);

game.set([
{ key: "messages", value: { text: "hey" }, ao: { index: 0 } },
{ key: "messages", value: { text: "universe" }, ao: { index: 1 } },
]);
```

- do not show "no games" page while loading a registered player. Fixes #134
(again).
8 changes: 5 additions & 3 deletions .gitpod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ tasks:
go install github.com/cortesi/modd/cmd/modd@latest
go install github.com/go-bindata/go-bindata/go-bindata@latest
go install github.com/abice/go-enum@latest
go install github.com/onsi/ginkgo/v2/ginkgo@latest
curl https://get.volta.sh | bash
volta install node
sudo apt update
supt apt install rsync expect -y
sudo apt install rsync expect -y
gp sync-done modd
git config --global gpg.format ssh && git config --global user.signingkey "$SSH_SIGNING_KEY"
git config commit.gpgsign true --global
git config --global gpg.format ssh
git config --global user.signingkey "$SSH_SIGNING_KEY"
git config --global commit.gpgsign true
command: bash
- name: Get Go deps
init: |
Expand Down
2 changes: 0 additions & 2 deletions lib/@empirica/core/.npmrc

This file was deleted.

7 changes: 2 additions & 5 deletions lib/@empirica/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,7 @@
"test": "echo 'tests temporarily disabled'",
"test:watch": "NODE_NO_WARNINGS=1 TS_NODE_FILES=true TS_NODE_TRANSPILE_ONLY=true ava --watch -v",
"prerelease:legacy": "npm run build",
"prepublish": "npm run build",
"release:next": "np --tag next",
"release:legacy": "np --tag latest"
"prepublish": "npm run build"
},
"devDependencies": {
"@testing-library/react": "13.3.0",
Expand All @@ -179,7 +177,6 @@
"c8": "7.11.3",
"global-jsdom": "8.4.0",
"jsdom": "20.0.0",
"np": "7.6.2",
"prettier": "2.7.1",
"sinon": "14.0.0",
"tmp": "0.2.1",
Expand All @@ -188,7 +185,7 @@
"typescript": "4.7.4"
},
"dependencies": {
"@empirica/tajriba": "1.0.0-rc.20",
"@empirica/tajriba": "1.0.0",
"@swc/helpers": "0.4.2",
"archiver": "5.3.1",
"rxjs": "7.5.5",
Expand Down
2 changes: 1 addition & 1 deletion lib/@empirica/core/src/admin/classic/classic_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ t("on game start, players get game, round, stange and playerX", async (t) => {
t.truthy(player.player?.round);
t.truthy(player.stage);
t.truthy(player.player?.stage);
for (const plyr of player.players) {
for (const plyr of player.players || []) {
t.truthy(plyr.game);
t.truthy(plyr.round);
t.truthy(plyr.stage);
Expand Down
1 change: 0 additions & 1 deletion lib/@empirica/core/src/admin/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export class Globals extends SharedGlobals {
attrProps.protected = ao.protected;
attrProps.immutable = ao.immutable;
attrProps.append = ao.append;
attrProps.vector = ao.vector;
attrProps.index = ao.index;
}

Expand Down
64 changes: 36 additions & 28 deletions lib/@empirica/core/src/player/classic/classic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ export class Stage extends Scope<Context, EmpiricaClassicKinds> {

// TODO update context
class Context {
public game?: Game;
public stage?: Stage;
public game?: Game | null;
public stage?: Stage | null;
}

type EmpiricaClassicKinds = {
Expand All @@ -117,12 +117,12 @@ export const kinds = {
};

export type EmpiricaClassicContext = {
game: BehaviorSubject<Game | undefined>;
player: BehaviorSubject<Player | undefined>;
players: BehaviorSubject<Player[]>;
round: BehaviorSubject<Round | undefined>;
stage: BehaviorSubject<Stage | undefined>;
globals: BehaviorSubject<Globals>;
game: BehaviorSubject<Game | null | undefined>;
player: BehaviorSubject<Player | null | undefined>;
players: BehaviorSubject<Player[] | undefined>;
round: BehaviorSubject<Round | null | undefined>;
stage: BehaviorSubject<Stage | null | undefined>;
globals: BehaviorSubject<Globals | undefined>;
};

export function EmpiricaClassic(
Expand Down Expand Up @@ -152,11 +152,11 @@ export function EmpiricaClassic(
const glob = new Globals(provider.globals);

const ret = {
game: new BehaviorSubject<Game | undefined>(undefined),
player: new BehaviorSubject<Player | undefined>(undefined),
players: new BehaviorSubject<Player[]>([]),
round: new BehaviorSubject<Round | undefined>(undefined),
stage: new BehaviorSubject<Stage | undefined>(undefined),
game: new BehaviorSubject<Game | null | undefined>(undefined),
player: new BehaviorSubject<Player | null | undefined>(undefined),
players: new BehaviorSubject<Player[] | undefined>(undefined),
round: new BehaviorSubject<Round | null | undefined>(undefined),
stage: new BehaviorSubject<Stage | null | undefined>(undefined),
globals: glob.self,
};

Expand Down Expand Up @@ -199,8 +199,8 @@ export function EmpiricaClassic(

let playersChanged = false;
const players: Player[] = [];
for (let i = 0; i < updated.players.length; i++) {
let p = updated.players[i];
for (let i = 0; i < (updated.players || []).length; i++) {
let p = updated.players![i];

if (p) {
const partID = attributes.nextAttributeValue(
Expand All @@ -212,7 +212,7 @@ export function EmpiricaClassic(
}
}

if (!playersChanged && scopeChanged(p, current.players[i])) {
if (!playersChanged && scopeChanged(p, (current.players || [])[i])) {
playersChanged = true;
}

Expand All @@ -233,18 +233,22 @@ export function EmpiricaClassic(
}

type mainObjects = {
game?: Game;
player?: Player;
round?: Round;
stage?: Stage;
players: Player[];
game?: Game | null;
player?: Player | null;
round?: Round | null;
stage?: Stage | null;
players?: Player[];
};

function scopeChanged(
current?: Scope<Context, EmpiricaClassicKinds>,
updated?: Scope<Context, EmpiricaClassicKinds>
current?: Scope<Context, EmpiricaClassicKinds> | null,
updated?: Scope<Context, EmpiricaClassicKinds> | null
): boolean {
if (!current && !updated) {
if (current === undefined && updated === null) {
return true;
}

return false;
}

Expand Down Expand Up @@ -274,6 +278,10 @@ function getMainObjects(

const res: mainObjects = {
players: Array.from(players.values()) as Player[],
game: null,
player: null,
round: null,
stage: null,
};

if (players.size === 0) {
Expand All @@ -294,7 +302,7 @@ function getMainObjects(
return res;
}

for (const player of res.players) {
for (const player of res.players || []) {
const key = `playerGameID-${res.game.id}`;
if (!nextScopeByKey(scopes, attributes, player, key)) {
return res;
Expand All @@ -306,7 +314,7 @@ function getMainObjects(
return res;
}

for (const player of res.players) {
for (const player of res.players || []) {
const key = `playerStageID-${res.stage.id}`;
if (!nextScopeByKey(scopes, attributes, player, key)) {
delete res.stage;
Expand All @@ -319,7 +327,7 @@ function getMainObjects(
return res;
}

for (const player of res.players) {
for (const player of res.players || []) {
const key = `playerRoundID-${res.round.id}`;
if (!nextScopeByKey(scopes, attributes, player, key)) {
delete res.stage;
Expand All @@ -339,8 +347,8 @@ function nextScopeByKey(
) {
const id = attributes.nextAttributeValue(scope.id, key);
if (!id || typeof id !== "string") {
return;
return null;
}

return scopes.scope(id);
return scopes.scope(id) || null;
}
24 changes: 12 additions & 12 deletions lib/@empirica/core/src/player/classic/classic_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ test("ClassicMode should return player", (t) => {
const { ctx, changes } = setupClassic();
setupPlayer(changes);

t.is(ctx.players.getValue().length, 1);
t.is(ctx.players.getValue()!.length, 1);
t.truthy(ctx.player.getValue());
t.falsy(ctx.game.getValue());
t.falsy(ctx.stage.getValue());
Expand Down Expand Up @@ -79,7 +79,7 @@ test("ClassicMode should return players", (t) => {
setupPlayer(changes, "player2", "participant2");
setupPlayer(changes, "player3", "participant3");

t.is(ctx.players.getValue().length, 3);
t.is(ctx.players.getValue()!.length, 3);
t.truthy(ctx.player.getValue());
t.falsy(ctx.game.getValue());
t.falsy(ctx.stage.getValue());
Expand All @@ -92,15 +92,15 @@ test("ClassicMode should filter players on participants", (t) => {
setupPlayer(changes, "player2", "participant2");
setupPlayer(changes, "player3", "participant3");

t.is(ctx.players.getValue().length, 3);
t.is(ctx.players.getValue()!.length, 3);

changes.next(partChange({ id: "participant2", removed: true }));
});

test("ClassicMode should update player", (t) => {
const { ctx, changes } = setupClassic();

const vals: (Player | undefined)[] = [];
const vals: (Player | null | undefined)[] = [];
ctx.player.subscribe({
next(val) {
vals.push(val);
Expand All @@ -124,7 +124,7 @@ test("ClassicMode should update player", (t) => {
test("ClassicMode should update game", (t) => {
const { ctx, changes } = setupClassic();

const vals: (Game | undefined)[] = [];
const vals: (Game | null | undefined)[] = [];
ctx.game.subscribe({
next(val) {
vals.push(val);
Expand Down Expand Up @@ -164,7 +164,7 @@ test("ClassicMode should update game", (t) => {
test("ClassicMode should update stage", (t) => {
const { ctx, changes } = setupClassic();

const vals: (Stage | undefined)[] = [];
const vals: (Stage | null | undefined)[] = [];
ctx.stage.subscribe({
next(val) {
vals.push(val);
Expand Down Expand Up @@ -226,8 +226,8 @@ test("ClassicMode game should have stage and round", (t) => {
setupPlayerStage(changes);

const game = ctx.game.getValue()!;
t.is(game.stage, ctx.stage.getValue());
t.is(game.round, ctx.round.getValue());
t.is(game.stage, ctx.stage.getValue()!);
t.is(game.round, ctx.round.getValue()!);
});

test("ClassicMode game should not have stage and round", (t) => {
Expand All @@ -239,8 +239,8 @@ test("ClassicMode game should not have stage and round", (t) => {
const game = ctx.game.getValue()!;
t.true(!game.stage);
t.true(!game.round);
t.is(game.stage, ctx.stage.getValue());
t.is(game.round, ctx.round.getValue());
t.is(game.stage, ctx.stage.getValue()!);
t.is(game.round, ctx.round.getValue()!);
});

test("ClassicMode stage can have timer", (t) => {
Expand Down Expand Up @@ -283,7 +283,7 @@ test("ClassicMode game should update on attribute change", (t) => {
setupGame(changes);
setupPlayerGame(changes);

const vals: (Game | undefined)[] = [];
const vals: (Game | null | undefined)[] = [];
ctx.game.subscribe({
next(val) {
vals.push(val);
Expand Down Expand Up @@ -329,7 +329,7 @@ test("ClassicMode player should update on PlayerGame change", (t) => {
})
);

const vals: (Player | undefined)[] = [];
const vals: (Player | null | undefined)[] = [];
ctx.player.subscribe({
next(val) {
vals.push(val);
Expand Down
Loading