@lovelace_lol/polymer 0.0.1
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 +226 -0
- package/dist/index.d.ts +2603 -0
- package/dist/index.js +1031 -0
- package/dist/test.cjs +1519 -0
- package/dist/worker/agency-worker.js +972 -0
- package/dist/worker/manifest.edn +1 -0
- package/package.json +57 -0
- package/shadow-cljs.edn +173 -0
- package/src/polymer/animation/agency.cljs +84 -0
- package/src/polymer/animation/domain.cljs +110 -0
- package/src/polymer/animation/planner.cljs +88 -0
- package/src/polymer/animation/runtime.cljs +137 -0
- package/src/polymer/animation/scheduler.cljs +178 -0
- package/src/polymer/animation/service.cljs +981 -0
- package/src/polymer/animation/state.cljs +112 -0
- package/src/polymer/blink/agency.cljs +131 -0
- package/src/polymer/blink/planner.cljs +115 -0
- package/src/polymer/blink/scheduler.cljs +90 -0
- package/src/polymer/blink/snippet.cljs +50 -0
- package/src/polymer/blink/state.cljs +102 -0
- package/src/polymer/camera_context/agency.cljs +132 -0
- package/src/polymer/camera_context/domain.cljs +186 -0
- package/src/polymer/camera_context/planner.cljs +50 -0
- package/src/polymer/camera_context/scheduler.cljs +113 -0
- package/src/polymer/camera_context/state.cljs +121 -0
- package/src/polymer/character.cljs +471 -0
- package/src/polymer/conversation/agency.cljs +83 -0
- package/src/polymer/conversation/domain.cljs +35 -0
- package/src/polymer/conversation/effector.cljs +130 -0
- package/src/polymer/conversation/planner.cljs +178 -0
- package/src/polymer/conversation/scheduler.cljs +202 -0
- package/src/polymer/conversation/service.cljs +329 -0
- package/src/polymer/conversation/state.cljs +186 -0
- package/src/polymer/core.cljs +172 -0
- package/src/polymer/eye_head/agency.cljs +75 -0
- package/src/polymer/eye_head/domain.cljs +133 -0
- package/src/polymer/eye_head/goap.cljs +106 -0
- package/src/polymer/eye_head/scheduler.cljs +176 -0
- package/src/polymer/eye_head/service.cljs +259 -0
- package/src/polymer/eye_head/snippet.cljs +92 -0
- package/src/polymer/eye_head/state.cljs +167 -0
- package/src/polymer/gaze/agency.cljs +85 -0
- package/src/polymer/gaze/domain.cljs +211 -0
- package/src/polymer/gaze/goap.cljs +208 -0
- package/src/polymer/gaze/scheduler.cljs +184 -0
- package/src/polymer/gaze/state.cljs +202 -0
- package/src/polymer/gesture/agency.cljs +185 -0
- package/src/polymer/gesture/goap.cljs +461 -0
- package/src/polymer/gesture/scheduler.cljs +122 -0
- package/src/polymer/gesture/snippet.cljs +198 -0
- package/src/polymer/gesture/state.cljs +273 -0
- package/src/polymer/lipsync/agency.cljs +593 -0
- package/src/polymer/lipsync/articulation/jaw.cljs +258 -0
- package/src/polymer/lipsync/articulation/snippet.cljs +605 -0
- package/src/polymer/lipsync/articulation/tongue.cljs +418 -0
- package/src/polymer/lipsync/articulation/visemes.cljs +429 -0
- package/src/polymer/lipsync/goap.cljs +132 -0
- package/src/polymer/lipsync/pronunciation/effector.cljs +51 -0
- package/src/polymer/lipsync/scheduler.cljs +302 -0
- package/src/polymer/lipsync/state.cljs +202 -0
- package/src/polymer/nlp/phonemes.cljs +164 -0
- package/src/polymer/prosodic/agency.cljs +196 -0
- package/src/polymer/prosodic/goap.cljs +122 -0
- package/src/polymer/prosodic/scheduler.cljs +99 -0
- package/src/polymer/prosodic/snippet.cljs +106 -0
- package/src/polymer/prosodic/state.cljs +139 -0
- package/src/polymer/runtime/embody.cljs +117 -0
- package/src/polymer/runtime/embody_wasm.cljs +177 -0
- package/src/polymer/runtime/rust_embody_host.cljs +645 -0
- package/src/polymer/society.cljs +239 -0
- package/src/polymer/stream.cljs +130 -0
- package/src/polymer/transcription/agency.cljs +69 -0
- package/src/polymer/transcription/domain.cljs +68 -0
- package/src/polymer/transcription/effector.cljs +611 -0
- package/src/polymer/transcription/planner.cljs +203 -0
- package/src/polymer/transcription/scheduler.cljs +225 -0
- package/src/polymer/transcription/service.cljs +209 -0
- package/src/polymer/transcription/state.cljs +239 -0
- package/src/polymer/tts/agency.cljs +503 -0
- package/src/polymer/tts/azure.cljs +290 -0
- package/src/polymer/tts/planner.cljs +178 -0
- package/src/polymer/tts/runtime.cljs +624 -0
- package/src/polymer/tts/scheduler.cljs +162 -0
- package/src/polymer/tts/state.cljs +191 -0
- package/src/polymer/worker.cljs +70 -0
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
(ns polymer.tts.scheduler)
|
|
2
|
+
|
|
3
|
+
;; TTS owns provider and audio side effects, but session callbacks should still
|
|
4
|
+
;; be scheduled in one place. This scheduler queues Web Speech fallback-start
|
|
5
|
+
;; callbacks and Azure audio-clock word-boundary polling. It does not synthesize
|
|
6
|
+
;; speech, play audio, or call LipSync directly; the TTS agency supplies
|
|
7
|
+
;; callbacks that translate scheduled facts into ordinary TTS/LipSync events.
|
|
8
|
+
|
|
9
|
+
(defn window* []
|
|
10
|
+
(when (exists? js/window)
|
|
11
|
+
js/window))
|
|
12
|
+
|
|
13
|
+
(defn now-ms []
|
|
14
|
+
(.now js/Date))
|
|
15
|
+
|
|
16
|
+
(defn clear-timeout! [timer]
|
|
17
|
+
(when timer
|
|
18
|
+
(when-let [window (window*)]
|
|
19
|
+
(.clearTimeout window timer))))
|
|
20
|
+
|
|
21
|
+
(defn clear-frame! [frame]
|
|
22
|
+
(when frame
|
|
23
|
+
(when-let [window (window*)]
|
|
24
|
+
(.cancelAnimationFrame window frame))))
|
|
25
|
+
|
|
26
|
+
(defn create-scheduler []
|
|
27
|
+
(let [start-fallbacks (atom {})
|
|
28
|
+
boundary-frames (atom {})
|
|
29
|
+
active-session (atom nil)
|
|
30
|
+
queue (atom [])
|
|
31
|
+
disposed? (atom false)]
|
|
32
|
+
(letfn [(enqueue! [effect]
|
|
33
|
+
(let [entry (assoc effect
|
|
34
|
+
:queueIndex (count @queue)
|
|
35
|
+
:queuedAt (now-ms))]
|
|
36
|
+
(swap! queue conj entry)
|
|
37
|
+
entry))
|
|
38
|
+
|
|
39
|
+
(cancel-start-fallback! [session-id]
|
|
40
|
+
(when-let [timer (get @start-fallbacks session-id)]
|
|
41
|
+
(clear-timeout! timer)
|
|
42
|
+
(swap! start-fallbacks dissoc session-id)))
|
|
43
|
+
|
|
44
|
+
(cancel-boundaries! [session-id]
|
|
45
|
+
(when-let [frame (get @boundary-frames session-id)]
|
|
46
|
+
(clear-frame! frame)
|
|
47
|
+
(swap! boundary-frames dissoc session-id)))
|
|
48
|
+
|
|
49
|
+
(request-boundary-frame! [session-id tick]
|
|
50
|
+
(when-let [window (window*)]
|
|
51
|
+
(swap! boundary-frames assoc session-id (.requestAnimationFrame window tick))))
|
|
52
|
+
|
|
53
|
+
(stop-session! [session-id]
|
|
54
|
+
(cancel-start-fallback! session-id)
|
|
55
|
+
(cancel-boundaries! session-id))]
|
|
56
|
+
{:start-session
|
|
57
|
+
(fn [session-id engine snippet-name]
|
|
58
|
+
(when-not @disposed?
|
|
59
|
+
;; Provider callbacks can arrive late, but the scheduler records the
|
|
60
|
+
;; accepted session before browser/backend work begins. Stop/reset can
|
|
61
|
+
;; then cancel the active scheduled work from one place.
|
|
62
|
+
(reset! active-session {:sessionId session-id
|
|
63
|
+
:engine engine
|
|
64
|
+
:snippetName snippet-name})
|
|
65
|
+
(enqueue! {:type "ttsSessionStarted"
|
|
66
|
+
:agency "tts"
|
|
67
|
+
:sessionId session-id
|
|
68
|
+
:engine engine
|
|
69
|
+
:snippetName snippet-name})))
|
|
70
|
+
|
|
71
|
+
:schedule-start-fallback
|
|
72
|
+
(fn [session-id delay-ms callback]
|
|
73
|
+
(when-not @disposed?
|
|
74
|
+
(cancel-start-fallback! session-id)
|
|
75
|
+
(enqueue! {:type "webSpeechStartFallback"
|
|
76
|
+
:agency "tts"
|
|
77
|
+
:sessionId session-id
|
|
78
|
+
:delayMs delay-ms})
|
|
79
|
+
(when-let [window (window*)]
|
|
80
|
+
(swap! start-fallbacks
|
|
81
|
+
assoc
|
|
82
|
+
session-id
|
|
83
|
+
(.setTimeout window
|
|
84
|
+
(fn []
|
|
85
|
+
(swap! start-fallbacks dissoc session-id)
|
|
86
|
+
(when-not @disposed?
|
|
87
|
+
(callback)))
|
|
88
|
+
delay-ms)))))
|
|
89
|
+
|
|
90
|
+
:cancel-start-fallback
|
|
91
|
+
(fn [session-id]
|
|
92
|
+
(cancel-start-fallback! session-id))
|
|
93
|
+
|
|
94
|
+
:schedule-boundaries
|
|
95
|
+
(fn [session-id active-session? clock word-timings on-boundary]
|
|
96
|
+
(when-not @disposed?
|
|
97
|
+
(cancel-boundaries! session-id)
|
|
98
|
+
(enqueue! {:type "audioBoundaryPolling"
|
|
99
|
+
:agency "tts"
|
|
100
|
+
:sessionId session-id
|
|
101
|
+
:wordCount (count word-timings)})
|
|
102
|
+
(let [index (atom 0)
|
|
103
|
+
tick (atom nil)]
|
|
104
|
+
(reset! tick
|
|
105
|
+
(fn []
|
|
106
|
+
(if (and (not @disposed?)
|
|
107
|
+
(active-session? session-id)
|
|
108
|
+
((:shouldContinue clock)))
|
|
109
|
+
(do
|
|
110
|
+
(let [current-time ((:currentTime clock))]
|
|
111
|
+
(while (and (< @index (count word-timings))
|
|
112
|
+
(<= (:startSec (nth word-timings @index)) (+ current-time 0.02)))
|
|
113
|
+
(let [boundary (nth word-timings @index)]
|
|
114
|
+
(on-boundary {:word (:word boundary)
|
|
115
|
+
:observedElapsedSec current-time})
|
|
116
|
+
(swap! index inc))))
|
|
117
|
+
(request-boundary-frame! session-id @tick))
|
|
118
|
+
(cancel-boundaries! session-id))))
|
|
119
|
+
(request-boundary-frame! session-id @tick))))
|
|
120
|
+
|
|
121
|
+
:stop-session
|
|
122
|
+
(fn [session-id]
|
|
123
|
+
(stop-session! session-id))
|
|
124
|
+
|
|
125
|
+
:stop-active-session
|
|
126
|
+
(fn [reason]
|
|
127
|
+
(when-not @disposed?
|
|
128
|
+
(let [session @active-session
|
|
129
|
+
session-id (:sessionId session)]
|
|
130
|
+
(when session-id
|
|
131
|
+
(stop-session! session-id)
|
|
132
|
+
(enqueue! {:type "ttsSessionStopped"
|
|
133
|
+
:agency "tts"
|
|
134
|
+
:sessionId session-id
|
|
135
|
+
:engine (:engine session)
|
|
136
|
+
:snippetName (:snippetName session)
|
|
137
|
+
:reason reason}))
|
|
138
|
+
(reset! active-session nil))))
|
|
139
|
+
|
|
140
|
+
:stop-all
|
|
141
|
+
(fn []
|
|
142
|
+
(doseq [session-id (keys @start-fallbacks)]
|
|
143
|
+
(cancel-start-fallback! session-id))
|
|
144
|
+
(doseq [session-id (keys @boundary-frames)]
|
|
145
|
+
(cancel-boundaries! session-id))
|
|
146
|
+
(reset! active-session nil))
|
|
147
|
+
|
|
148
|
+
:queue
|
|
149
|
+
(fn []
|
|
150
|
+
@queue)
|
|
151
|
+
|
|
152
|
+
:dispose
|
|
153
|
+
(fn []
|
|
154
|
+
(reset! disposed? true)
|
|
155
|
+
(doseq [timer (vals @start-fallbacks)]
|
|
156
|
+
(clear-timeout! timer))
|
|
157
|
+
(doseq [frame (vals @boundary-frames)]
|
|
158
|
+
(clear-frame! frame))
|
|
159
|
+
(reset! active-session nil)
|
|
160
|
+
(reset! start-fallbacks {})
|
|
161
|
+
(reset! boundary-frames {})
|
|
162
|
+
(reset! queue []))})))
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
(ns polymer.tts.state)
|
|
2
|
+
|
|
3
|
+
;; The TTS agency state is intentionally separate from LipSync state.
|
|
4
|
+
;; TTS owns provider/session facts; LipSync owns mouth planning.
|
|
5
|
+
|
|
6
|
+
(defn finite-number?
|
|
7
|
+
"Return true only for real numeric values that can safely enter timing math."
|
|
8
|
+
[value]
|
|
9
|
+
(and (number? value)
|
|
10
|
+
(not (js/isNaN value))
|
|
11
|
+
(js/isFinite value)))
|
|
12
|
+
|
|
13
|
+
(defn clamp
|
|
14
|
+
"Clamp a number into an inclusive range, using fallback when input is absent."
|
|
15
|
+
[minimum maximum value fallback]
|
|
16
|
+
(let [candidate (if (finite-number? value) value fallback)]
|
|
17
|
+
(min maximum (max minimum candidate))))
|
|
18
|
+
|
|
19
|
+
(defn string-or
|
|
20
|
+
"Keep non-blank string configuration values; treat nil/blank as missing."
|
|
21
|
+
[value fallback]
|
|
22
|
+
(if (and (string? value) (pos? (count (.trim value))))
|
|
23
|
+
value
|
|
24
|
+
fallback))
|
|
25
|
+
|
|
26
|
+
(defn bool-or
|
|
27
|
+
"Normalize optional booleans without treating false as missing."
|
|
28
|
+
[value fallback]
|
|
29
|
+
(if (nil? value) fallback (boolean value)))
|
|
30
|
+
|
|
31
|
+
(def default-config
|
|
32
|
+
;; Defaults mirror the old Latticework TTS defaults and the current UI defaults.
|
|
33
|
+
{:engine "webSpeech"
|
|
34
|
+
:backendUrl ""
|
|
35
|
+
:voiceName ""
|
|
36
|
+
:azureVoiceName "en-US-JennyNeural"
|
|
37
|
+
:azureStyle ""
|
|
38
|
+
:azureStyleDegree nil
|
|
39
|
+
:rate 1
|
|
40
|
+
:pitch 1
|
|
41
|
+
:volume 1
|
|
42
|
+
:visualLeadMs 35
|
|
43
|
+
:lipsyncIntensity 1
|
|
44
|
+
:jawScale 1
|
|
45
|
+
:tongueScale 1
|
|
46
|
+
;; Web Speech does not expose provider viseme timings. Polymer builds an
|
|
47
|
+
;; utterance-level fallback timeline from text, then receives coarse browser
|
|
48
|
+
;; word-boundary callbacks. A tight correction threshold makes that fallback
|
|
49
|
+
;; clip jump at every boundary, which reads as choppy lip sync. Keep Azure
|
|
50
|
+
;; tighter because Azure sends real viseme timings; keep Web Speech loose so
|
|
51
|
+
;; boundaries correct only material drift.
|
|
52
|
+
:webSpeechDriftThresholdSec 0.35
|
|
53
|
+
:azureDriftThresholdSec 0.04
|
|
54
|
+
:azureCacheLimit 8
|
|
55
|
+
:debug false
|
|
56
|
+
;; none | displayMedia — experimental Web Speech barge-in reference capture.
|
|
57
|
+
:webSpeechReferenceMode "none"
|
|
58
|
+
:providers nil})
|
|
59
|
+
|
|
60
|
+
(defn normalize-config
|
|
61
|
+
"Create the public, clamped TTS config map used by all agency decisions."
|
|
62
|
+
[config]
|
|
63
|
+
(let [input (merge default-config (or config {}))
|
|
64
|
+
reference-mode (string-or (:webSpeechReferenceMode input)
|
|
65
|
+
(:webSpeechReferenceMode default-config))]
|
|
66
|
+
{:engine (string-or (:engine input) (:engine default-config))
|
|
67
|
+
:backendUrl (string-or (:backendUrl input) (:backendUrl default-config))
|
|
68
|
+
:voiceName (string-or (:voiceName input) (:voiceName default-config))
|
|
69
|
+
:azureVoiceName (string-or (:azureVoiceName input) (:azureVoiceName default-config))
|
|
70
|
+
:azureStyle (string-or (:azureStyle input) (:azureStyle default-config))
|
|
71
|
+
:azureStyleDegree (:azureStyleDegree input)
|
|
72
|
+
:rate (clamp 0.5 2.0 (:rate input) (:rate default-config))
|
|
73
|
+
:pitch (clamp 0.5 2.0 (:pitch input) (:pitch default-config))
|
|
74
|
+
:volume (clamp 0 1 (:volume input) (:volume default-config))
|
|
75
|
+
:visualLeadMs (clamp 0 250 (:visualLeadMs input) (:visualLeadMs default-config))
|
|
76
|
+
:lipsyncIntensity (clamp 0 2 (:lipsyncIntensity input) (:lipsyncIntensity default-config))
|
|
77
|
+
:jawScale (clamp 0 2 (:jawScale input) (:jawScale default-config))
|
|
78
|
+
:tongueScale (clamp 0 2 (:tongueScale input) (:tongueScale default-config))
|
|
79
|
+
:webSpeechDriftThresholdSec (clamp 0 1 (:webSpeechDriftThresholdSec input) (:webSpeechDriftThresholdSec default-config))
|
|
80
|
+
:azureDriftThresholdSec (clamp 0 1 (:azureDriftThresholdSec input) (:azureDriftThresholdSec default-config))
|
|
81
|
+
:azureCacheLimit (int (clamp 0 64 (:azureCacheLimit input) (:azureCacheLimit default-config)))
|
|
82
|
+
:debug (bool-or (:debug input) false)
|
|
83
|
+
:webSpeechReferenceMode (if (#{"none" "displayMedia"} reference-mode)
|
|
84
|
+
reference-mode
|
|
85
|
+
"none")
|
|
86
|
+
:providers (:providers input)}))
|
|
87
|
+
|
|
88
|
+
(defn default-state
|
|
89
|
+
"Build a fresh state ledger for one TTS agency instance."
|
|
90
|
+
[config]
|
|
91
|
+
(let [normalized-config (normalize-config config)]
|
|
92
|
+
{:agency "tts"
|
|
93
|
+
:status "idle"
|
|
94
|
+
:engine (:engine normalized-config)
|
|
95
|
+
:speaking false
|
|
96
|
+
:currentText nil
|
|
97
|
+
:snippetName nil
|
|
98
|
+
:sessionId 0
|
|
99
|
+
:startedAt nil
|
|
100
|
+
:endedAt nil
|
|
101
|
+
:wordIndex 0
|
|
102
|
+
:webSpeechVoices []
|
|
103
|
+
:azureVoices []
|
|
104
|
+
:azureStatus "checking"
|
|
105
|
+
:azureStatusMessage "Azure voices have not been checked yet."
|
|
106
|
+
:lastPlan nil
|
|
107
|
+
:lastError nil
|
|
108
|
+
:config normalized-config}))
|
|
109
|
+
|
|
110
|
+
(defn visible-state
|
|
111
|
+
"Expose serializable state without private JS handles or provider functions."
|
|
112
|
+
[state]
|
|
113
|
+
(update-in state [:config] dissoc :providers))
|
|
114
|
+
|
|
115
|
+
(defn configure
|
|
116
|
+
"Merge public config into state and keep status/session fields untouched."
|
|
117
|
+
[state config]
|
|
118
|
+
(let [next-config (normalize-config (merge (:config state) (or config {})))]
|
|
119
|
+
(assoc state
|
|
120
|
+
:engine (:engine next-config)
|
|
121
|
+
:config next-config)))
|
|
122
|
+
|
|
123
|
+
(defn next-session
|
|
124
|
+
"Advance the session token so late provider callbacks can be ignored."
|
|
125
|
+
[state]
|
|
126
|
+
(update state :sessionId inc))
|
|
127
|
+
|
|
128
|
+
(defn record-plan
|
|
129
|
+
"Store provider-plan data that explains how this speech command will execute."
|
|
130
|
+
[state plan]
|
|
131
|
+
(assoc state :lastPlan plan))
|
|
132
|
+
|
|
133
|
+
(defn record-loading
|
|
134
|
+
"Mark speech synthesis/playback setup as active."
|
|
135
|
+
[state engine text snippet-name]
|
|
136
|
+
(assoc state
|
|
137
|
+
:status "loading"
|
|
138
|
+
:engine engine
|
|
139
|
+
:currentText text
|
|
140
|
+
:snippetName snippet-name
|
|
141
|
+
:speaking false
|
|
142
|
+
:startedAt nil
|
|
143
|
+
:endedAt nil
|
|
144
|
+
:wordIndex 0
|
|
145
|
+
:lastError nil))
|
|
146
|
+
|
|
147
|
+
(defn record-speaking
|
|
148
|
+
"Mark real audio playback as started."
|
|
149
|
+
[state started-at]
|
|
150
|
+
(assoc state
|
|
151
|
+
:status "speaking"
|
|
152
|
+
:speaking true
|
|
153
|
+
:startedAt started-at
|
|
154
|
+
:endedAt nil
|
|
155
|
+
:lastError nil))
|
|
156
|
+
|
|
157
|
+
(defn record-word-boundary
|
|
158
|
+
"Remember the latest provider word boundary without making host code poll state."
|
|
159
|
+
[state word-index]
|
|
160
|
+
(assoc state :wordIndex (inc (max 0 (int word-index)))))
|
|
161
|
+
|
|
162
|
+
(defn record-stopped
|
|
163
|
+
"Return the agency to idle after stop, end, or replacement."
|
|
164
|
+
[state ended-at]
|
|
165
|
+
(assoc state
|
|
166
|
+
:status "idle"
|
|
167
|
+
:speaking false
|
|
168
|
+
:currentText nil
|
|
169
|
+
:snippetName nil
|
|
170
|
+
:endedAt ended-at))
|
|
171
|
+
|
|
172
|
+
(defn record-error
|
|
173
|
+
"Move the agency into an error state while keeping the last useful context."
|
|
174
|
+
[state message]
|
|
175
|
+
(assoc state
|
|
176
|
+
:status "error"
|
|
177
|
+
:speaking false
|
|
178
|
+
:lastError message))
|
|
179
|
+
|
|
180
|
+
(defn record-web-speech-voices
|
|
181
|
+
"Store normalized browser voice choices for UI consumers."
|
|
182
|
+
[state voices]
|
|
183
|
+
(assoc state :webSpeechVoices (vec voices)))
|
|
184
|
+
|
|
185
|
+
(defn record-azure-status
|
|
186
|
+
"Store backend Azure availability separately from the active speech status."
|
|
187
|
+
[state status message voices]
|
|
188
|
+
(cond-> (assoc state
|
|
189
|
+
:azureStatus status
|
|
190
|
+
:azureStatusMessage message)
|
|
191
|
+
voices (assoc :azureVoices (vec voices))))
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
(ns polymer.worker
|
|
2
|
+
(:require [polymer.core :as polymer]))
|
|
3
|
+
|
|
4
|
+
;; This namespace is an opt-in transport around the existing character API.
|
|
5
|
+
;; It proves that Polymer can compile for a real Worker and exchange plain-data
|
|
6
|
+
;; messages across that boundary. The character network still owns all
|
|
7
|
+
;; cross-agency routing, planning, scheduling, and private effectors.
|
|
8
|
+
|
|
9
|
+
(defonce systems (atom {}))
|
|
10
|
+
|
|
11
|
+
(defn- post! [message]
|
|
12
|
+
(.postMessage js/self (clj->js message)))
|
|
13
|
+
|
|
14
|
+
(defn- post-error! [id message]
|
|
15
|
+
(post! {:id id
|
|
16
|
+
:stream "events"
|
|
17
|
+
:event {:type "error"
|
|
18
|
+
:agency "character"
|
|
19
|
+
:message message}}))
|
|
20
|
+
|
|
21
|
+
(defn- dispose-system! [id]
|
|
22
|
+
(when-let [system (get @systems id)]
|
|
23
|
+
(.dispose ^js system)
|
|
24
|
+
(swap! systems dissoc id)
|
|
25
|
+
true))
|
|
26
|
+
|
|
27
|
+
(defn- create-system! [id config]
|
|
28
|
+
(dispose-system! id)
|
|
29
|
+
(let [system (polymer/createCharacterAgencies (clj->js (or config {})))]
|
|
30
|
+
;; Mirror only character-level events. The worker must not become a second
|
|
31
|
+
;; message bus between individual agencies.
|
|
32
|
+
(.subscribeEvents
|
|
33
|
+
^js system
|
|
34
|
+
#(post! {:id id
|
|
35
|
+
:stream "events"
|
|
36
|
+
:event (js->clj % :keywordize-keys true)}))
|
|
37
|
+
(swap! systems assoc id system)
|
|
38
|
+
(post! {:id id
|
|
39
|
+
:stream "events"
|
|
40
|
+
:event {:type "ready"
|
|
41
|
+
:agency "character"}})))
|
|
42
|
+
|
|
43
|
+
(defn- dispatch! [id message]
|
|
44
|
+
(if-let [system (get @systems id)]
|
|
45
|
+
(.dispatch ^js system (clj->js message))
|
|
46
|
+
(post-error! id "Unknown Polymer agency system id")))
|
|
47
|
+
|
|
48
|
+
(defn- snapshot! [id]
|
|
49
|
+
(if-let [system (get @systems id)]
|
|
50
|
+
(post! {:id id
|
|
51
|
+
:stream "snapshot"
|
|
52
|
+
:snapshot (js->clj (.snapshot ^js system) :keywordize-keys true)})
|
|
53
|
+
(post-error! id "Unknown Polymer agency system id")))
|
|
54
|
+
|
|
55
|
+
(defn- handle-message [message]
|
|
56
|
+
(let [payload (js->clj (.-data message) :keywordize-keys true)
|
|
57
|
+
id (or (:id payload) "character")]
|
|
58
|
+
(case (:type payload)
|
|
59
|
+
"createCharacterAgencies" (create-system! id (:config payload))
|
|
60
|
+
"dispatch" (dispatch! id (:message payload))
|
|
61
|
+
"snapshot" (snapshot! id)
|
|
62
|
+
"dispose" (do
|
|
63
|
+
(dispose-system! id)
|
|
64
|
+
(post! {:id id
|
|
65
|
+
:stream "lifecycle"
|
|
66
|
+
:event {:type "disposed"}}))
|
|
67
|
+
(post-error! id (str "Unknown worker message: " (:type payload))))))
|
|
68
|
+
|
|
69
|
+
(defn init []
|
|
70
|
+
(.addEventListener js/self "message" handle-message))
|