phoenix_duskmoon 10.0.12 → 10.1.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.
@@ -0,0 +1,125 @@
1
+ import {
2
+ ComposerUncertainTransportError,
3
+ snapshotComposerReceipt,
4
+ snapshotComposerSubmission,
5
+ } from "@duskmoon-dev/components/chat";
6
+
7
+ export function requestId() {
8
+ if (typeof crypto.randomUUID === "function") return crypto.randomUUID();
9
+ const bytes = crypto.getRandomValues(new Uint8Array(16));
10
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
11
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
12
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0"));
13
+ return [
14
+ hex.slice(0, 4),
15
+ hex.slice(4, 6),
16
+ hex.slice(6, 8),
17
+ hex.slice(8, 10),
18
+ hex.slice(10),
19
+ ]
20
+ .map((part) => part.join(""))
21
+ .join("-");
22
+ }
23
+
24
+ // Compatible with the upstream session controller's uncertain-transport boundary.
25
+ export class ChatTransportError extends ComposerUncertainTransportError {
26
+ constructor(message, correlation, cause) {
27
+ super(message, { cause });
28
+ this.name = "ChatTransportError";
29
+ this.outcome = "unknown";
30
+ Object.assign(this, correlation);
31
+ }
32
+ }
33
+
34
+ export function unknownReceipt(snapshot, error) {
35
+ return Object.freeze({
36
+ protocolVersion: 1,
37
+ clientSubmissionId: snapshot.clientSubmissionId,
38
+ scope: snapshot.scope,
39
+ operation: snapshot.operation,
40
+ outcome: "unknown",
41
+ error: {
42
+ code: error.code || "uncertain_transport",
43
+ message: error.message,
44
+ ...(error.request_id
45
+ ? {
46
+ details: {
47
+ request_id: error.request_id,
48
+ instanceGeneration: error.instanceGeneration,
49
+ },
50
+ }
51
+ : {}),
52
+ },
53
+ });
54
+ }
55
+
56
+ // Reject binary handles before any JSON serialization, including the legacy path.
57
+ export function assertChatJSON(value, ancestors = new Set()) {
58
+ if (value == null || ["string", "boolean"].includes(typeof value)) return;
59
+ if (typeof value === "number" && Number.isFinite(value)) return;
60
+ if (typeof value !== "object" || ancestors.has(value))
61
+ throw new TypeError("Chat payload must contain portable JSON only");
62
+ if (
63
+ !Array.isArray(value) &&
64
+ Object.getPrototypeOf(value) !== Object.prototype &&
65
+ Object.getPrototypeOf(value) !== null
66
+ )
67
+ throw new TypeError(
68
+ "Upload File/Blob objects through an application adapter before submitting JSON",
69
+ );
70
+ ancestors.add(value);
71
+ for (const item of Object.values(value)) assertChatJSON(item, ancestors);
72
+ ancestors.delete(value);
73
+ }
74
+
75
+ export function submissionSnapshot(payload, state) {
76
+ if (state.protocolVersion !== 1)
77
+ throw new TypeError("Structured composer protocol is not enabled");
78
+ const { request_id, ...submission } = payload;
79
+ assertChatJSON(submission);
80
+ const snapshot = snapshotComposerSubmission(submission);
81
+ if (
82
+ snapshot.scope.contextKey !== state.scope.contextKey ||
83
+ snapshot.scope.conversationId !== state.scope.conversationId
84
+ )
85
+ throw new TypeError("Submission scope does not match this chat instance");
86
+ return snapshot;
87
+ }
88
+
89
+ export function structuredSnapshot(payload, state) {
90
+ const snapshot = submissionSnapshot(payload, state);
91
+ const caps = state.composer?.capabilities;
92
+ if (
93
+ caps?.protocolVersion !== 1 ||
94
+ !caps.documentVersions?.includes(snapshot.document.schemaVersion)
95
+ )
96
+ throw new TypeError("Unsupported composer document version");
97
+ if (
98
+ snapshot.operation === "command"
99
+ ? caps.commands !== true
100
+ : !caps.intents?.includes(snapshot.intent)
101
+ )
102
+ throw new TypeError("Unsupported composer operation or intent");
103
+ if (snapshot.attachments.length && caps.attachments !== true)
104
+ throw new TypeError("Attachments are not supported by this application");
105
+ return snapshot;
106
+ }
107
+
108
+ export function chatJSONKey(value) {
109
+ if (Array.isArray(value)) return `[${value.map(chatJSONKey).join(",")}]`;
110
+ if (value && typeof value === "object")
111
+ return `{${Object.keys(value)
112
+ .sort()
113
+ .map((key) => `${JSON.stringify(key)}:${chatJSONKey(value[key])}`)
114
+ .join(",")}}`;
115
+ return JSON.stringify(value);
116
+ }
117
+
118
+ export function correlatedReceipt(response, snapshot, correlation) {
119
+ if (
120
+ response?.request_id !== correlation.request_id ||
121
+ response?.instanceGeneration !== correlation.instanceGeneration
122
+ )
123
+ throw new TypeError("Chat response correlation mismatch");
124
+ return snapshotComposerReceipt(response.receipt, snapshot);
125
+ }
@@ -1,6 +1,63 @@
1
+ export function sameChatScope(left, right) {
2
+ return (
3
+ typeof left?.conversationId === "string" &&
4
+ left.conversationId.length > 0 &&
5
+ typeof left.contextKey === "string" &&
6
+ left.contextKey.length > 0 &&
7
+ left.conversationId === right?.conversationId &&
8
+ left.contextKey === right?.contextKey
9
+ );
10
+ }
11
+
12
+ function revision(value) {
13
+ return Number.isSafeInteger(value) && value >= 0;
14
+ }
15
+
16
+ function requestSync(state) {
17
+ return state.needsSync ? state : { ...state, needsSync: true };
18
+ }
19
+
20
+ export function validChatQueue(queue, scope) {
21
+ return (
22
+ queue?.protocolVersion === 1 &&
23
+ sameChatScope(queue.scope, scope) &&
24
+ revision(queue.revision) &&
25
+ Array.isArray(queue.items) &&
26
+ queue.items.every(
27
+ (item) => typeof item?.id === "string" && item.id.length > 0,
28
+ ) &&
29
+ new Set(queue.items.map((item) => item.id)).size === queue.items.length
30
+ );
31
+ }
32
+
33
+ function validProjection(composer, state) {
34
+ if (!composer || !revision(composer.revision)) return false;
35
+ const queue = composer.queue;
36
+ if (queue == null) return state.composer?.queue == null;
37
+ return (
38
+ validChatQueue(queue, state.scope) &&
39
+ queue.revision >= (state.composer?.queue?.revision ?? -1)
40
+ );
41
+ }
42
+
43
+ // Each queue revision is immutable, independently of capabilities or runtime.
44
+ function nextProjection(composer, state) {
45
+ if (
46
+ state.composer?.queue &&
47
+ composer.queue?.revision === state.composer.queue.revision
48
+ ) {
49
+ return { ...composer, queue: state.composer.queue };
50
+ }
51
+ return composer;
52
+ }
53
+
1
54
  export function initialState(conversationId, messages = [], application = {}) {
2
55
  return {
3
56
  conversationId,
57
+ protocolVersion: application.protocolVersion ?? null,
58
+ scope: application.scope ?? null,
59
+ instanceGeneration: application.instanceGeneration ?? null,
60
+ composer: application.composer ?? null,
4
61
  messages,
5
62
  metadata: application.metadata || {},
6
63
  runtime: application.runtime || {},
@@ -13,12 +70,82 @@ export function initialState(conversationId, messages = [], application = {}) {
13
70
 
14
71
  export function reduceChat(state, event) {
15
72
  if (event?.conversation_id !== state.conversationId) return state;
73
+ const structured = state.protocolVersion === 1;
74
+ if (
75
+ structured &&
76
+ (event.protocolVersion !== 1 ||
77
+ !sameChatScope(state.scope, event.scope) ||
78
+ state.scope.conversationId !== state.conversationId ||
79
+ !revision(state.instanceGeneration) ||
80
+ event.instanceGeneration !== state.instanceGeneration)
81
+ )
82
+ return state;
16
83
  if (["chat.snapshot", "chat.reset"].includes(event.type)) {
84
+ if (structured && !revision(event.composer?.revision))
85
+ return requestSync(state);
86
+ const currentRevision = state.composer?.revision ?? -1;
87
+ if (structured && event.composer.revision < currentRevision) return state;
88
+ if (structured && !validProjection(event.composer, state))
89
+ return requestSync(state);
90
+ if (structured) {
91
+ const sequences = event.sequences ?? {};
92
+ if (
93
+ typeof sequences !== "object" ||
94
+ Array.isArray(sequences) ||
95
+ !Object.values(sequences).every(revision)
96
+ )
97
+ return requestSync(state);
98
+ // Transcript sequence counters and composer revisions are independent.
99
+ // Only a new explicit reset may discard known counters.
100
+ if (
101
+ (event.type !== "chat.reset" ||
102
+ event.composer.revision === currentRevision) &&
103
+ Object.entries(state.sequences).some(
104
+ ([messageId, sequence]) => (sequences[messageId] ?? 0) < sequence,
105
+ )
106
+ )
107
+ return state;
108
+ }
109
+ const unchanged = structured && event.composer.revision === currentRevision;
17
110
  return {
18
- ...initialState(state.conversationId, event.messages || [], event),
111
+ ...initialState(state.conversationId, event.messages || [], {
112
+ ...event,
113
+ // Protocol mode and application identity can change only at the hook boundary.
114
+ protocolVersion: state.protocolVersion,
115
+ scope: state.scope,
116
+ instanceGeneration: state.instanceGeneration,
117
+ composer: structured
118
+ ? unchanged
119
+ ? state.composer
120
+ : nextProjection(event.composer, state)
121
+ : state.composer,
122
+ ...(unchanged
123
+ ? { runtime: state.runtime, admission: state.admission }
124
+ : {}),
125
+ }),
19
126
  sequences: event.sequences || {},
20
127
  };
21
128
  }
129
+ if (event.type === "chat.composer-state") {
130
+ if (!structured || state.needsSync) return state;
131
+ if (!revision(event.composer?.revision)) return requestSync(state);
132
+ const currentRevision = state.composer?.revision ?? -1;
133
+ if (event.composer.revision <= currentRevision) return state;
134
+ if (
135
+ (event.previousRevision !== undefined &&
136
+ (!revision(event.previousRevision) ||
137
+ event.previousRevision !== currentRevision)) ||
138
+ !validProjection(event.composer, state)
139
+ )
140
+ return requestSync(state);
141
+ return {
142
+ ...state,
143
+ composer: nextProjection(event.composer, state),
144
+ metadata: event.metadata ?? state.metadata,
145
+ runtime: event.runtime ?? state.runtime,
146
+ admission: event.admission ?? state.admission,
147
+ };
148
+ }
22
149
  if (
23
150
  !event.message_id ||
24
151
  !Number.isSafeInteger(event.sequence) ||
@@ -27,7 +154,7 @@ export function reduceChat(state, event) {
27
154
  return state;
28
155
  const last = state.sequences[event.message_id] || 0;
29
156
  if (event.sequence <= last) return state;
30
- if (event.sequence !== last + 1) return { ...state, needsSync: true };
157
+ if (event.sequence !== last + 1) return requestSync(state);
31
158
  if (state.needsSync) return state;
32
159
  const index = state.messages.findIndex(
33
160
  (message) => message.id === event.message_id,
@@ -40,7 +167,7 @@ export function reduceChat(state, event) {
40
167
  ? [...messages, event.message]
41
168
  : messages.map((m, i) => (i === index ? event.message : m));
42
169
  } else {
43
- if (index < 0) return { ...state, needsSync: true };
170
+ if (index < 0) return requestSync(state);
44
171
  const message = messages[index];
45
172
  let next;
46
173
  switch (event.type) {
@@ -83,8 +210,10 @@ export function reduceChat(state, event) {
83
210
  ...state,
84
211
  messages,
85
212
  metadata: event.metadata ?? state.metadata,
86
- runtime: event.runtime ?? state.runtime,
87
- admission: event.admission ?? state.admission,
213
+ runtime: structured ? state.runtime : (event.runtime ?? state.runtime),
214
+ admission: structured
215
+ ? state.admission
216
+ : (event.admission ?? state.admission),
88
217
  sequences: { ...state.sequences, [event.message_id]: event.sequence },
89
218
  };
90
219
  }
@@ -71,7 +71,7 @@ export const DuskmoonReactForm = {
71
71
  nativeControls() {
72
72
  const managedPaths = this.fields.map((element) => pathOf(parseJSON(element.dataset.dmReactField).name).map(String));
73
73
  return [...this.form.querySelectorAll("input[name], select[name], textarea[name]")].filter((element) => {
74
- if (element.closest("[data-dm-react-field]")) return false;
74
+ if (element.matches(":disabled") || element.closest("[data-dm-react-field]")) return false;
75
75
  const path = pathOf(element.name).map(String);
76
76
  return !managedPaths.some((managed) => managed.slice(0, Math.min(path.length, managed.length)).every((key, index) => key === path[index]));
77
77
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "phoenix_duskmoon",
3
- "version": "10.0.12",
3
+ "version": "10.1.0",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./priv/static/assets/js/phoenix_duskmoon.js",
@@ -38,9 +38,9 @@
38
38
  },
39
39
  "dependencies": {
40
40
  "@duskmoon-dev/art-elements": "1.10.2",
41
- "@duskmoon-dev/components": "0.5.5",
42
- "@duskmoon-dev/core": "1.20.8",
43
- "@duskmoon-dev/css-art": "1.20.8",
41
+ "@duskmoon-dev/components": "0.6.0",
42
+ "@duskmoon-dev/core": "1.21.0",
43
+ "@duskmoon-dev/css-art": "1.21.0",
44
44
  "@duskmoon-dev/elements": "1.10.2",
45
45
  "react": "19.3.1",
46
46
  "react-dom": "19.3.1"
@@ -51,6 +51,20 @@
51
51
  "postcss": "8.5.32"
52
52
  },
53
53
  "peerDependencies": {
54
- "tailwindcss": "^4.3.2"
54
+ "tailwindcss": "^4.3.2",
55
+ "lexical": "0.53.0",
56
+ "@lexical/history": "0.53.0",
57
+ "@lexical/plain-text": "0.53.0"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "lexical": {
61
+ "optional": true
62
+ },
63
+ "@lexical/history": {
64
+ "optional": true
65
+ },
66
+ "@lexical/plain-text": {
67
+ "optional": true
68
+ }
55
69
  }
56
70
  }