@motiadev/core 0.2.1-beta.73 → 0.2.1-beta.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/locked-data.d.ts +2 -1
- package/dist/src/locked-data.js +11 -16
- package/dist/src/python/context.py +3 -0
- package/dist/src/python/dot_dict.py +16 -0
- package/dist/src/python/python-runner.py +10 -2
- package/dist/src/python/rpc_stream_manager.py +50 -0
- package/dist/src/server.js +6 -6
- package/dist/src/socket-server.d.ts +2 -1
- package/dist/src/socket-server.js +5 -2
- package/dist/src/types-stream.d.ts +2 -0
- package/package.json +1 -1
|
@@ -32,6 +32,7 @@ export declare class LockedData {
|
|
|
32
32
|
pythonSteps(): Step[];
|
|
33
33
|
tsSteps(): Step[];
|
|
34
34
|
getStreams(): Record<string, StateStreamFactory<any>>;
|
|
35
|
+
listStreams(): Stream[];
|
|
35
36
|
findStream(path: string): Stream | undefined;
|
|
36
37
|
updateStep(oldStep: Step, newStep: Step, options?: {
|
|
37
38
|
disableTypeCreation?: boolean;
|
|
@@ -43,7 +44,7 @@ export declare class LockedData {
|
|
|
43
44
|
disableTypeCreation?: boolean;
|
|
44
45
|
}): void;
|
|
45
46
|
private createFactoryWrapper;
|
|
46
|
-
createStream<TData>(
|
|
47
|
+
createStream<TData>(baseStream: Omit<Stream, 'factory'>, options?: {
|
|
47
48
|
disableTypeCreation?: boolean;
|
|
48
49
|
}): StateStreamFactory<TData>;
|
|
49
50
|
deleteStream(stream: Stream, options?: {
|
package/dist/src/locked-data.js
CHANGED
|
@@ -79,14 +79,13 @@ class LockedData {
|
|
|
79
79
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
80
80
|
const streams = {};
|
|
81
81
|
for (const [key, value] of Object.entries(this.streams)) {
|
|
82
|
-
|
|
83
|
-
const streamFactory = baseConfig.storageType === 'custom' ? baseConfig.factory : null;
|
|
84
|
-
if (streamFactory) {
|
|
85
|
-
streams[key] = streamFactory;
|
|
86
|
-
}
|
|
82
|
+
streams[key] = value.factory;
|
|
87
83
|
}
|
|
88
84
|
return streams;
|
|
89
85
|
}
|
|
86
|
+
listStreams() {
|
|
87
|
+
return Object.values(this.streams);
|
|
88
|
+
}
|
|
90
89
|
findStream(path) {
|
|
91
90
|
return Object.values(this.streams).find((stream) => stream.filePath === path);
|
|
92
91
|
}
|
|
@@ -197,24 +196,23 @@ class LockedData {
|
|
|
197
196
|
return streamFactory();
|
|
198
197
|
};
|
|
199
198
|
}
|
|
200
|
-
createStream(
|
|
199
|
+
createStream(baseStream, options = {}) {
|
|
200
|
+
const stream = baseStream;
|
|
201
201
|
this.streams[stream.config.name] = stream;
|
|
202
202
|
this.streamHandlers['stream-created'].forEach((handler) => handler(stream));
|
|
203
|
-
let factory;
|
|
204
203
|
if (stream.config.baseConfig.storageType === 'state') {
|
|
205
|
-
factory = this.createFactoryWrapper(stream, () => new state_stream_1.InternalStateStream(this.state));
|
|
204
|
+
stream.factory = this.createFactoryWrapper(stream, () => new state_stream_1.InternalStateStream(this.state));
|
|
206
205
|
}
|
|
207
206
|
else {
|
|
208
|
-
factory = this.createFactoryWrapper(stream, stream.config.baseConfig.factory);
|
|
207
|
+
stream.factory = this.createFactoryWrapper(stream, stream.config.baseConfig.factory);
|
|
209
208
|
}
|
|
210
|
-
stream.config.baseConfig = { storageType: 'custom', factory };
|
|
211
209
|
if (!stream.hidden) {
|
|
212
210
|
this.printer.printStreamCreated(stream);
|
|
213
211
|
if (!options.disableTypeCreation) {
|
|
214
212
|
this.saveTypes();
|
|
215
213
|
}
|
|
216
214
|
}
|
|
217
|
-
return factory;
|
|
215
|
+
return stream.factory;
|
|
218
216
|
}
|
|
219
217
|
deleteStream(stream, options = {}) {
|
|
220
218
|
Object.entries(this.streams).forEach(([streamName, { filePath }]) => {
|
|
@@ -234,15 +232,12 @@ class LockedData {
|
|
|
234
232
|
if (oldStream.config.name !== stream.config.name) {
|
|
235
233
|
delete this.streams[oldStream.config.name];
|
|
236
234
|
}
|
|
237
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
238
|
-
let factory;
|
|
239
235
|
if (stream.config.baseConfig.storageType === 'state') {
|
|
240
|
-
factory = this.createFactoryWrapper(stream, () => new state_stream_1.InternalStateStream(this.state));
|
|
236
|
+
stream.factory = this.createFactoryWrapper(stream, () => new state_stream_1.InternalStateStream(this.state));
|
|
241
237
|
}
|
|
242
238
|
else {
|
|
243
|
-
factory = this.createFactoryWrapper(stream, stream.config.baseConfig.factory);
|
|
239
|
+
stream.factory = this.createFactoryWrapper(stream, stream.config.baseConfig.factory);
|
|
244
240
|
}
|
|
245
|
-
stream.config.baseConfig = { storageType: 'custom', factory };
|
|
246
241
|
this.streams[stream.config.name] = stream;
|
|
247
242
|
this.streamHandlers['stream-updated'].forEach((handler) => handler(stream));
|
|
248
243
|
if (!stream.hidden) {
|
|
@@ -3,6 +3,7 @@ from type_definitions import HandlerResult
|
|
|
3
3
|
from rpc import RpcSender
|
|
4
4
|
from rpc_state_manager import RpcStateManager
|
|
5
5
|
from logger import Logger
|
|
6
|
+
from dot_dict import DotDict
|
|
6
7
|
|
|
7
8
|
class Context:
|
|
8
9
|
def __init__(
|
|
@@ -10,11 +11,13 @@ class Context:
|
|
|
10
11
|
trace_id: str,
|
|
11
12
|
flows: List[str],
|
|
12
13
|
rpc: RpcSender,
|
|
14
|
+
streams: DotDict,
|
|
13
15
|
):
|
|
14
16
|
self.trace_id = trace_id
|
|
15
17
|
self.flows = flows
|
|
16
18
|
self.rpc = rpc
|
|
17
19
|
self.state = RpcStateManager(rpc)
|
|
20
|
+
self.streams = streams
|
|
18
21
|
self.logger = Logger(self.trace_id, self.flows, rpc)
|
|
19
22
|
|
|
20
23
|
async def emit(self, event: Any) -> Optional[HandlerResult]:
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
class DotDict(dict):
|
|
2
|
+
def __getattr__(self, key):
|
|
3
|
+
try:
|
|
4
|
+
value = self[key]
|
|
5
|
+
return DotDict(value) if isinstance(value, dict) else value
|
|
6
|
+
except KeyError:
|
|
7
|
+
raise AttributeError(f"No such attribute: {key}")
|
|
8
|
+
|
|
9
|
+
def __setattr__(self, key, value):
|
|
10
|
+
self[key] = value
|
|
11
|
+
|
|
12
|
+
def __delattr__(self, key):
|
|
13
|
+
try:
|
|
14
|
+
del self[key]
|
|
15
|
+
except KeyError:
|
|
16
|
+
raise AttributeError(f"No such attribute: {key}")
|
|
@@ -5,12 +5,13 @@ import os
|
|
|
5
5
|
import asyncio
|
|
6
6
|
import traceback
|
|
7
7
|
from typing import Optional, Any, Callable, List, Dict
|
|
8
|
-
|
|
9
8
|
from rpc import RpcSender
|
|
10
9
|
from type_definitions import FlowConfig, ApiResponse
|
|
11
10
|
from context import Context
|
|
12
11
|
from validation import validate_with_jsonschema
|
|
13
12
|
from middleware import compose_middleware
|
|
13
|
+
from rpc_stream_manager import RpcStreamManager
|
|
14
|
+
from dot_dict import DotDict
|
|
14
15
|
|
|
15
16
|
def parse_args(arg: str) -> Dict:
|
|
16
17
|
"""Parse command line arguments into HandlerArgs"""
|
|
@@ -84,7 +85,14 @@ async def run_python_module(file_path: str, rpc: RpcSender, args: Dict) -> None:
|
|
|
84
85
|
flows = args.get("flows") or []
|
|
85
86
|
data = args.get("data")
|
|
86
87
|
context_in_first_arg = args.get("contextInFirstArg")
|
|
87
|
-
|
|
88
|
+
streams_config = args.get("streams") or []
|
|
89
|
+
|
|
90
|
+
streams = DotDict()
|
|
91
|
+
for item in streams_config:
|
|
92
|
+
name = item.get("name")
|
|
93
|
+
streams[name] = RpcStreamManager(name, rpc)
|
|
94
|
+
|
|
95
|
+
context = Context(trace_id, flows, rpc, streams)
|
|
88
96
|
|
|
89
97
|
validation_result = await validate_handler_input(module, args, context, is_api_handler)
|
|
90
98
|
if validation_result:
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
import asyncio
|
|
3
|
+
from rpc import RpcSender
|
|
4
|
+
import functools
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
class RpcStreamManager:
|
|
8
|
+
def __init__(self, stream_name: str,rpc: RpcSender):
|
|
9
|
+
self.rpc = rpc
|
|
10
|
+
self.stream_name = stream_name
|
|
11
|
+
self._loop = asyncio.get_event_loop()
|
|
12
|
+
|
|
13
|
+
async def get(self, group_id: str, id: str) -> asyncio.Future[Any]:
|
|
14
|
+
result = await self.rpc.send(f'streams.{self.stream_name}.get', {'groupId': group_id, 'id': id})
|
|
15
|
+
return result
|
|
16
|
+
|
|
17
|
+
async def set(self, group_id: str, id: str, data: Any) -> asyncio.Future[None]:
|
|
18
|
+
future = await self.rpc.send(f'streams.{self.stream_name}.set', {'groupId': group_id, 'id': id, 'data': data})
|
|
19
|
+
return future
|
|
20
|
+
|
|
21
|
+
async def delete(self, group_id: str, id: str) -> asyncio.Future[None]:
|
|
22
|
+
return await self.rpc.send(f'streams.{self.stream_name}.delete', {'groupId': group_id, 'id': id})
|
|
23
|
+
|
|
24
|
+
async def getGroup(self, group_id: str) -> asyncio.Future[None]:
|
|
25
|
+
return await self.rpc.send(f'streams.{self.stream_name}.getGroup', {'groupId': group_id})
|
|
26
|
+
|
|
27
|
+
# Add wrappers to handle non-awaited coroutines
|
|
28
|
+
def __getattribute__(self, name):
|
|
29
|
+
attr = super().__getattribute__(name)
|
|
30
|
+
if name in ('get', 'set', 'delete', 'getGroup') and asyncio.iscoroutinefunction(attr):
|
|
31
|
+
@functools.wraps(attr)
|
|
32
|
+
def wrapper(*args, **kwargs):
|
|
33
|
+
coro = attr(*args, **kwargs)
|
|
34
|
+
# Check if this is being awaited
|
|
35
|
+
frame = sys._getframe(1)
|
|
36
|
+
if frame.f_code.co_name != '__await__':
|
|
37
|
+
# Not being awaited, schedule in background
|
|
38
|
+
# But we need to make sure this task completes before the handler returns
|
|
39
|
+
# So we'll return the task for the caller to await if needed
|
|
40
|
+
task = asyncio.create_task(coro)
|
|
41
|
+
# Add error handling for the background task
|
|
42
|
+
def handle_exception(t):
|
|
43
|
+
if t.done() and not t.cancelled() and t.exception():
|
|
44
|
+
print(f"Unhandled exception in background task: {t.exception()}", file=sys.stderr)
|
|
45
|
+
task.add_done_callback(handle_exception)
|
|
46
|
+
return task
|
|
47
|
+
# Being awaited, return coroutine as normal
|
|
48
|
+
return coro
|
|
49
|
+
return wrapper
|
|
50
|
+
return attr
|
package/dist/src/server.js
CHANGED
|
@@ -51,9 +51,9 @@ const createServer = async (lockedData, eventManager, state, config) => {
|
|
|
51
51
|
return () => {
|
|
52
52
|
const suuper = stream();
|
|
53
53
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
-
const wrapObject = (id, object) => ({
|
|
54
|
+
const wrapObject = (groupId, id, object) => ({
|
|
55
55
|
...object,
|
|
56
|
-
__motia: { type: 'state-stream', streamName, id },
|
|
56
|
+
__motia: { type: 'state-stream', streamName, groupId, id },
|
|
57
57
|
});
|
|
58
58
|
const wrapper = {
|
|
59
59
|
...suuper,
|
|
@@ -62,7 +62,7 @@ const createServer = async (lockedData, eventManager, state, config) => {
|
|
|
62
62
|
},
|
|
63
63
|
async get(groupId, id) {
|
|
64
64
|
const result = await suuper.get.apply(wrapper, [groupId, id]);
|
|
65
|
-
return wrapObject(id, result);
|
|
65
|
+
return wrapObject(groupId, id, result);
|
|
66
66
|
},
|
|
67
67
|
async set(groupId, id, data) {
|
|
68
68
|
if (!data) {
|
|
@@ -71,7 +71,7 @@ const createServer = async (lockedData, eventManager, state, config) => {
|
|
|
71
71
|
const exists = await suuper.get(groupId, id);
|
|
72
72
|
const updated = await suuper.set.apply(wrapper, [groupId, id, data]);
|
|
73
73
|
const result = updated ?? data;
|
|
74
|
-
const wrappedResult = wrapObject(id, result);
|
|
74
|
+
const wrappedResult = wrapObject(groupId, id, result);
|
|
75
75
|
const type = exists ? 'update' : 'create';
|
|
76
76
|
pushEvent({ streamName, groupId, id, event: { type, data: result } });
|
|
77
77
|
return wrappedResult;
|
|
@@ -79,11 +79,11 @@ const createServer = async (lockedData, eventManager, state, config) => {
|
|
|
79
79
|
async delete(groupId, id) {
|
|
80
80
|
const result = await suuper.delete.apply(wrapper, [groupId, id]);
|
|
81
81
|
pushEvent({ streamName, groupId, id, event: { type: 'delete', data: result } });
|
|
82
|
-
return wrapObject(id, result);
|
|
82
|
+
return wrapObject(groupId, id, result);
|
|
83
83
|
},
|
|
84
84
|
async getGroup(groupId) {
|
|
85
85
|
const list = await suuper.getGroup.apply(wrapper, [groupId]);
|
|
86
|
-
return list.map((object) => wrapObject(object.id, object));
|
|
86
|
+
return list.map((object) => wrapObject(groupId, object.id, object));
|
|
87
87
|
},
|
|
88
88
|
};
|
|
89
89
|
return wrapper;
|
|
@@ -25,6 +25,7 @@ type StreamEvent<TData> = {
|
|
|
25
25
|
};
|
|
26
26
|
};
|
|
27
27
|
type EventMessage<TData> = BaseMessage & {
|
|
28
|
+
timestamp: number;
|
|
28
29
|
event: StreamEvent<TData>;
|
|
29
30
|
};
|
|
30
31
|
type Props = {
|
|
@@ -33,7 +34,7 @@ type Props = {
|
|
|
33
34
|
onJoinGroup: <TData>(streamName: string, groupId: string) => Promise<TData[] | undefined>;
|
|
34
35
|
};
|
|
35
36
|
export declare const createSocketServer: ({ server, onJoin, onJoinGroup }: Props) => {
|
|
36
|
-
pushEvent: <TData>(message: EventMessage<TData>) => void;
|
|
37
|
+
pushEvent: <TData>(message: Omit<EventMessage<TData>, "timestamp">) => void;
|
|
37
38
|
socketServer: WsServer<typeof import("ws"), typeof http.IncomingMessage>;
|
|
38
39
|
};
|
|
39
40
|
export {};
|
|
@@ -22,6 +22,7 @@ const createSocketServer = ({ server, onJoin, onJoinGroup }) => {
|
|
|
22
22
|
const item = await onJoin(message.data.streamName, message.data.groupId, message.data.id);
|
|
23
23
|
if (item) {
|
|
24
24
|
const resultMessage = {
|
|
25
|
+
timestamp: Date.now(),
|
|
25
26
|
streamName: message.data.streamName,
|
|
26
27
|
groupId: message.data.groupId,
|
|
27
28
|
id: message.data.id,
|
|
@@ -34,6 +35,7 @@ const createSocketServer = ({ server, onJoin, onJoinGroup }) => {
|
|
|
34
35
|
const items = await onJoinGroup(message.data.streamName, message.data.groupId);
|
|
35
36
|
if (items) {
|
|
36
37
|
const resultMessage = {
|
|
38
|
+
timestamp: Date.now(),
|
|
37
39
|
streamName: message.data.streamName,
|
|
38
40
|
groupId: message.data.groupId,
|
|
39
41
|
event: { type: 'sync', data: items },
|
|
@@ -61,13 +63,14 @@ const createSocketServer = ({ server, onJoin, onJoinGroup }) => {
|
|
|
61
63
|
const pushEvent = (message) => {
|
|
62
64
|
const { groupId, streamName, id } = message;
|
|
63
65
|
const groupRoom = getRoom({ streamName, groupId });
|
|
66
|
+
const eventMessage = JSON.stringify({ timestamp: Date.now(), ...message });
|
|
64
67
|
if (rooms[groupRoom]) {
|
|
65
|
-
rooms[groupRoom].forEach((socket) => socket.send(
|
|
68
|
+
rooms[groupRoom].forEach((socket) => socket.send(eventMessage));
|
|
66
69
|
}
|
|
67
70
|
if (id) {
|
|
68
71
|
const itemRoom = getRoom({ groupId, streamName, id });
|
|
69
72
|
if (rooms[itemRoom]) {
|
|
70
|
-
rooms[itemRoom].forEach((socket) => socket.send(
|
|
73
|
+
rooms[itemRoom].forEach((socket) => socket.send(eventMessage));
|
|
71
74
|
}
|
|
72
75
|
}
|
|
73
76
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ZodObject } from 'zod';
|
|
2
|
+
import { StateStreamFactory } from './state-stream';
|
|
2
3
|
export interface StateStreamConfig {
|
|
3
4
|
name: string;
|
|
4
5
|
schema: ZodObject<any>;
|
|
@@ -24,6 +25,7 @@ export type Stream<TConfig extends StateStreamConfig = StateStreamConfig> = {
|
|
|
24
25
|
filePath: string;
|
|
25
26
|
config: TConfig;
|
|
26
27
|
hidden?: boolean;
|
|
28
|
+
factory: StateStreamFactory<unknown>;
|
|
27
29
|
};
|
|
28
30
|
export interface IStateStream<TData> {
|
|
29
31
|
get(groupId: string, id: string): Promise<BaseStreamItem<TData> | null>;
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@motiadev/core",
|
|
3
3
|
"description": "Core functionality for the Motia framework, providing the foundation for building event-driven workflows.",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
|
-
"version": "0.2.1-beta.
|
|
5
|
+
"version": "0.2.1-beta.74",
|
|
6
6
|
"dependencies": {
|
|
7
7
|
"@amplitude/analytics-node": "^1.3.8",
|
|
8
8
|
"body-parser": "^1.20.3",
|