Skip to content

Commit 966dbb4

Browse files
committed
Fixed closure issue with state decorators when running multiple instances
1 parent c92a89e commit 966dbb4

File tree

5 files changed

+59
-68
lines changed

5 files changed

+59
-68
lines changed

packages/module/src/runtime/Runtime.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -311,9 +311,7 @@ export class Runtime<Modules extends RuntimeModulesRecord>
311311
}
312312

313313
public get stateServiceProvider(): StateServiceProvider {
314-
return this.dependencyContainer.resolve<StateServiceProvider>(
315-
"StateServiceProvider"
316-
);
314+
return this.container.resolve<StateServiceProvider>("StateServiceProvider");
317315
}
318316

319317
public get stateService(): SimpleAsyncStateService {

packages/protocol/src/state/protocol/ProtocolState.ts

Lines changed: 52 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -27,42 +27,6 @@ export interface StatefulModule {
2727
};
2828
}
2929

30-
export function createStateGetter<TargetModule extends StatefulModule>(
31-
target: TargetModule,
32-
propertyKey: string,
33-
valueReference: Reference<State<unknown> | undefined>,
34-
prefix: string,
35-
debugInfo: { parentName: string; baseModuleNames: string }
36-
) {
37-
return function getter(this: TargetModule) {
38-
// const self = this;
39-
const { value } = valueReference;
40-
// Short-circuit this to return the state in case its already initialized
41-
if (value !== undefined && value.path !== undefined) {
42-
return value;
43-
}
44-
45-
if (this.name === undefined) {
46-
throw errors.missingName(this.constructor.name);
47-
}
48-
49-
if (!this.parent) {
50-
throw errors.missingParent(
51-
this.constructor.name,
52-
debugInfo.parentName,
53-
debugInfo.baseModuleNames
54-
);
55-
}
56-
57-
const path = Path.fromProperty(this.name, propertyKey, prefix);
58-
if (value) {
59-
value.path = path;
60-
value.stateServiceProvider = this.parent.stateServiceProvider;
61-
}
62-
return value;
63-
};
64-
}
65-
6630
/**
6731
* Decorates a runtime module property as state, passing down some
6832
* underlying values to improve developer experience.
@@ -72,31 +36,62 @@ export function state() {
7236
target: TargetTransitioningModule,
7337
propertyKey: string
7438
) => {
75-
const stateReference = createReference<State<unknown> | undefined>(
76-
undefined
77-
);
78-
79-
const isProtocol = target instanceof TransitioningProtocolModule;
80-
const statePrefix = isProtocol
81-
? PROTOKIT_PREFIXES.STATE_PROTOCOL
82-
: PROTOKIT_PREFIXES.STATE_RUNTIME;
83-
const debugInfo = isProtocol
84-
? { parentName: "protocol", baseModuleNames: "...Hook" }
85-
: { parentName: "runtime", baseModuleNames: "RuntimeModule" };
86-
8739
Object.defineProperty(target, propertyKey, {
8840
enumerable: true,
8941

90-
get: createStateGetter(
91-
target,
92-
propertyKey,
93-
stateReference,
94-
statePrefix,
95-
debugInfo
96-
),
42+
get: function get(this: TargetTransitioningModule) {
43+
// The reason for why we store the state value in this weird way is that
44+
// in the decorator on the prototype of the class. This means that if there
45+
// are multiple instances of this class, any closure that this getter shares
46+
// will be the same for all instances.
47+
// Therefore, we need to somehow save the set instance on the instance itself
48+
49+
// eslint-disable-next-line max-len
50+
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions,@typescript-eslint/no-unsafe-assignment
51+
const reference: Reference<State<unknown>> | undefined = (this as any)[
52+
`protokit_state_cache_${propertyKey}`
53+
];
54+
55+
// Short-circuit this to return the state in case its already initialized
56+
if (reference !== undefined && reference.value.path !== undefined) {
57+
return reference.value;
58+
}
59+
60+
if (this.name === undefined) {
61+
throw errors.missingName(this.constructor.name);
62+
}
63+
64+
const isProtocol = target instanceof TransitioningProtocolModule;
65+
66+
if (!this.parent) {
67+
const debugInfo = isProtocol
68+
? { parentName: "protocol", baseModuleNames: "...Hook" }
69+
: { parentName: "runtime", baseModuleNames: "RuntimeModule" };
70+
71+
throw errors.missingParent(
72+
this.constructor.name,
73+
debugInfo.parentName,
74+
debugInfo.baseModuleNames
75+
);
76+
}
77+
78+
const statePrefix = isProtocol
79+
? PROTOKIT_PREFIXES.STATE_PROTOCOL
80+
: PROTOKIT_PREFIXES.STATE_RUNTIME;
81+
const path = Path.fromProperty(this.name, propertyKey, statePrefix);
82+
if (reference) {
83+
const { value } = reference;
84+
value.path = path;
85+
value.stateServiceProvider = this.parent.stateServiceProvider;
86+
}
87+
return reference?.value;
88+
},
9789

98-
set: (newValue: State<unknown>) => {
99-
stateReference.value = newValue;
90+
set: function set(
91+
this: TargetTransitioningModule & any,
92+
newValue: State<unknown>
93+
) {
94+
this[`protokit_state_cache_${propertyKey}`] = createReference(newValue);
10095
},
10196
});
10297
};

packages/sequencer/src/sequencer/executor/Sequencer.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ export class Sequencer<Modules extends SequencerModulesRecord>
120120
// eslint-disable-next-line no-await-in-loop
121121
await sequencerModule.start();
122122
}
123+
124+
// TODO This currently also warns for client appchains
123125
if (!moduleClassNames.includes("SequencerStartupModule")) {
124126
log.warn("SequencerStartupModule is not defined.");
125127
}

packages/stack/test/graphql/Post.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
1-
import {
2-
RuntimeModule,
3-
runtimeMethod,
4-
runtimeModule,
5-
state,
6-
} from "@proto-kit/module";
7-
import { StateMap } from "@proto-kit/protocol";
1+
import { RuntimeModule, runtimeMethod, runtimeModule } from "@proto-kit/module";
2+
import { StateMap, state } from "@proto-kit/protocol";
83
import {
94
CircuitString,
105
Field,

packages/stack/test/graphql/graphql.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
GraphqlNetworkStateTransportModule,
2121
} from "@proto-kit/sdk";
2222
import { beforeAll } from "@jest/globals";
23+
import { container } from "tsyringe";
2324

2425
import { startServer, TestBalances } from "../../src/scripts/graphql/server";
2526

@@ -103,7 +104,7 @@ describe("graphql client test", () => {
103104

104105
appChain = prepareClient();
105106

106-
await appChain.start();
107+
await appChain.start(false, container.createChildContainer());
107108

108109
trigger = server.sequencer.resolveOrFail(
109110
"BlockTrigger",

0 commit comments

Comments
 (0)