Skip to content

Commit

Permalink
feat(everything): changes to linting, chain ending, ensuring changes …
Browse files Browse the repository at this point in the history
…during evolution don't broadcast
  • Loading branch information
Jake Lauer authored and Jake Lauer committed Jun 22, 2024
1 parent e95a3a0 commit 06773d1
Show file tree
Hide file tree
Showing 25 changed files with 193 additions and 106 deletions.
2 changes: 1 addition & 1 deletion .examples/tic-tac-toe/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const TurnEvolver = Evolver.create("TurnEvolver", { noun: "gameState" })
.via.iterateTurnCount()
.and.updateLastPlayer(mark)
.and.updateLastPlayedCoords(coords)
.result;
.end();

return gameState;
},
Expand Down
2 changes: 1 addition & 1 deletion .tutorial/parts/part-0--FIRM-encapsulation.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ const BathroomEvolver = Evolver.create("BathroomEvolver", { noun: "room" })
.and.cleanWindow()
.and.wipeCounter();

await OdorEvolver.mutate(mutableRoom).via.deodorizerSpray().resultAsync;
await OdorEvolver.mutate(mutableRoom).via.deodorizerSpray().endAsync();

return mutableRoom;
},
Expand Down
14 changes: 8 additions & 6 deletions .tutorial/parts/part-ii--evolving-state.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,16 +129,18 @@ const onMoveUnavailable = () => {
### Evolutions (multiple changes)
Evolutions allow for chaining many methods in a single call. Each method returns the other mutators. In order to get the
final resulting object, simply end the chain with `result` or `resultAsync` (depending on whether any item in the chain
final resulting object, simply end the chain with `result` or `endAsync` (depending on whether any item in the chain
is asynchronous).
All chained methods will be run in order, serially.
```typescript
// No more viable moves are available
const onTurnTaken = (previousGameState: GameState) => {
return GameMetaEvolver.evolve(previousGameState).via.updateLastPlayer(mark).and.updateLastPlayedCoords(coords)
.result;
return GameMetaEvolver.evolve(previousGameState)
.via.updateLastPlayer(mark)
.and.updateLastPlayedCoords(coords)
.end();
};
```
Expand All @@ -164,7 +166,7 @@ because the data being evolved is mutable.
A chain of mutations becomes asynchronous if _any part_ of the chain is asynchronous, regardless of where it is in the
chain. In that case, the chain must be awaited, or the returned data may not be fully modified at return time.
For this reason, it's advisable to use `result` and `resultAsync` even when it's unnecessary, as a visual reminder to
For this reason, it's advisable to use `result` and `endAsync` even when it's unnecessary, as a visual reminder to
avoid asynchronous race conditions.
```typescript
Expand All @@ -175,7 +177,7 @@ avoid asynchronous race conditions.
.via.asyncAddPlayer()
.via.asyncUpdateLastPlayer(mark)
.and.asyncUpdateLastPlayedCoords(coords)
.resultAsync;
.endAsync();

console.log(result)

Expand All @@ -190,7 +192,7 @@ avoid asynchronous race conditions.
.via.asyncAddPlayer()
.via.asyncUpdateLastPlayer(mark)
.and.asyncUpdateLastPlayedCoords(coords)
.resultAsync
.endAsync()
.then(result => {

console.log(result);
Expand Down
14 changes: 12 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"request": "launch",
"name": "Debug ESLint",
"program": "${workspaceFolder}/node_modules/eslint/bin/eslint.js",
"args": ["--no-ignore", "packages/eslint-plugin-theseus/lib/tests/break-on-chainable-testfile.js"], // Adjust this to point to your JavaScript files
"args": ["--no-ignore", "packages/eslint-plugin-theseus/lib/_test/break-on-chainable.sandbox.js"], // Adjust this to point to your JavaScript files
"stopOnEntry": false,
"cwd": "${workspaceFolder}",
"runtimeArgs": ["--nolazy", "--inspect-brk"],
Expand All @@ -29,7 +29,17 @@
"request": "launch",
"name": "Run Base Tests",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["test:base", "--timeout", "60000", "--", "--grep", "\"should correctly handle instance retrieval and updates by ID\""],
"runtimeArgs": ["test:base", "--timeout", "60000"],
"internalConsoleOptions": "openOnSessionStart",
"cwd": "${workspaceFolder}",
"outputCapture": "std"
},
{
"type": "node",
"request": "launch",
"name": "Run Base Test (single)",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["test:base", "--timeout", "60000", "--", "--grep", "\"should return a new object copy that won't change after further evolutions\""],
"internalConsoleOptions": "openOnSessionStart",
"cwd": "${workspaceFolder}",
"outputCapture": "std"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

const obj ={};
obj.via.one().end();
27 changes: 17 additions & 10 deletions packages/eslint-plugin-theseus/lib/_test/break-on-chainable.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,15 @@ ruleTester.run("break-on-chainable", rule, {
code: "obj.via.thing()\n.lastly.thing()",
},
{
code: "obj.via.thing()\n.result",
// If an ending method is used without chaining, don't force a line break
code: "obj.via.thing().end()",
},
{
code: "obj.via.thing()\n.resultAsync",
code: "obj.via.thing().endAsync()",
},
{
// If no `.via` is present, we're not interested in the chain
code: "obj.thing().and.anotherThing().lastly.thing();",
},
],
invalid: [
Expand All @@ -33,26 +38,28 @@ ruleTester.run("break-on-chainable", rule, {
output: "obj.via.thing()\n.and.anotherThing()\n.lastly.thing();",
},
{
code: "obj.via.thing().result;",
code: "obj.via.thing().and.thing().end();",
errors: [
{ message: "Expected line break before `.result`." },
{ message: "Expected line break before `.and`." },
{ message: "Expected line break before `.end`." },
],
output: "obj.via.thing()\n.result;",
output: "obj.via.thing()\n.and.thing()\n.end();",
},
{
code: "obj.via.thing().resultAsync;",
code: "obj.via.thing().and.thing().endAsync();",
errors: [
{ message: "Expected line break before `.resultAsync`." },
{ message: "Expected line break before `.and`." },
{ message: "Expected line break before `.endAsync`." },
],
output: "obj.via.thing()\n.resultAsync;",
output: "obj.via.thing()\n.and.thing()\n.endAsync();",
},
{
code: "GameMeta.iterateTurnCount().and.updateLastPlayer(mark).and.updateLastPlayedCoords(coords);",
code: "GameMeta.evolve.iterateTurnCount().and.updateLastPlayer(mark).and.updateLastPlayedCoords(coords);",
errors: [
{ message: "Expected line break before `.and`." },
{ message: "Expected line break before `.and`." },
],
output: "GameMeta.iterateTurnCount()\n.and.updateLastPlayer(mark)\n.and.updateLastPlayedCoords(coords);",
output: "GameMeta.evolve.iterateTurnCount()\n.and.updateLastPlayer(mark)\n.and.updateLastPlayedCoords(coords);",
},
],
});
48 changes: 42 additions & 6 deletions packages/eslint-plugin-theseus/lib/rules/break-on-chainable.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ module.exports = {
meta: {
type: "layout",
docs: {
description: "Require a newline before specific method or property access in a chain",
description: "Require a newline before specific method or property access in a chain, but only if `.via` is in the chain",
recommended: false,
url: "https://eslint.org/docs/latest/rules/newline-per-chained-call",
},
Expand All @@ -20,24 +20,60 @@ module.exports = {
},
},


create(context)
{
const sourceCode = context.getSourceCode();
const targetedMethods = ["and", "lastly", "result", "resultAsync"];
const chainingMethods = new Set(["and", "lastly"]);
const endingMethods = new Set(["end", "endAsync"]);
const targetedRoots = new Set(["via", "evolve", "mutate"]);

function isLikelyTheseus(node)
{
const tokens = sourceCode.getTokensBefore(node, { count: 20, filter: ({ type,value }) =>
{
return type === "Identifier" && targetedRoots.has(value);
} });

return tokens.length > 0;
}

function isEndingMethodPrecededByChaining(node)
{
const tokens = sourceCode.getTokensBefore(node, { count: 20, filter: ({ type,value }) =>
{
return type === "Identifier" && chainingMethods.has(value);
} });

return tokens.length > 0;
}

return {
"Identifier"(node)
{
if (targetedMethods.includes(node.name) && node.parent.type === "MemberExpression")
if (!node.parent.type === "MemberExpression")
{
return;
}

const isEndingMethod = endingMethods.has(node.name);
const matchingNodeName = isEndingMethod || chainingMethods.has(node.name);

if (isEndingMethod && !isEndingMethodPrecededByChaining(node))
{
return;
}

if (matchingNodeName && isLikelyTheseus(node))
{
// Only consider the direct .property access case, ensuring it's part of a member expression chain
const tokenBeforeNode = sourceCode.getTokenBefore(node, { includeComments: false });
if (tokenBeforeNode && tokenBeforeNode.type === "Punctuator" && tokenBeforeNode.value === ".")
if (tokenBeforeNode && tokenBeforeNode.type === "Punctuator" && tokenBeforeNode.value === ".")
{
const dotBeforeNode = tokenBeforeNode;
const tokenBeforeDot = sourceCode.getTokenBefore(dotBeforeNode, { includeComments: false });
if (tokenBeforeDot && tokenBeforeDot.loc.end.line === node.loc.start.line)

// Ensure `.via` is in the chain
if (tokenBeforeDot && tokenBeforeDot.loc.end.line === node.loc.start.line)
{
// Report if the dot connecting this property to the previous is on the same line
context.report({
Expand Down
3 changes: 2 additions & 1 deletion packages/eslint-plugin-theseus/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"compileOnSave": false,
"include": ["./lib/**/*"],
"include": ["./lib/rules/*"],
"exclude": ["./lib/_test/*.js"],
"compilerOptions": {
"allowJs": true,
"rootDir": "./lib",
Expand Down
5 changes: 3 additions & 2 deletions src/Theseus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export class Theseus<
super(params?.broadcasterParams);

this.#uuid = uuidv4();
this.setData(frost(data));
this.setData(data);
Theseus.instancesById[this.#uuid] = this;
}

Expand All @@ -61,7 +61,7 @@ export class Theseus<

private setData = (data: TData) =>
{
this.internalState = sandbox(data, { mode: "copy" });
this.internalState = sandbox(frost(data), { mode: "copy" });
};

/**
Expand All @@ -78,6 +78,7 @@ export class Theseus<

log.verbose(`Updated state for instance ${this.__uuid}`);
await this.broadcast(this.internalState);

log.verbose(`Broadcasted state for instance ${this.__uuid}`);

return true;
Expand Down
12 changes: 6 additions & 6 deletions src/TheseusBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,14 +114,14 @@ export default <TData extends object>(data: TData) => ({
mutate: evolverComplex.mutate(innerInstance.state),
};

log.debug(
log.verbose(
`Added evolvers and mutators to extension for Theseus instance ${innerInstance.__uuid}`,
extension,
);

if (refineries)
{
log.debug(
log.verbose(
`Refineries found, adding to extension for Theseus instance ${innerInstance.__uuid}`,
);
const complex: RefineryComplexInstance<TData, TParamNoun, TForges, TRefineries> =
Expand All @@ -142,7 +142,7 @@ export default <TData extends object>(data: TData) => ({
},
) as Extension;

log.debug(
log.verbose(
`Added refineries to extension for Theseus instance ${innerInstance.__uuid}`,
extension,
);
Expand All @@ -151,18 +151,18 @@ export default <TData extends object>(data: TData) => ({
return extension as Extension;
};

log.debug(
log.verbose(
`Extending Theseus instance ${theseusInstance.__uuid} with evolvers and refineries`,
theseusInstance,
);

const extension = addEvolversAndRefineries(theseusInstance, evolvers, refineries);

log.debug(`Built extension for Theseus instance ${theseusInstance.__uuid}`, extension);
log.verbose(`Built extension for Theseus instance ${theseusInstance.__uuid}`, extension);

const theseusExtended = extendTheseusWith<ITheseus<TData>, Extension>(theseusInstance, extension);

log.debug(`Theseus instance ${theseusInstance.__uuid} is ready`, theseusExtended);
log.verbose(`Theseus instance ${theseusInstance.__uuid} is ready`, theseusExtended);

return theseusExtended;
},
Expand Down
2 changes: 1 addition & 1 deletion src/_test/Theseus.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe("Observation", () =>
observation.observe(callback);

await observation["update"]({ test: "new value" });
expect(observation.state).to.deep.equal({ test: "new value" });
expect(JSON.stringify(observation.state)).to.deep.equal(JSON.stringify({ test: "new value" }));
expect(callback.calledWith({ test: "new value" })).to.be.true;

return;
Expand Down
Loading

0 comments on commit 06773d1

Please sign in to comment.