@xano/xanoscript-language-server 12.7.0 → 12.9.0

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/README.md CHANGED
@@ -57,8 +57,12 @@ The parser supports the following XanoScript constructs:
57
57
  | `mcp_server` | MCP server definitions |
58
58
  | `mcp_server_trigger` | MCP server trigger handlers |
59
59
  | `middleware` | Middleware definitions |
60
- | `realtime_channel` | Realtime channel definitions |
61
- | `realtime_trigger` | Realtime trigger handlers |
60
+ | `realtime_channel` | Realtime v1 channel definitions |
61
+ | `realtime_trigger` | Realtime v1 trigger handlers |
62
+ | `channel` | Realtime v2 channel definitions (path templates) |
63
+ | `message` | Realtime v2 channel message handlers |
64
+ | `channel_trigger` | Realtime v2 channel lifecycle triggers (join/leave) |
65
+ | `realtime_server_trigger` | Realtime v2 connection lifecycle triggers (connect/disconnect) |
62
66
  | `tool` | Tool definitions |
63
67
  | `workspace` | Workspace definitions |
64
68
  | `workspace_trigger` | Workspace trigger handlers |
@@ -0,0 +1,72 @@
1
+ import { ServerDetailsToken } from "./ai.js";
2
+ import { Identifier } from "./identifier.js";
3
+ import { createTokenByName } from "./utils.js";
4
+
5
+ // "server" (channel's realtime_server reference).
6
+ // longer_alt points at server_details so the longer keyword still wins.
7
+ export const ServerToken = createTokenByName("server", {
8
+ longer_alt: [ServerDetailsToken, Identifier],
9
+ categories: [Identifier], // could also be an identifier
10
+ });
11
+
12
+ // "access"
13
+ export const AccessToken = createTokenByName("access", {
14
+ longer_alt: Identifier,
15
+ categories: [Identifier], // could also be an identifier
16
+ });
17
+
18
+ // "publish"
19
+ export const PublishToken = createTokenByName("publish", {
20
+ longer_alt: Identifier,
21
+ categories: [Identifier], // could also be an identifier
22
+ });
23
+
24
+ // "conversation"
25
+ export const ConversationToken = createTokenByName("conversation", {
26
+ longer_alt: Identifier,
27
+ categories: [Identifier], // could also be an identifier
28
+ });
29
+
30
+ // "delivery"
31
+ export const DeliveryToken = createTokenByName("delivery", {
32
+ longer_alt: Identifier,
33
+ categories: [Identifier], // could also be an identifier
34
+ });
35
+
36
+ // "rate_limit"
37
+ export const RateLimitToken = createTokenByName("rate_limit", {
38
+ longer_alt: Identifier,
39
+ categories: [Identifier], // could also be an identifier
40
+ });
41
+
42
+ // ServerToken must be lexed AFTER `server_details` (a longer AI keyword) so it
43
+ // does not shadow it; the rest must be lexed BEFORE shorter-prefix keywords
44
+ // (e.g. `delivery` before `del`). They are therefore spread at two different
45
+ // points in the master token list — see lexer/tokens.js.
46
+ export const ChannelServerToken = ServerToken;
47
+
48
+ export const ChannelObjectTokens = [
49
+ AccessToken,
50
+ PublishToken,
51
+ ConversationToken,
52
+ DeliveryToken,
53
+ RateLimitToken,
54
+ ];
55
+
56
+ /**
57
+ * Maps a token name to a type
58
+ * @param {string} token the token name
59
+ */
60
+ export function mapTokenToType(token) {
61
+ switch (token) {
62
+ case ServerToken.name:
63
+ case AccessToken.name:
64
+ case PublishToken.name:
65
+ case ConversationToken.name:
66
+ case DeliveryToken.name:
67
+ case RateLimitToken.name:
68
+ return "keyword";
69
+ default:
70
+ return null;
71
+ }
72
+ }
@@ -0,0 +1,23 @@
1
+ import { Identifier } from "./identifier.js";
2
+ import { createTokenByName } from "./utils.js";
3
+
4
+ // "channel_trigger"
5
+ export const ChannelTriggerToken = createTokenByName("channel_trigger", {
6
+ longer_alt: Identifier,
7
+ categories: [Identifier], // could also be an identifier
8
+ });
9
+
10
+ export const ChannelTriggerTokens = [ChannelTriggerToken];
11
+
12
+ /**
13
+ * Maps a token name to a type
14
+ * @param {string} token the token name
15
+ */
16
+ export function mapTokenToType(token) {
17
+ switch (token) {
18
+ case ChannelTriggerToken.name:
19
+ return "keyword";
20
+ default:
21
+ return null;
22
+ }
23
+ }
@@ -0,0 +1,31 @@
1
+ import { Identifier } from "./identifier.js";
2
+ import { createTokenByName } from "./utils.js";
3
+
4
+ // "message"
5
+ export const MessageToken = createTokenByName("message", {
6
+ longer_alt: Identifier,
7
+ categories: [Identifier], // could also be an identifier
8
+ });
9
+
10
+ // "deliver_to"
11
+ export const DeliverToToken = createTokenByName("deliver_to", {
12
+ longer_alt: Identifier,
13
+ categories: [Identifier], // could also be an identifier
14
+ });
15
+
16
+ export const MessageTokens = [MessageToken, DeliverToToken];
17
+
18
+ /**
19
+ * Maps a token name to a type
20
+ * @param {string} token the token name
21
+ */
22
+ export function mapTokenToType(token) {
23
+ switch (token) {
24
+ case MessageToken.name:
25
+ return "keyword";
26
+ case DeliverToToken.name:
27
+ return "variable";
28
+ default:
29
+ return null;
30
+ }
31
+ }
@@ -0,0 +1,26 @@
1
+ import { Identifier } from "./identifier.js";
2
+ import { createTokenByName } from "./utils.js";
3
+
4
+ // realtime
5
+ export const RealtimeToken = createTokenByName("realtime", {
6
+ longer_alt: Identifier,
7
+ categories: [Identifier],
8
+ });
9
+
10
+ // get_session
11
+ export const GetSessionToken = createTokenByName("get_session", {
12
+ longer_alt: Identifier,
13
+ categories: [Identifier],
14
+ });
15
+
16
+ export const RealtimeStatementTokens = [RealtimeToken, GetSessionToken];
17
+
18
+ export function mapTokenToType(token) {
19
+ switch (token) {
20
+ case RealtimeToken.name:
21
+ case GetSessionToken.name:
22
+ return "function";
23
+ default:
24
+ return null;
25
+ }
26
+ }
@@ -0,0 +1,38 @@
1
+ import { Identifier } from "./identifier.js";
2
+ import { createTokenByName } from "./utils.js";
3
+
4
+ // "realtime_server_trigger"
5
+ export const RealtimeServerTriggerToken = createTokenByName(
6
+ "realtime_server_trigger",
7
+ {
8
+ longer_alt: Identifier,
9
+ categories: [Identifier], // could also be an identifier
10
+ }
11
+ );
12
+
13
+ // "realtime_server" (parent-reference keyword used by realtimeServerClause).
14
+ // longer_alt points at the trigger token so `realtime_server_trigger` wins
15
+ // over the shorter `realtime_server` prefix.
16
+ export const RealtimeServerToken = createTokenByName("realtime_server", {
17
+ longer_alt: RealtimeServerTriggerToken,
18
+ categories: [Identifier], // could also be an identifier
19
+ });
20
+
21
+ export const RealtimeServerTriggerTokens = [
22
+ RealtimeServerTriggerToken,
23
+ RealtimeServerToken,
24
+ ];
25
+
26
+ /**
27
+ * Maps a token name to a type
28
+ * @param {string} token the token name
29
+ */
30
+ export function mapTokenToType(token) {
31
+ switch (token) {
32
+ case RealtimeServerTriggerToken.name:
33
+ case RealtimeServerToken.name:
34
+ return "keyword";
35
+ default:
36
+ return null;
37
+ }
38
+ }
package/lexer/tokens.js CHANGED
@@ -39,6 +39,15 @@ import {
39
39
  mapTokenToType as mapBranchTokenToType,
40
40
  } from "./branch.js";
41
41
  import { CastTokens, mapTokenToType as mapCastTokenToType } from "./cast.js";
42
+ import {
43
+ ChannelObjectTokens,
44
+ ChannelServerToken,
45
+ mapTokenToType as mapChannelObjectTokenToType,
46
+ } from "./channel.js";
47
+ import {
48
+ ChannelTriggerTokens,
49
+ mapTokenToType as mapChannelTriggerTokenToType,
50
+ } from "./channel_trigger.js";
42
51
  import { CloudTokens, mapTokenToType as mapCloudTokenToType } from "./cloud.js";
43
52
  import {
44
53
  ColumnTokens,
@@ -83,6 +92,10 @@ import {
83
92
  mapTokenToType as mapMcpServerTriggerTokenToType,
84
93
  McpServerTriggerTokens,
85
94
  } from "./mcp_server_trigger.js";
95
+ import {
96
+ mapTokenToType as mapMessageTokenToType,
97
+ MessageTokens,
98
+ } from "./message.js";
86
99
  import {
87
100
  mapTokenToType as mapMiddlewareTokenToType,
88
101
  MiddlewareTokens,
@@ -96,6 +109,14 @@ import {
96
109
  ObjectTokens,
97
110
  } from "./object.js";
98
111
  import { mapTokenToType as mapQueryTokenToType, QueryTokens } from "./query.js";
112
+ import {
113
+ mapTokenToType as mapRealtimeTokenToType,
114
+ RealtimeStatementTokens,
115
+ } from "./realtime.js";
116
+ import {
117
+ mapTokenToType as mapRealtimeServerTriggerTokenToType,
118
+ RealtimeServerTriggerTokens,
119
+ } from "./realtime_server_trigger.js";
99
120
  import {
100
121
  mapTokenToType as mapRealtimeTriggerTokenToType,
101
122
  RealtimeTriggerTokens,
@@ -365,7 +386,13 @@ export const allTokens = uniq([
365
386
  ...AgentTriggerTokens,
366
387
  ...McpServerTriggerTokens,
367
388
  ...McpServerTokens,
389
+ ...RealtimeServerTriggerTokens,
368
390
  ...RealtimeTriggerTokens,
391
+ ...ChannelTriggerTokens,
392
+ ...MessageTokens,
393
+ // channel-object keywords (access/publish/conversation/delivery/rate_limit)
394
+ // must precede shorter-prefix keywords like `del` (db/redis)
395
+ ...ChannelObjectTokens,
369
396
  ...ArrayFnTokens,
370
397
  ...ColumnTokens,
371
398
  ...LiteralTokens,
@@ -412,6 +439,10 @@ export const allTokens = uniq([
412
439
  ...WorkflowTestTokens,
413
440
  ...ApiGroupTokens,
414
441
  ...ApiFnTokens,
442
+ // bare `realtime` must be lexed AFTER the longer `realtime_*` keywords
443
+ // (realtime_trigger/realtime_channel/realtime_server + api.realtime_event)
444
+ // so it does not shadow them (UNREACHABLE lexer error otherwise).
445
+ ...RealtimeStatementTokens,
415
446
  ...VariableTokens,
416
447
  ...TableTokens,
417
448
  ...CloudTokens,
@@ -427,6 +458,9 @@ export const allTokens = uniq([
427
458
  ...VarTokens,
428
459
  ...MathTokens,
429
460
  ...AiTokens, // AI tokens must come before AgentTokens to avoid conflicts
461
+ // `server` must be lexed AFTER `server_details` (an AI keyword) so it does not
462
+ // shadow it; its longer_alt keeps `server_details` input matching correctly.
463
+ ChannelServerToken,
430
464
  ...AgentTokens, // Must come before ToolTokens because of ToolsToken
431
465
  ...ToolTokens,
432
466
  ...AddonTokens,
@@ -450,6 +484,8 @@ const tokenMappers = [
450
484
  mapArrayTokenToType,
451
485
  mapBranchTokenToType,
452
486
  mapCastTokenToType,
487
+ mapChannelObjectTokenToType,
488
+ mapChannelTriggerTokenToType,
453
489
  mapCloudTokenToType,
454
490
  mapColumnTokenToType,
455
491
  mapCommentTokenToType,
@@ -464,10 +500,13 @@ const tokenMappers = [
464
500
  mapMathTokenToType,
465
501
  mapMcpServerTokenToType,
466
502
  mapMcpServerTriggerTokenToType,
503
+ mapMessageTokenToType,
467
504
  mapMiddlewareTokenToType,
468
505
  mapMultilineTokenToType,
469
506
  mapObjectTokenToType,
470
507
  mapQueryTokenToType,
508
+ mapRealtimeTokenToType,
509
+ mapRealtimeServerTriggerTokenToType,
471
510
  mapRealtimeTriggerTokenToType,
472
511
  mapRedisTokenToType,
473
512
  mapRunTokenToType,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xano/xanoscript-language-server",
3
- "version": "12.7.0",
3
+ "version": "12.9.0",
4
4
  "description": "Language Server Protocol implementation for XanoScript",
5
5
  "type": "module",
6
6
  "main": "server.js",
@@ -41,6 +41,8 @@
41
41
  "scripts": {
42
42
  "test-one": "mocha",
43
43
  "test": "mocha \"**/*.spec.js\"",
44
+ "coverage": "c8 mocha \"**/*.spec.js\"",
45
+ "coverage:report": "c8 --reporter=html --reporter=text mocha \"**/*.spec.js\"",
44
46
  "test-alt": "NODE_OPTIONS=\"--trace-warnings\" mocha --loader node \"*/**/*.spec.js\"",
45
47
  "lint": "eslint . --ext .js",
46
48
  "lint:fix": "eslint . --ext .js --fix",
@@ -60,6 +62,7 @@
60
62
  },
61
63
  "devDependencies": {
62
64
  "@eslint/js": "^9.22.0",
65
+ "c8": "^11.0.0",
63
66
  "chai": "^5.2.0",
64
67
  "eslint": "^9.22.0",
65
68
  "eslint-plugin-simple-import-sort": "^12.1.1",
@@ -0,0 +1,189 @@
1
+ import {
2
+ AccessToken,
3
+ ConversationToken,
4
+ DeliveryToken,
5
+ PublishToken,
6
+ RateLimitToken,
7
+ ServerToken,
8
+ } from "../lexer/channel.js";
9
+ import { ChannelToken } from "../lexer/columns.js";
10
+ import { EqualToken, LCurly, RCurly } from "../lexer/control.js";
11
+ import { StringLiteral } from "../lexer/literal.js";
12
+ import { Identifier, NewlineToken } from "../lexer/tokens.js";
13
+
14
+ export function channelDeclaration($) {
15
+ return () => {
16
+ let hasServer = false;
17
+ let hasAccess = false;
18
+ let hasPublish = false;
19
+ let hasConversation = false;
20
+ let hasDelivery = false;
21
+ let hasRateLimit = false;
22
+ let hasHistory = false;
23
+ let hasInput = false;
24
+ let hasDescription = false;
25
+ let hasGuid = false;
26
+ let hasTags = false;
27
+
28
+ $.sectionStack.push("channelDeclaration");
29
+ // Allow leading comments and newlines before the channel declaration
30
+ $.SUBRULE($.optionalCommentBlockFn);
31
+
32
+ const parent = $.CONSUME(ChannelToken); // channel
33
+ const nameToken = $.OR([
34
+ { ALT: () => $.CONSUME(StringLiteral) },
35
+ { ALT: () => $.CONSUME(Identifier) },
36
+ ]);
37
+ $.validateDeclarationName(nameToken, "channel");
38
+ $.CONSUME(LCurly); // "{"
39
+ $.MANY(() => {
40
+ $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
41
+ $.OR2([
42
+ {
43
+ ALT: () => $.SUBRULE($.commentBlockFn),
44
+ },
45
+ {
46
+ GATE: () => !hasServer,
47
+ ALT: () => {
48
+ hasServer = true;
49
+ const subParent = $.CONSUME(ServerToken); // "server"
50
+ $.CONSUME(EqualToken); // "="
51
+ const result = $.CONSUME2(StringLiteral); // "server_name"
52
+ if (result.image === `""`) {
53
+ $.addMissingError(subParent, "server cannot be empty");
54
+ }
55
+ },
56
+ },
57
+ {
58
+ GATE: () => !hasAccess,
59
+ ALT: () => {
60
+ hasAccess = true;
61
+ const subParent = $.CONSUME(AccessToken); // "access"
62
+ $.CONSUME2(EqualToken); // "="
63
+ $.SUBRULE($.schemaParseObjectFn, {
64
+ ARGS: [
65
+ subParent,
66
+ {
67
+ "anonymous?": "[boolean]",
68
+ "presence?": "[boolean]",
69
+ },
70
+ ],
71
+ });
72
+ },
73
+ },
74
+ {
75
+ GATE: () => !hasPublish,
76
+ ALT: () => {
77
+ hasPublish = true;
78
+ const subParent = $.CONSUME(PublishToken); // "publish"
79
+ $.CONSUME3(EqualToken); // "="
80
+ $.SUBRULE1($.schemaParseObjectFn, {
81
+ ARGS: [
82
+ subParent,
83
+ {
84
+ "who?": ["nobody", "anyone", "authenticated"],
85
+ "direct?": "[boolean]",
86
+ },
87
+ ],
88
+ });
89
+ },
90
+ },
91
+ {
92
+ GATE: () => !hasConversation,
93
+ ALT: () => {
94
+ hasConversation = true;
95
+ const subParent = $.CONSUME(ConversationToken); // "conversation"
96
+ $.CONSUME4(EqualToken); // "="
97
+ $.SUBRULE2($.schemaParseObjectFn, {
98
+ ARGS: [
99
+ subParent,
100
+ {
101
+ "active?": "[boolean]",
102
+ "limit?": "[number]",
103
+ "ttl?": "[number]",
104
+ },
105
+ ],
106
+ });
107
+ },
108
+ },
109
+ {
110
+ GATE: () => !hasDelivery,
111
+ ALT: () => {
112
+ hasDelivery = true;
113
+ const subParent = $.CONSUME(DeliveryToken); // "delivery"
114
+ $.CONSUME5(EqualToken); // "="
115
+ $.SUBRULE3($.schemaParseObjectFn, {
116
+ ARGS: [
117
+ subParent,
118
+ {
119
+ "guarantee?": ["at_most_once", "at_least_once"],
120
+ "per_recipient?": "[boolean]",
121
+ },
122
+ ],
123
+ });
124
+ },
125
+ },
126
+ {
127
+ GATE: () => !hasRateLimit,
128
+ ALT: () => {
129
+ hasRateLimit = true;
130
+ const subParent = $.CONSUME(RateLimitToken); // "rate_limit"
131
+ $.CONSUME6(EqualToken); // "="
132
+ $.SUBRULE4($.schemaParseObjectFn, {
133
+ ARGS: [
134
+ subParent,
135
+ {
136
+ "messages_per_minute?": "[number]",
137
+ },
138
+ ],
139
+ });
140
+ },
141
+ },
142
+ {
143
+ GATE: () => !hasHistory,
144
+ ALT: () => {
145
+ hasHistory = true;
146
+ $.SUBRULE($.historyClause);
147
+ },
148
+ },
149
+ {
150
+ GATE: () => !hasInput,
151
+ ALT: () => {
152
+ hasInput = true;
153
+ $.SUBRULE($.inputClause);
154
+ },
155
+ },
156
+ {
157
+ GATE: () => !hasDescription,
158
+ ALT: () => {
159
+ hasDescription = true;
160
+ $.SUBRULE($.descriptionFieldAttribute);
161
+ },
162
+ },
163
+ {
164
+ GATE: () => !hasGuid,
165
+ ALT: () => {
166
+ hasGuid = true;
167
+ $.SUBRULE($.guidFieldAttribute);
168
+ },
169
+ },
170
+ {
171
+ GATE: () => !hasTags,
172
+ ALT: () => {
173
+ hasTags = true;
174
+ $.SUBRULE($.tagsAttribute);
175
+ },
176
+ },
177
+ ]);
178
+ });
179
+
180
+ if (!hasServer) {
181
+ $.addMissingError(parent, "{} is missing a server clause");
182
+ }
183
+
184
+ $.MANY1(() => $.CONSUME1(NewlineToken)); // optional new line
185
+ $.CONSUME(RCurly); // "}"
186
+ $.MANY2(() => $.CONSUME2(NewlineToken)); // optional new line
187
+ $.sectionStack.pop();
188
+ };
189
+ }
@@ -0,0 +1,145 @@
1
+ import { ChannelTriggerToken } from "../lexer/channel_trigger.js";
2
+ import { EqualToken, LCurly, RCurly } from "../lexer/control.js";
3
+ import { StringLiteral } from "../lexer/literal.js";
4
+ import { ActionsToken, ActiveToken } from "../lexer/mcp_server_trigger.js";
5
+ import { Identifier, NewlineToken } from "../lexer/tokens.js";
6
+
7
+ export function channelTriggerDeclaration($) {
8
+ return () => {
9
+ let hasActions = false;
10
+ let hasActive = false;
11
+ let hasChannel = false;
12
+ let hasDescription = false;
13
+ let hasGuid = false;
14
+ let hasHistory = false;
15
+ let hasInput = false;
16
+ let hasResponse = false;
17
+ let hasStack = false;
18
+ let hasTags = false;
19
+
20
+ $.sectionStack.push("channelTriggerDeclaration");
21
+ // Allow leading comments and newlines before the channel_trigger declaration
22
+ $.SUBRULE($.optionalCommentBlockFn);
23
+
24
+ const parent = $.CONSUME(ChannelTriggerToken); // channel_trigger
25
+ const nameToken = $.OR([
26
+ { ALT: () => $.CONSUME(StringLiteral) },
27
+ { ALT: () => $.CONSUME(Identifier) },
28
+ ]);
29
+ $.validateDeclarationName(nameToken, "channel_trigger");
30
+ $.CONSUME(LCurly); // "{"
31
+ $.MANY(() => {
32
+ $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
33
+ $.OR2([
34
+ {
35
+ ALT: () => $.SUBRULE($.commentBlockFn),
36
+ },
37
+ {
38
+ GATE: () => !hasActions,
39
+ ALT: () => {
40
+ hasActions = true;
41
+
42
+ const subParent = $.CONSUME(ActionsToken); // "actions"
43
+ $.CONSUME(EqualToken); // "="
44
+
45
+ $.SUBRULE($.schemaParseObjectFn, {
46
+ ARGS: [
47
+ subParent,
48
+ {
49
+ "join?": "[boolean]",
50
+ "leave?": "[boolean]",
51
+ },
52
+ ],
53
+ });
54
+ },
55
+ },
56
+ {
57
+ GATE: () => !hasActive,
58
+ ALT: () => {
59
+ hasActive = true;
60
+ $.CONSUME(ActiveToken);
61
+ $.CONSUME2(EqualToken);
62
+ $.SUBRULE($.booleanValue);
63
+ },
64
+ },
65
+ {
66
+ GATE: () => !hasChannel,
67
+ ALT: () => {
68
+ hasChannel = true;
69
+ $.SUBRULE($.channelClause);
70
+ },
71
+ },
72
+ {
73
+ GATE: () => !hasDescription,
74
+ ALT: () => {
75
+ hasDescription = true;
76
+ $.SUBRULE($.descriptionFieldAttribute);
77
+ },
78
+ },
79
+ {
80
+ GATE: () => !hasGuid,
81
+ ALT: () => {
82
+ hasGuid = true;
83
+ $.SUBRULE($.guidFieldAttribute);
84
+ },
85
+ },
86
+ {
87
+ GATE: () => !hasHistory,
88
+ ALT: () => {
89
+ hasHistory = true;
90
+ $.SUBRULE($.historyClause);
91
+ },
92
+ },
93
+ {
94
+ GATE: () => !hasInput,
95
+ ALT: () => {
96
+ hasInput = true;
97
+ $.SUBRULE($.inputClause);
98
+ },
99
+ },
100
+ {
101
+ GATE: () => !hasResponse,
102
+ ALT: () => {
103
+ hasResponse = true;
104
+ $.SUBRULE($.responseClause);
105
+ },
106
+ },
107
+ {
108
+ GATE: () => !hasStack,
109
+ ALT: () => {
110
+ hasStack = true;
111
+ $.SUBRULE($.stackClause);
112
+ },
113
+ },
114
+ {
115
+ GATE: () => !hasTags,
116
+ ALT: () => {
117
+ hasTags = true;
118
+ $.SUBRULE($.tagsAttribute);
119
+ },
120
+ },
121
+ ]);
122
+ });
123
+
124
+ if (!hasChannel) {
125
+ $.addMissingError(parent, "{} is missing a channel clause");
126
+ }
127
+
128
+ if (!hasInput) {
129
+ $.addMissingError(parent, "{} is missing an input clause");
130
+ }
131
+
132
+ if (!hasStack) {
133
+ $.addMissingError(parent, "{} is missing a stack clause");
134
+ }
135
+
136
+ if (!hasResponse) {
137
+ $.addMissingError(parent, "{} is missing a response clause");
138
+ }
139
+
140
+ $.MANY1(() => $.CONSUME1(NewlineToken)); // optional new line
141
+ $.CONSUME(RCurly); // "}"
142
+ $.MANY2(() => $.CONSUME2(NewlineToken)); // optional new line
143
+ $.sectionStack.pop();
144
+ };
145
+ }
@@ -22,6 +22,7 @@ export function indexClause($) {
22
22
  "btree|unique",
23
23
  "search",
24
24
  "vector",
25
+ "hash",
25
26
  ],
26
27
  field: [{ name: "[string]", "op?": "[string]" }],
27
28
  "lang?": "[string]",
@@ -36,6 +36,7 @@ export function nakedStackFn($) {
36
36
  { ALT: () => $.SUBRULE($.mathFn) },
37
37
  { ALT: () => $.SUBRULE($.textFn) },
38
38
  { ALT: () => $.SUBRULE($.utilFn) },
39
+ { ALT: () => $.SUBRULE($.realtimeFn) },
39
40
  { ALT: () => $.SUBRULE($.preconditionFn) },
40
41
  { ALT: () => $.SUBRULE($.zipFn) },
41
42
  { ALT: () => $.SUBRULE($.objectFn) },
@@ -0,0 +1,23 @@
1
+ import { EqualToken } from "../../lexer/control.js";
2
+ import { StringLiteral } from "../../lexer/literal.js";
3
+ import { RealtimeServerToken } from "../../lexer/realtime_server_trigger.js";
4
+
5
+ /**
6
+ * @param {import('../base_parser.js').XanoBaseParser} $
7
+ * @returns
8
+ */
9
+ export const realtimeServerClause = ($) => () => {
10
+ $.sectionStack.push("realtimeServerClause");
11
+
12
+ const parent = $.CONSUME(RealtimeServerToken); // realtime_server
13
+ $.CONSUME(EqualToken); // =
14
+
15
+ const result = $.CONSUME(StringLiteral); // "realtime_server_name"
16
+
17
+ // Don't allow empty values inside quotes
18
+ if (result.image === `""`) {
19
+ $.addMissingError(parent, "realtime_server cannot be empty");
20
+ }
21
+
22
+ $.sectionStack.pop();
23
+ };
@@ -12,6 +12,7 @@ import { mcpServerClause } from "./mcpServerClause.js";
12
12
  import { middlewareClause } from "./middlewareClause.js";
13
13
  import { nakedStackFn } from "./nakedStackFn.js";
14
14
  import { outputClause } from "./outputClause.js";
15
+ import { realtimeServerClause } from "./realtimeServerClause.js";
15
16
  import { responseClause } from "./responseClause.js";
16
17
  import { scheduleClause } from "./scheduleClause.js";
17
18
  import { schemaClause } from "./schemaClause.js";
@@ -39,6 +40,10 @@ export const register = ($) => {
39
40
  $.inputClause = $.RULE("inputClause", inputClause($));
40
41
  $.outputClause = $.RULE("outputClause", outputClause($));
41
42
  $.mcpServerClause = $.RULE("mcpServerClause", mcpServerClause($));
43
+ $.realtimeServerClause = $.RULE(
44
+ "realtimeServerClause",
45
+ realtimeServerClause($)
46
+ );
42
47
  $.middlewareClause = $.RULE("middlewareClause", middlewareClause($));
43
48
  $.nakedStackFn = $.RULE("nakedStackFn", nakedStackFn($));
44
49
  $.responseClause = $.RULE("responseClause", responseClause($));
@@ -0,0 +1,20 @@
1
+ import { GetSessionToken } from "../../../lexer/realtime.js";
2
+
3
+ /**
4
+ * @param {import('../../base_parser.js').XanoBaseParser} $
5
+ */
6
+ export function realtimeGetSessionFn($) {
7
+ // realtime.get_session as $session
8
+ return () => {
9
+ const requiredAttrs = [];
10
+ const optionalAttrs = ["description", "disabled"];
11
+
12
+ $.sectionStack.push("realtimeGetSessionFn");
13
+ const fnToken = $.CONSUME(GetSessionToken); // "get_session"
14
+ $.SUBRULE($.functionAttrReq, {
15
+ ARGS: [fnToken, requiredAttrs, optionalAttrs],
16
+ });
17
+ $.SUBRULE($.asVariable, { ARGS: [fnToken] });
18
+ $.sectionStack.pop();
19
+ };
20
+ }
@@ -0,0 +1,12 @@
1
+ import { realtimeGetSessionFn } from "./realtimeGetSessionFn.js";
2
+
3
+ /**
4
+ * Registers all the parsers in this folder
5
+ * @param {import('../base_parser.js').XanoBaseParser} $
6
+ */
7
+ export const register = ($) => {
8
+ $.realtimeGetSessionFn = $.RULE(
9
+ "realtimeGetSessionFn",
10
+ realtimeGetSessionFn($),
11
+ );
12
+ };
@@ -0,0 +1,18 @@
1
+ import { RealtimeToken } from "../../lexer/realtime.js";
2
+ import { DotToken } from "../../lexer/tokens.js";
3
+
4
+ /**
5
+ * realtime.get_session
6
+ * @param {import('../base_parser.js').XanoBaseParser} $
7
+ */
8
+ export function realtimeFn($) {
9
+ return () => {
10
+ $.sectionStack.push("realtime");
11
+ $.CONSUME(RealtimeToken); // "realtime"
12
+ $.CONSUME(DotToken); // "."
13
+ $.OR([
14
+ { ALT: () => $.SUBRULE($.realtimeGetSessionFn) }, // "realtime.get_session"
15
+ ]);
16
+ $.sectionStack.pop();
17
+ };
18
+ }
@@ -22,6 +22,8 @@ import { mathFn } from "./mathFn.js";
22
22
  import { middlewareCallFn } from "./middlewareCallFn.js";
23
23
  import { register as register_object } from "./object/register.js";
24
24
  import { objectFn } from "./objectFn.js";
25
+ import { register as register_realtime } from "./realtime/register.js";
26
+ import { realtimeFn } from "./realtimeFn.js";
25
27
  import { register as register_redis } from "./redis/register.js";
26
28
  import { redisFn } from "./redisFn.js";
27
29
  import { register as register_schema } from "./schema/register.js";
@@ -63,6 +65,7 @@ export const register = ($) => {
63
65
  $.debugFn = $.RULE("debugFn", debugFn($));
64
66
  $.mathFn = $.RULE("mathFn", mathFn($));
65
67
  $.objectFn = $.RULE("objectFn", objectFn($));
68
+ $.realtimeFn = $.RULE("realtimeFn", realtimeFn($));
66
69
  $.redisFn = $.RULE("redisFn", redisFn($));
67
70
  $.securityFn = $.RULE("securityFn", securityFn($));
68
71
  $.storageFn = $.RULE("storageFn", storageFn($));
@@ -86,6 +89,7 @@ export const register = ($) => {
86
89
  register_debug($);
87
90
  register_math($);
88
91
  register_object($);
92
+ register_realtime($);
89
93
  register_redis($);
90
94
  register_security($);
91
95
  register_storage($);
@@ -0,0 +1,161 @@
1
+ import { EqualToken, LCurly, RCurly } from "../lexer/control.js";
2
+ import { StringLiteral } from "../lexer/literal.js";
3
+ import { DeliverToToken, MessageToken } from "../lexer/message.js";
4
+ import { AuthToken, Identifier, NewlineToken } from "../lexer/tokens.js";
5
+
6
+ export function messageDeclaration($) {
7
+ return () => {
8
+ let hasAuth = false;
9
+ let hasChannel = false;
10
+ let hasDeliverTo = false;
11
+ let hasDescription = false;
12
+ let hasGuid = false;
13
+ let hasHistory = false;
14
+ let hasInput = false;
15
+ let hasMiddleware = false;
16
+ let hasResponse = false;
17
+ let hasStack = false;
18
+ let hasTags = false;
19
+
20
+ $.sectionStack.push("messageDeclaration");
21
+ // Allow leading comments and newlines before the message declaration
22
+ $.SUBRULE($.optionalCommentBlockFn);
23
+
24
+ const parent = $.CONSUME(MessageToken);
25
+ const nameToken = $.OR([
26
+ { ALT: () => $.CONSUME(StringLiteral) },
27
+ { ALT: () => $.CONSUME(Identifier) },
28
+ ]);
29
+ $.validateDeclarationName(nameToken, "message");
30
+ $.CONSUME(LCurly); // "{"
31
+ $.MANY(() => {
32
+ $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
33
+ $.OR2([
34
+ { ALT: () => $.SUBRULE($.commentBlockFn) },
35
+ {
36
+ GATE: () => !hasAuth,
37
+ ALT: () => {
38
+ hasAuth = true;
39
+ // message auth references an auth DBO by name, e.g. auth = "user"
40
+ $.CONSUME(AuthToken); // "auth"
41
+ $.CONSUME2(EqualToken); // "="
42
+ $.CONSUME2(StringLiteral); // "user"
43
+ },
44
+ },
45
+ {
46
+ GATE: () => !hasChannel,
47
+ ALT: () => {
48
+ hasChannel = true;
49
+ $.SUBRULE($.channelClause);
50
+ },
51
+ },
52
+ {
53
+ GATE: () => !hasDeliverTo,
54
+ ALT: () => {
55
+ hasDeliverTo = true;
56
+
57
+ const subParent = $.CONSUME(DeliverToToken); // "deliver_to"
58
+ $.CONSUME(EqualToken); // "="
59
+ $.SUBRULE($.schemaParseEnumFn, {
60
+ ARGS: [
61
+ subParent,
62
+ ["channel", "sender", "others", "explicit"],
63
+ ],
64
+ });
65
+ },
66
+ },
67
+ {
68
+ GATE: () => !hasDescription,
69
+ ALT: () => {
70
+ hasDescription = true;
71
+ $.SUBRULE($.descriptionFieldAttribute);
72
+ },
73
+ },
74
+ {
75
+ GATE: () => !hasGuid,
76
+ ALT: () => {
77
+ hasGuid = true;
78
+ $.SUBRULE($.guidFieldAttribute);
79
+ },
80
+ },
81
+ {
82
+ GATE: () => !hasHistory,
83
+ ALT: () => {
84
+ hasHistory = true;
85
+ $.SUBRULE($.historyClause);
86
+ },
87
+ },
88
+ {
89
+ GATE: () => !hasInput,
90
+ ALT: () => {
91
+ hasInput = true;
92
+ $.SUBRULE($.inputClause);
93
+ },
94
+ },
95
+ {
96
+ GATE: () => !hasMiddleware,
97
+ ALT: () => {
98
+ hasMiddleware = true;
99
+ $.SUBRULE($.middlewareClause);
100
+ },
101
+ },
102
+ {
103
+ GATE: () => !hasResponse,
104
+ ALT: () => {
105
+ hasResponse = true;
106
+ $.SUBRULE($.responseClause);
107
+ },
108
+ },
109
+ {
110
+ GATE: () => !hasStack,
111
+ ALT: () => {
112
+ hasStack = true;
113
+ $.SUBRULE($.stackClause, {
114
+ ARGS: [{ allowCallStatements: true }],
115
+ });
116
+ },
117
+ },
118
+ {
119
+ GATE: () => !hasTags,
120
+ ALT: () => {
121
+ hasTags = true;
122
+ $.SUBRULE($.tagsAttribute);
123
+ },
124
+ },
125
+ ]);
126
+ });
127
+
128
+ if (!hasChannel) {
129
+ $.addMissingError(
130
+ parent,
131
+ `{} ${nameToken.image} is missing a channel clause`,
132
+ );
133
+ }
134
+
135
+ if (!hasInput) {
136
+ $.addMissingError(
137
+ parent,
138
+ `{} ${nameToken.image} is missing an input clause`,
139
+ );
140
+ }
141
+
142
+ if (!hasStack) {
143
+ $.addMissingError(
144
+ parent,
145
+ `{} ${nameToken.image} is missing a stack clause`,
146
+ );
147
+ }
148
+
149
+ if (!hasResponse) {
150
+ $.addMissingError(
151
+ parent,
152
+ `{} ${nameToken.image} is missing a response clause`,
153
+ );
154
+ }
155
+
156
+ $.MANY1(() => $.CONSUME1(NewlineToken)); // optional new line
157
+ $.CONSUME(RCurly); // "}"
158
+ $.MANY2(() => $.CONSUME2(NewlineToken)); // optional new line
159
+ $.sectionStack.pop();
160
+ };
161
+ }
package/parser/parser.js CHANGED
@@ -36,6 +36,12 @@ export function xanoscriptParser(text, scheme, preTokenized = null) {
36
36
  case "branch":
37
37
  parser.branchDeclaration();
38
38
  return parser;
39
+ case "channel":
40
+ parser.channelDeclaration();
41
+ return parser;
42
+ case "channel_trigger":
43
+ parser.channelTriggerDeclaration();
44
+ return parser;
39
45
  case "error_trigger":
40
46
  parser.errorTriggerDeclaration();
41
47
  return parser;
@@ -48,9 +54,15 @@ export function xanoscriptParser(text, scheme, preTokenized = null) {
48
54
  case "mcp_server_trigger":
49
55
  parser.mcpServerTriggerDeclaration();
50
56
  return parser;
57
+ case "message":
58
+ parser.messageDeclaration();
59
+ return parser;
51
60
  case "middleware":
52
61
  parser.middlewareDeclaration();
53
62
  return parser;
63
+ case "realtime_server_trigger":
64
+ parser.realtimeServerTriggerDeclaration();
65
+ return parser;
54
66
  case "realtime_trigger":
55
67
  parser.realtimeTriggerDeclaration();
56
68
  return parser;
@@ -0,0 +1,145 @@
1
+ import { EqualToken, LCurly, RCurly } from "../lexer/control.js";
2
+ import { StringLiteral } from "../lexer/literal.js";
3
+ import { ActionsToken, ActiveToken } from "../lexer/mcp_server_trigger.js";
4
+ import { RealtimeServerTriggerToken } from "../lexer/realtime_server_trigger.js";
5
+ import { Identifier, NewlineToken } from "../lexer/tokens.js";
6
+
7
+ export function realtimeServerTriggerDeclaration($) {
8
+ return () => {
9
+ let hasActions = false;
10
+ let hasActive = false;
11
+ let hasDescription = false;
12
+ let hasGuid = false;
13
+ let hasHistory = false;
14
+ let hasInput = false;
15
+ let hasRealtimeServer = false;
16
+ let hasResponse = false;
17
+ let hasStack = false;
18
+ let hasTags = false;
19
+
20
+ $.sectionStack.push("realtimeServerTriggerDeclaration");
21
+ // Allow leading comments and newlines before the declaration
22
+ $.SUBRULE($.optionalCommentBlockFn);
23
+
24
+ const parent = $.CONSUME(RealtimeServerTriggerToken); // realtime_server_trigger
25
+ const nameToken = $.OR([
26
+ { ALT: () => $.CONSUME(StringLiteral) },
27
+ { ALT: () => $.CONSUME(Identifier) },
28
+ ]);
29
+ $.validateDeclarationName(nameToken, "realtime_server_trigger");
30
+ $.CONSUME(LCurly); // "{"
31
+ $.MANY(() => {
32
+ $.AT_LEAST_ONE(() => $.CONSUME(NewlineToken)); // at least one new line
33
+ $.OR2([
34
+ {
35
+ ALT: () => $.SUBRULE($.commentBlockFn),
36
+ },
37
+ {
38
+ GATE: () => !hasActions,
39
+ ALT: () => {
40
+ hasActions = true;
41
+
42
+ const subParent = $.CONSUME(ActionsToken); // "actions"
43
+ $.CONSUME(EqualToken); // "="
44
+
45
+ $.SUBRULE($.schemaParseObjectFn, {
46
+ ARGS: [
47
+ subParent,
48
+ {
49
+ "connect?": "[boolean]",
50
+ "disconnect?": "[boolean]",
51
+ },
52
+ ],
53
+ });
54
+ },
55
+ },
56
+ {
57
+ GATE: () => !hasActive,
58
+ ALT: () => {
59
+ hasActive = true;
60
+ $.CONSUME(ActiveToken);
61
+ $.CONSUME2(EqualToken);
62
+ $.SUBRULE($.booleanValue);
63
+ },
64
+ },
65
+ {
66
+ GATE: () => !hasDescription,
67
+ ALT: () => {
68
+ hasDescription = true;
69
+ $.SUBRULE($.descriptionFieldAttribute);
70
+ },
71
+ },
72
+ {
73
+ GATE: () => !hasGuid,
74
+ ALT: () => {
75
+ hasGuid = true;
76
+ $.SUBRULE($.guidFieldAttribute);
77
+ },
78
+ },
79
+ {
80
+ GATE: () => !hasHistory,
81
+ ALT: () => {
82
+ hasHistory = true;
83
+ $.SUBRULE($.historyClause);
84
+ },
85
+ },
86
+ {
87
+ GATE: () => !hasInput,
88
+ ALT: () => {
89
+ hasInput = true;
90
+ $.SUBRULE($.inputClause);
91
+ },
92
+ },
93
+ {
94
+ GATE: () => !hasRealtimeServer,
95
+ ALT: () => {
96
+ hasRealtimeServer = true;
97
+ $.SUBRULE($.realtimeServerClause);
98
+ },
99
+ },
100
+ {
101
+ GATE: () => !hasResponse,
102
+ ALT: () => {
103
+ hasResponse = true;
104
+ $.SUBRULE($.responseClause);
105
+ },
106
+ },
107
+ {
108
+ GATE: () => !hasStack,
109
+ ALT: () => {
110
+ hasStack = true;
111
+ $.SUBRULE($.stackClause);
112
+ },
113
+ },
114
+ {
115
+ GATE: () => !hasTags,
116
+ ALT: () => {
117
+ hasTags = true;
118
+ $.SUBRULE($.tagsAttribute);
119
+ },
120
+ },
121
+ ]);
122
+ });
123
+
124
+ if (!hasRealtimeServer) {
125
+ $.addMissingError(parent, "{} is missing a realtime_server clause");
126
+ }
127
+
128
+ if (!hasInput) {
129
+ $.addMissingError(parent, "{} is missing an input clause");
130
+ }
131
+
132
+ if (!hasStack) {
133
+ $.addMissingError(parent, "{} is missing a stack clause");
134
+ }
135
+
136
+ if (!hasResponse) {
137
+ $.addMissingError(parent, "{} is missing a response clause");
138
+ }
139
+
140
+ $.MANY1(() => $.CONSUME1(NewlineToken)); // optional new line
141
+ $.CONSUME(RCurly); // "}"
142
+ $.MANY2(() => $.CONSUME2(NewlineToken)); // optional new line
143
+ $.sectionStack.pop();
144
+ };
145
+ }
@@ -4,6 +4,8 @@ import { agentTriggerDeclaration } from "./agent_trigger_parser.js";
4
4
  import { apiGroupDeclaration } from "./api_group_parser.js";
5
5
  import { register as register_attributes } from "./attributes/register.js";
6
6
  import { branchDeclaration } from "./branch_parser.js";
7
+ import { channelDeclaration } from "./channel_parser.js";
8
+ import { channelTriggerDeclaration } from "./channel_trigger_parser.js";
7
9
  import { register as register_clauses } from "./clauses/register.js";
8
10
  import { register as register_definitions } from "./definitions/register.js";
9
11
  import { errorTriggerDeclaration } from "./error_trigger_parser.js";
@@ -12,10 +14,12 @@ import { register as register_functions } from "./functions/register.js";
12
14
  import { register as register_generic } from "./generic/register.js";
13
15
  import { mcpServerDeclaration } from "./mcp_server_parser.js";
14
16
  import { mcpServerTriggerDeclaration } from "./mcp_server_trigger_parser.js";
17
+ import { messageDeclaration } from "./message_parser.js";
15
18
  import { register as register_metadata } from "./metadata/register.js";
16
19
  import { middlewareDeclaration } from "./middleware_parser.js";
17
20
  import { queryDeclaration } from "./query_parser.js";
18
21
  import { realtimeChannelDeclaration } from "./realtime_channel_parser.js";
22
+ import { realtimeServerTriggerDeclaration } from "./realtime_server_trigger_parser.js";
19
23
  import { realtimeTriggerDeclaration } from "./realtime_trigger_parser.js";
20
24
  import {
21
25
  runDeclaration,
@@ -51,6 +55,11 @@ export const register = ($) => {
51
55
  $.queryDeclaration = $.RULE("queryDeclaration", queryDeclaration($));
52
56
  $.apiGroupDeclaration = $.RULE("apiGroupDeclaration", apiGroupDeclaration($));
53
57
  $.branchDeclaration = $.RULE("branchDeclaration", branchDeclaration($));
58
+ $.channelDeclaration = $.RULE("channelDeclaration", channelDeclaration($));
59
+ $.channelTriggerDeclaration = $.RULE(
60
+ "channelTriggerDeclaration",
61
+ channelTriggerDeclaration($)
62
+ );
54
63
  $.errorTriggerDeclaration = $.RULE(
55
64
  "errorTriggerDeclaration",
56
65
  errorTriggerDeclaration($)
@@ -64,10 +73,15 @@ export const register = ($) => {
64
73
  "mcpServerTriggerDeclaration",
65
74
  mcpServerTriggerDeclaration($)
66
75
  );
76
+ $.messageDeclaration = $.RULE("messageDeclaration", messageDeclaration($));
67
77
  $.middlewareDeclaration = $.RULE(
68
78
  "middlewareDeclaration",
69
79
  middlewareDeclaration($)
70
80
  );
81
+ $.realtimeServerTriggerDeclaration = $.RULE(
82
+ "realtimeServerTriggerDeclaration",
83
+ realtimeServerTriggerDeclaration($)
84
+ );
71
85
  $.realtimeTriggerDeclaration = $.RULE(
72
86
  "realtimeTriggerDeclaration",
73
87
  realtimeTriggerDeclaration($)
package/utils.js CHANGED
@@ -23,12 +23,16 @@ const schemeByFirstWord = {
23
23
  agent_trigger: "agent_trigger",
24
24
  api_group: "api_group",
25
25
  branch: "branch",
26
+ channel: "channel",
27
+ channel_trigger: "channel_trigger",
26
28
  error_trigger: "error_trigger",
27
29
  function: "cfn",
28
30
  mcp_server: "mcp_server",
29
31
  mcp_server_trigger: "mcp_server_trigger",
32
+ message: "message",
30
33
  middleware: "middleware",
31
34
  query: "api",
35
+ realtime_server_trigger: "realtime_server_trigger",
32
36
  realtime_trigger: "realtime_trigger",
33
37
  realtime_channel: "realtime_channel",
34
38
  run: "run",