Skip to content

Commit 41a7556

Browse files
committed
[skip] Add reactive sessions.
This commit makes a `context.session` object available to resources. It works by implicitly adding a `__sessions` input collection to services, which maps `string`s to arbitrary `Json`, and using the `Session-Id` HTTP header passed to the server's read end points to inject the required session into the context. Sessions can be manipulated via two new control end points: - `PUT /v1/sessions/:session_id`, - `DELETE /v1/sessions/:session_id`.
1 parent 7a2f4bf commit 41a7556

File tree

13 files changed

+349
-115
lines changed

13 files changed

+349
-115
lines changed

examples/hackernews/reactive_service/src/hackernews.service.ts

+11-60
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,11 @@ class PostsMapper {
7777
}
7878

7979
class CleanupMapper {
80-
constructor(private readonly session: Session | null) {}
80+
private readonly session: Session | null;
81+
82+
constructor(session: Json | null) {
83+
this.session = session as Session | null;
84+
}
8185

8286
mapEntry(
8387
key: [number, number],
@@ -107,85 +111,33 @@ class CleanupMapper {
107111

108112
type PostsResourceInputs = {
109113
postsWithUpvotes: EagerCollection<[number, number], PostWithUpvoteIds>;
110-
sessions: EagerCollection<string, Session>;
111114
};
112115

113-
type PostsResourceParams = { limit?: number; session_id?: string };
116+
type PostsResourceParams = { limit?: number };
114117

115118
class PostsResource implements Resource<PostsResourceInputs> {
116119
private limit: number;
117-
private session_id: string;
118120

119121
constructor(jsonParams: Json) {
120122
const params = jsonParams as PostsResourceParams;
121123
if (params.limit === undefined) this.limit = 25;
122124
else this.limit = params.limit;
123-
if (params.session_id === undefined)
124-
throw new Error("Missing required session_id.");
125-
else this.session_id = params.session_id as string;
126125
}
127126

128127
instantiate(
129128
collections: PostsResourceInputs,
129+
context: Context,
130130
): EagerCollection<number, PostWithUpvoteCount> {
131-
let session;
132-
try {
133-
session = collections.sessions.getUnique(this.session_id);
134-
} catch {
135-
session = null;
136-
}
137131
return collections.postsWithUpvotes
138132
.take(this.limit)
139-
.map(CleanupMapper, session);
133+
.map(CleanupMapper, context.session);
140134
}
141135
}
142136

143-
class FilterSessionMapper {
144-
constructor(private session_id: string) {}
145-
146-
mapEntry(key: string, values: Values<Session>): Iterable<[number, Session]> {
147-
if (key != this.session_id) return [];
148-
const sessions = values.toArray();
149-
if (sessions.length > 0) return [[0, sessions[0] as Session]];
150-
else return [];
151-
}
152-
}
153-
154-
type SessionsResourceInputs = {
155-
sessions: EagerCollection<string, Session>;
156-
};
157-
158-
class SessionsResource implements Resource<SessionsResourceInputs> {
159-
private session_id: string;
160-
161-
constructor(jsonParams: Json) {
162-
const params = jsonParams as PostsResourceParams;
163-
if (params.session_id === undefined)
164-
throw new Error("Missing required session_id.");
165-
else this.session_id = params.session_id as string;
166-
}
167-
168-
instantiate(
169-
collections: SessionsResourceInputs,
170-
): EagerCollection<number, Session> {
171-
return collections.sessions.map(FilterSessionMapper, this.session_id);
172-
}
173-
}
174-
175-
type PostsServiceInputs = {
176-
sessions: EagerCollection<string, Session>;
177-
};
178-
179-
export const service: SkipService<PostsServiceInputs, PostsResourceInputs> = {
180-
initialData: {
181-
sessions: [],
182-
},
183-
resources: { posts: PostsResource, sessions: SessionsResource },
137+
export const service: SkipService<{}, PostsResourceInputs> = {
138+
resources: { posts: PostsResource },
184139
externalServices: { postgres },
185-
createGraph(
186-
inputs: PostsServiceInputs,
187-
context: Context,
188-
): PostsResourceInputs {
140+
createGraph(_inputs: {}, context: Context): PostsResourceInputs {
189141
const serialIDKey = { key: { col: "id", type: "SERIAL" } };
190142
const posts = context.useExternalResource<number, Post>({
191143
service: "postgres",
@@ -208,7 +160,6 @@ export const service: SkipService<PostsServiceInputs, PostsResourceInputs> = {
208160
users,
209161
upvotes.map(UpvotesMapper),
210162
),
211-
sessions: inputs.sessions,
212163
};
213164
},
214165
};

skipruntime-ts/addon/src/fromjs.cc

+9-4
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ using v8::External;
1616
using v8::Isolate;
1717
using v8::Local;
1818
using v8::MaybeLocal;
19+
using v8::Null;
1920
using v8::Number;
2021
using v8::Object;
2122
using v8::Persistent;
@@ -147,15 +148,19 @@ void SkipRuntime_deleteExternalService(uint32_t externalSupplierId) {
147148
}
148149

149150
char* SkipRuntime_Resource__instantiate(uint32_t resourceId,
150-
CJObject collections) {
151+
CJObject collections, char* sessionId) {
151152
Isolate* isolate = Isolate::GetCurrent();
152153
Local<Object> externFunctions = kExternFunctions.Get(isolate);
153-
Local<Value> argv[2] = {
154+
Local<Value> argv[3] = {
154155
Number::New(isolate, resourceId),
155156
External::New(isolate, collections),
157+
Null(isolate),
156158
};
159+
if (sessionId != nullptr) {
160+
argv[2] = FromUtf8(isolate, sessionId);
161+
}
157162
return CallJSStringFunction(isolate, externFunctions,
158-
"SkipRuntime_Resource__instantiate", 2, argv);
163+
"SkipRuntime_Resource__instantiate", 3, argv);
159164
}
160165

161166
void SkipRuntime_deleteResource(uint32_t resourceId) {
@@ -296,4 +301,4 @@ void SkipRuntime_deleteReducer(uint32_t reducerId) {
296301
}
297302
} // extern "C"
298303

299-
} // namespace skipruntime
304+
} // namespace skipruntime

skipruntime-ts/addon/src/tojs.cc

+55-22
Original file line numberDiff line numberDiff line change
@@ -63,15 +63,17 @@ CJSON SkipRuntime_LazyCollection__getArray(char* handle, CJSON key);
6363
CJSON SkipRuntime_LazyCollection__getUnique(char* handle, CJSON key);
6464

6565
double SkipRuntime_Runtime__createResource(char* identifier, char* resource,
66-
CJObject jsonParams);
66+
CJObject jsonParams,
67+
char* session_id);
6768
double SkipRuntime_Runtime__closeResource(char* identifier);
6869
int64_t SkipRuntime_Runtime__subscribe(char* reactiveId, SKNotifier notifier,
6970
char* watermark);
7071
double SkipRuntime_Runtime__unsubscribe(int64_t id);
7172
CJSON SkipRuntime_Runtime__getAll(char* resource, CJObject jsonParams,
72-
SKRequest optRequest);
73+
char* session_id, SKRequest optRequest);
7374
CJSON SkipRuntime_Runtime__getForKey(char* resource, CJObject jsonParams,
74-
CJSON key, SKRequest optRequest);
75+
char* session_id, CJSON key,
76+
SKRequest optRequest);
7577
double SkipRuntime_Runtime__update(char* input, CJSON values);
7678
}
7779

@@ -979,7 +981,8 @@ void GetUniqueOfLazyCollection(const FunctionCallbackInfo<Value>& args) {
979981

980982
void CreateResourceOfRuntime(const FunctionCallbackInfo<Value>& args) {
981983
Isolate* isolate = args.GetIsolate();
982-
if (!args[0]->IsString() || !args[1]->IsString() || !args[2]->IsExternal()) {
984+
if (!args[0]->IsString() || !args[1]->IsString() || !args[2]->IsExternal() ||
985+
!(args[3]->IsString() || args[3]->IsNull())) {
983986
// Throw an Error that is passed back to JavaScript
984987
isolate->ThrowException(
985988
Exception::TypeError(FromUtf8(isolate, "Invalid parameters.")));
@@ -989,8 +992,12 @@ void CreateResourceOfRuntime(const FunctionCallbackInfo<Value>& args) {
989992
char* skidentifier = ToSKString(isolate, args[0].As<String>());
990993
char* skresource = ToSKString(isolate, args[1].As<String>());
991994
CJObject skparams = args[2].As<External>()->Value();
992-
double skerror =
993-
SkipRuntime_Runtime__createResource(skidentifier, skresource, skparams);
995+
char* sksessionId = nullptr;
996+
if (args[3]->IsString()) {
997+
sksessionId = ToSKString(isolate, args[3].As<String>());
998+
}
999+
double skerror = SkipRuntime_Runtime__createResource(
1000+
skidentifier, skresource, skparams, sksessionId);
9941001
args.GetReturnValue().Set(Number::New(isolate, skerror));
9951002
});
9961003
}
@@ -1077,21 +1084,31 @@ void GetAllOfRuntime(const FunctionCallbackInfo<Value>& args) {
10771084
FromUtf8(isolate, "The second parameter must be a pointer.")));
10781085
return;
10791086
}
1080-
if (!args[2]->IsExternal() && !args[2]->IsNull() && !args[2]->IsUndefined()) {
1087+
if (!args[2]->IsString() && !args[2]->IsNull()) {
1088+
// Throw an Error that is passed back to JavaScript
1089+
isolate->ThrowException(Exception::TypeError(
1090+
FromUtf8(isolate, "The third parameter must be a string or null.")));
1091+
return;
1092+
};
1093+
if (!args[3]->IsExternal() && !args[3]->IsNull() && !args[3]->IsUndefined()) {
10811094
// Throw an Error that is passed back to JavaScript
10821095
isolate->ThrowException(Exception::TypeError(FromUtf8(
1083-
isolate, "The third parameter must be a pointer or undefined.")));
1096+
isolate, "The fourth parameter must be a pointer or undefined.")));
10841097
return;
10851098
};
10861099
NatTryCatch(isolate, [&args](Isolate* isolate) {
10871100
char* skresource = ToSKString(isolate, args[0].As<String>());
10881101
CJObject skparams = args[1].As<External>()->Value();
1102+
char* sksessionId = nullptr;
1103+
if (args[2]->IsString()) {
1104+
sksessionId = ToSKString(isolate, args[2].As<String>());
1105+
}
10891106
SKRequest skrequest = nullptr;
1090-
if (args[2]->IsExternal()) {
1091-
skrequest = args[2].As<External>()->Value();
1107+
if (args[3]->IsExternal()) {
1108+
skrequest = args[3].As<External>()->Value();
10921109
}
1093-
CJSON skresult =
1094-
SkipRuntime_Runtime__getAll(skresource, skparams, skrequest);
1110+
CJSON skresult = SkipRuntime_Runtime__getAll(skresource, skparams,
1111+
sksessionId, skrequest);
10951112
args.GetReturnValue().Set(External::New(isolate, skresult));
10961113
});
10971114
}
@@ -1104,28 +1121,44 @@ void GetForKeyOfRuntime(const FunctionCallbackInfo<Value>& args) {
11041121
FromUtf8(isolate, "The first parameter must be a string.")));
11051122
return;
11061123
}
1107-
if (!args[1]->IsExternal() || !args[2]->IsExternal()) {
1124+
if (!args[1]->IsExternal()) {
11081125
// Throw an Error that is passed back to JavaScript
1109-
isolate->ThrowException(Exception::TypeError(FromUtf8(
1110-
isolate, "The second and third parameters must be pointers.")));
1126+
isolate->ThrowException(Exception::TypeError(
1127+
FromUtf8(isolate, "The second parameter must be a pointer.")));
11111128
return;
11121129
}
1113-
if (!args[3]->IsExternal() && !args[3]->IsNull() && !args[3]->IsUndefined()) {
1130+
if (!args[2]->IsString() && !args[2]->IsNull()) {
1131+
// Throw an Error that is passed back to JavaScript
1132+
isolate->ThrowException(Exception::TypeError(
1133+
FromUtf8(isolate, "The third parameter must be a string or null.")));
1134+
return;
1135+
};
1136+
if (!args[3]->IsExternal() || !args[3]->IsExternal()) {
1137+
// Throw an Error that is passed back to JavaScript
1138+
isolate->ThrowException(Exception::TypeError(
1139+
FromUtf8(isolate, "The fourth parameter must be a pointer.")));
1140+
return;
1141+
}
1142+
if (!args[4]->IsExternal() && !args[4]->IsNull() && !args[4]->IsUndefined()) {
11141143
// Throw an Error that is passed back to JavaScript
11151144
isolate->ThrowException(Exception::TypeError(FromUtf8(
1116-
isolate, "The fourth parameter must be a pointer or undefined.")));
1145+
isolate, "The fifth parameter must be a pointer or undefined.")));
11171146
return;
11181147
};
11191148
NatTryCatch(isolate, [&args](Isolate* isolate) {
11201149
char* skresource = ToSKString(isolate, args[0].As<String>());
11211150
CJObject skparams = args[1].As<External>()->Value();
1122-
CJSON skkey = args[2].As<External>()->Value();
1151+
char* sksessionId = nullptr;
1152+
if (args[2]->IsString()) {
1153+
sksessionId = ToSKString(isolate, args[2].As<String>());
1154+
}
1155+
CJSON skkey = args[3].As<External>()->Value();
11231156
SKRequest skrequest = nullptr;
1124-
if (args[3]->IsExternal()) {
1125-
skrequest = args[3].As<External>()->Value();
1157+
if (args[4]->IsExternal()) {
1158+
skrequest = args[4].As<External>()->Value();
11261159
}
1127-
CJSON skresult =
1128-
SkipRuntime_Runtime__getForKey(skresource, skparams, skkey, skrequest);
1160+
CJSON skresult = SkipRuntime_Runtime__getForKey(
1161+
skresource, skparams, sksessionId, skkey, skrequest);
11291162
args.GetReturnValue().Set(External::New(isolate, skresult));
11301163
});
11311164
}

skipruntime-ts/core/src/api.ts

+2
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,8 @@ export interface LazyCompute<K extends Json, V extends Json> {
318318
* Skip Runtime internal state.
319319
*/
320320
export interface Context extends Managed {
321+
readonly session: (Json & DepSafe) | null;
322+
321323
/**
322324
* Create a lazy reactive collection.
323325
*

skipruntime-ts/core/src/binding.ts

+3
Original file line numberDiff line numberDiff line change
@@ -186,17 +186,20 @@ export interface FromBinding {
186186
identifier: string,
187187
resource: string,
188188
jsonParams: Pointer<Internal.CJObject>,
189+
sessionId: string | null,
189190
): Handle<Error>;
190191

191192
SkipRuntime_Runtime__getAll(
192193
resource: string,
193194
jsonParams: Pointer<Internal.CJObject>,
195+
sessionId: string | null,
194196
request: Pointer<Internal.Request> | null,
195197
): Pointer<Internal.CJObject | Internal.CJFloat>;
196198

197199
SkipRuntime_Runtime__getForKey(
198200
resource: string,
199201
jsonParams: Pointer<Internal.CJObject>,
202+
sessionId: string | null,
200203
key: Pointer<Internal.CJSON>,
201204
request: Pointer<Internal.Request> | null,
202205
): Pointer<Internal.CJObject | Internal.CJFloat>;

0 commit comments

Comments
 (0)