-
Notifications
You must be signed in to change notification settings - Fork 0
/
client-notey.js
366 lines (339 loc) · 9.55 KB
/
client-notey.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
import io from 'socket.io-client'
import feathers from 'feathers/client'
import socketio from 'feathers-socketio/client'
const socket = io('http://localhost:3030')
const client = feathers()
.configure(socketio(socket))
const projects = client.service('/projects')
// Fetch entries
projects.find().then(results => {
console.log('Projects:', results)
})
// Listen for events
projects.on('created', datum => {
console.log('Project added:', datum)
})
// Create entries
const button = document.querySelector('#new-project')
const name = document.querySelector('#name')
button.addEventListener('click', () => {
projects.create({
path: `./${name.value}`,
})
})
// If a real file is dirty (source files
// newer than it), you can hit a button
// to `touch` it
//
// Having dirty source files
// also makes a file dirty
const formulas = {
'scenePage': {
filename: '{i}.jpg',
input: null
},
'lineRectangle': {
filename: 'rects/{i}.json',
input: {'scenePage': 'single'}
},
'lineRecording': {
filename: 'lines/{i}.wav',
input: {
'scenePage': 'single',
'lineRectangle': 'single'
}
},
'sceneAudio': {
filename: '{i}.wav',
input: {'lineRecording': 'multiple'}
},
'sceneFLA': {
filename: '{i}.fla',
input: {'sceneAudio': 'single'}
},
'sceneRendered': {
filename: '{i}.avi',
input: {'sceneFLA': 'single'}
},
'movie': {
filename: '{folderName}.mp4',
input: {'sceneRendered': 'multiple'}
}
}
/*
*
* pageStoryboard - JPG, branch by file list
* lineStoryboard - JSON, branch by file list
* lineAudio - WAV, 1:1
* pageAudio - WAV, collapse
* pageFLA - FLA, 1:1
* pageRendered - AVI, 1:1
* movie - MP4, collapse
*
*
* pagesStoryboard = getFileList(folder)
* pagesLinesStoryboard = pagesStoryboard
* .map(splitPageIntoLines)
* pagesLinesAudio = pagesLinesStoryboard.map(
* linesStoryboard => linesStoryboard
* .map(voiceLineStoryboard)
* )
* pagesAudio = joinLinesAudio(pagesLinesAudio)
* pagesFLA = pagesAudio.map(animateAudio)
* pagesRendered = pagesFLA.map(renderFLA)
* movie = joinPagesRendered(pagesRendered)
*
* result = performStep(steps, 'final step')
* function performStep(steps, stepID) {
* const {action, srcStep} = steps[stepID]
* return action(performStep(steps, srcStep))
* }
*
* The way a step uses data can't just be
* the behavior of the function because
* you need that info for the dirtiness graph
*
* FILE EXTENSIONS MEAN NOTHING TO THE MATCHER
*
* Filenames with spaces are ignored
* (regarding many-from-one branchings)
* SHOW A "(+20 ineligible files)"
* NEED explanation of what a filename could
* be to be considered eligible
*
* It's ALWAYS in sorted order
* USE THE `node-natural-sort` PACKAGE
*
* * * * * * * * * * * * * * * * * * * * * * *
*
* function findSources(filename, step, branchesLeft = 0) {
* const newBL = branchesLeft + {
* 'oneFromMany: 1, 'manyFromOne': -1
* }[step.mapping] || 0
*
* // Degenerate case
* if (newBL <= 0) {
* return [step.filePattern.undo(filename)]
* }
*
* const sources = findSources(???, step.prevStep, newBL)
* return (
* step.mapping === 'oneFromMany'
* ? sources.slice(0, 1)
* : sources
* ).map(source => step.filePattern.transform(source)
*
*
*
*
*
* ` if (step.mapping === 'oneFromOne') {
* return [step.filePattern.undo(filename)]
* } else if (step.mapping === 'manyFromOne') {
* return findSources(
* step.filePattern.undo(filename),
* step.prevStep,
* branchesLeft - 1
* )
* } else if (step.mapping === 'oneFromMany') {
* }
* }
*
* function makeThing(filename, step) {
* // Degenerate case
* if (fileExists(filename)) return read(filename)
*
* const sourceNames = findSources(filename, step)
* sourceNames = step.filePattern.undo(filename)
* const sourceFiles = sourceNames.map(name =>
* makeThing(name, step.prevStep))
*
* return step.action(sources)
* }
*
*/
const recipes = {
'page-storyboard': {
filePattern: (srcName, srcI) =>
['.jpg'],
predecessor: null,
mapping: 'manyFromOne'
},
'line-storyboard': {
filePattern: srcName => `rects/${srcName}`,
predecessor: 'page-storyboard',
mapping: 'manyFromOne'
}
}
/*
* _ _
* | |_ _ __ _ _ __ _ __ _ __ _(_)_ __
* | __| '__| | | | / _` |/ _` |/ _` | | '_ \
* | |_| | | |_| | | (_| | (_| | (_| | | | | |
* \__|_| \__, | \__,_|\__, |\__,_|_|_| |_|
* |___/ |___/
* This is just from above:
* pageStoryboard - JPG, branch by file list
* lineStoryboard - JSON, branch by file list
* lineAudio - WAV, 1:1
* pageAudio - WAV, collapse
* pageFLA - FLA, 1:1
* pageRendered - AVI, 1:1
* movie - MP4, collapse
*
* Now this time, why don't we go from the
* bottom up, rather than the top down?
* So, let's start with the storyboard pages.
*
*/
// Add options:
//
// [ ] Don't do any file generation
// (This means branching can't be)
// (affected by file content )
//
// [ ] Callback function that gets
// notified of each file list,
// like to build up a total tree
const result = recipes.reduce((files, recipe) => {
// Deal with mappings that aren't 1:1
if (recipe.mapping === 'oneFromMany') {
// Collapse groups, grouping by dest filename
const f = files.entries()
const files = f.reduce((newFiles, entry) => {
const [path, content] = entry
const newPath = recipe.nameTransform(path, content)
if (!newFiles.has(newPath)) {
newFiles.set(newPath, {
date: 0, source: [], data: []
})
}
const newContent = newFiles.get(newPath)
newContent.data.push(entry)
newContent.source.push(path)
newContent.date = Math.max(
newContent.date, content.date
)
return newFiles
}, new Map())
} else if (recipe.mapping === 'manyFromOne') {
// Split each file into files, content cloned
const f = files.entries()
files = new Map([...entries.map(entry => {
const [path, content] = entry
const newPaths = [
...recipe.expandFind().map(instance =>
recipe.nameTransform(path, content, instance)
),
...recipe.expandDefault().map(instance =>
recipe.nameTransform(path, content, instance)
),
// Not needed for bfdi
]
return newPaths.map(newPath => {
return [newPath, {source: path, ...content}]
})
})])
} else {
files = new Map(files.entries().map(entry => {
const [path, content] = entry
const newPath = recipe.nameTransform(path, content)
content.source = path
return [newPath, content]
}))
}
// Now do a map (which is 1:1)
return new Map(files.entries().map(entry => {
const [path, content] = entry
const shouldOverride = fileExists(path)
const newData = shouldOverride
? readFile(newPath)
: recipe.transform(content.data)
const newContent = {
data: newData
dirty: shouldOverride &&
content.date > fileDate(newPath)
}
return [path, newContent]
})),
}, new Map(['~/my/project/root', null]))
const recipes = {
'teams': {
mapping: 'manyFromOne',
prevStep: null
},
'members': {
mapping: 'manyFromOne',
expandPattern: '
prevStep: 'teams'
}
}
interface FileContent {
source: string,
date: number,
data: {}
}
/* ___ _ __ ___ ___ _ __ ___ ___ _ __ ___
* / _ \| '_ \ / __/ _ \ | '_ ` _ \ / _ \| '__/ _ \
* | (_) | | | | (_| __/ | | | | | | (_) | | | __/
* \___/|_| |_|\___\___| |_| |_| |_|\___/|_| \___|
*
* There's 2 dependency trees: names & contents
* Names tell you what files exist: branching
*
* Names:
* [list1] -> [list2] -> [list3]
* {parent: string}
*
* Names overridden by directory listing
* Files overridden by existent files
*
* function getFileResult(targetName, recipe) {
* return file(targetName) || recipe.transform(targetName, getSourceNames())
* }
* recipe.transform = function (targetName, sourceNames) {
* sourceNames.map(name => getFileResult(name, thi))
* }
*
*/
/* _ _
* ___ __ _ __ _(_)_ __ | |_ _ __ _ _
* |__ `/ _` |/ _` | | '_ \ | __| '__| | | |
* / __ | (_| | (_| | | | | | | |_| | | |_| |
* \__,_|\__, |\__,_|_|_| |_| \__|_| \__, |
* |___/ |___/
*
* The benefit of passing along a list of files is that
* it's exclusive. You don't need to do a whole search
* on every step. Feedback loops are still possible,
* since searches are still req'd for branches
*
* Each search (req'd for branches) adds a 'listener'
* to the single Chokidar event stream and
* filters based on its own search pattern and
* triggers changes to the file-list realtime DB
*
* List of all files (imaginary or real) as Feathers DB model
*
* Online-realtime-editable files exist in database
* foremost, get serialized to FS later
*
* > Chokidar will give us redundant events
* > after we write to FS, but let's not
* > worry about that
* ____ ^ __________ ________
* | | - chokidar > | | -------> | |
* | FS | | Feathers | realtime | Client |
* |____| < buf write- |__________| <------- |________|
*
*/
const listings$s = [xs.never()]
for (const recipe of recipes) {
const latest = listings$s[listings$s.length - 1]
listings$s.push(nextNames(latest, recipe))
}
function nextNames(fileListings$, recipe) {
if (recipe.mapping === 'oneFromMany') {
return fileListings$.map(listings => xs.combine(listings))
}
}