@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.
@@ -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>(stream: Stream, options?: {
47
+ createStream<TData>(baseStream: Omit<Stream, 'factory'>, options?: {
47
48
  disableTypeCreation?: boolean;
48
49
  }): StateStreamFactory<TData>;
49
50
  deleteStream(stream: Stream, options?: {
@@ -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
- const baseConfig = value.config.baseConfig;
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(stream, options = {}) {
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
- context = Context(trace_id, flows, rpc)
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
@@ -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(JSON.stringify(message)));
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(JSON.stringify(message)));
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.73",
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",