CMCc: Purse provides simple-minded array persistence using variable traces and files.
package provide Purse 0.2
namespace eval ::purse {}
proc ::purse::param {var val {control purse}} {
variable $control
upvar 0 $control cont
if {![info exists cont]} {
set cont() [dict create dir [pwd]]
}
if {$var == "dir"} {
file mkdir $val
}
dict set cont() $var $val
}
proc ::purse::restart { {glob ::*} {control purse}} {
variable $control
upvar 0 $control cont
init $control
set arrays [glob -directory [dict get $cont() dir] \
-nocomplain \
-tails [string map [dict get $cont() todisk] $glob]]
foreach array $arrays {
set array [fromdisk $array]
set nspace [namespace qualifiers $array]
if { $nspace ne "" } {
namespace eval $nspace {}
}
r $control $array $array
}
return $arrays
}
proc ::purse::init { {control purse} } {
global tcl_platform
variable $control
upvar 0 $control cont
if {![info exists cont]} {
set cont() [dict create]
}
if {![dict exists $cont() dir]} {
param dir [pwd] $control
}
if { ![dict exists $cont() todisk] } {
if { $tcl_platform(platform) eq "windows" } {
param todisk [list ":" "¨"] $control
} else {
param todisk {} $control
}
}
if { ![dict exists $cont() fromdisk] } {
if { $tcl_platform(platform) eq "windows" } {
param fromdisk [list "¨" ":"] $control
} else {
param fromdisk {} $control
}
}
}
proc ::purse::todisk { name {control purse}} {
variable $control
upvar 0 $control cont
init $control
return [file join [dict get $cont() dir] \
[string map [dict get $cont() todisk] $name]]
}
proc ::purse::fromdisk { name {control purse}} {
variable $control
upvar 0 $control cont
init $control
return [string map [dict get $cont() fromdisk] $name]
}
proc ::purse::purse {array {control purse}} {
upvar $array arr
variable $control
upvar 0 $control cont
init $control
trace add variable arr read [list ::purse::r $control $array]
trace add variable arr array [list ::purse::r $control $array]
trace add variable arr write [list ::purse::w $control $array]
trace add variable arr unset [list ::purse::u $control $array]
set pfx ::exit_purse_[string map [list ":" ""] $control]_
if { [info commands ${pfx}*] eq "" } {
set newex ${pfx}[expr rand()]
rename ::exit $newex
proc ::exit {} [subst {
::purse::flush * $control
$newex
}]
}
}
proc ::purse::r {control name array args} {
upvar $array arr
variable $control
upvar 0 $control cont
trace remove variable arr read [list ::purse::r $control $name]
trace remove variable arr array [list ::purse::r $control $name]
trace remove variable arr write [list ::purse::w $control $name]
trace remove variable arr unset [list ::purse::u $control $name]
set file [todisk $name $control]
if {[file exists $file]} {
set fd [open $file r+]
while {![eof $fd]} {
array set arr [gets $fd]
}
close $fd
set cont($name) [open $file w]
fconfigure $cont($name) -buffering line
puts $cont($name) [array get arr]
} else {
set cont($name) [open $file w]
fconfigure $cont($name) -buffering line
}
trace add variable arr write [list ::purse::w $control $name]
trace add variable arr unset [list ::purse::u $control $name]
}
proc ::purse::w {control name array el op} {
upvar $array arr
variable $control
upvar 0 $control cont
if {![info exists cont($name)]} {
r $control $name arr
}
set fd $cont($name)
array set junk [list $el $arr($el)]
puts $fd [array get junk]
}
proc ::purse::u {control name array el op} {
upvar $array arr
variable $control
upvar 0 $control cont
if {![info exists cont($name)]} {
r $control $name arr
if {$el != "" && [info exists arr($el)]} {
unset arr($el)
}
return
}
set file [todisk $name $control]
if {$el == ""} {
if {[file exists $file]} {
file remove $file
}
} else {
close $cont($name)
set cont($name) [open $file w]
fconfigure $cont($name) -buffering line
puts $cont($name) [array get arr]
}
}
proc ::purse::flush {{glob *} {control purse}} {
variable $control
upvar 0 $control cont
foreach {array fd} [array get cont $glob] {
if {$array == ""} continue
upvar #0 $array arr
catch {close $cont($array)}
set file [todisk $array $control]
set cont($array) [open $file w]
fconfigure $cont($array) -buffering line
puts $cont($array) [array get arr]
::flush $cont($array)
}
}
namespace export -clear purse
Now for some simple tests
if {[info script] == $argv0} {
purse::param dir [file join [pwd] .purse]
purse::purse x
puts "initial: [array get x]"
set x(1) [clock scan now]
set x(2) [clock scan now]
unset x(2)
puts "subsequent: [array get x]"
exit
}
Note that by the nature of the implementation, [info exists] will report 0 on every element of the array until the array is loaded. We load lazily (although that would be easy to change) so it would make sense to perform an [array size] or similar to provoke loading, if you need to test existence on an element.
EF I modified slightly the original code so that:
- It resists better to failures (abrupt end of the process) by forcing line buffering, which, in effect will write changes to disk as soon as they happen.
- Removed the numerous exit functions, the previous code would create one exit function that would flush ALL pursed array for each purse array that would have been created. Instead, there is only one exit function that flushes all arrays for each known control purse, which is I believe was intended in the first place.
- Changed the order of the flush procedure so that it better respect the rest of the API (i.e. the control purse is placed at the end).
- Added a restart procedure to force catching up with global arrays on restart (thus avoiding the lazy loading if this was explicitly wanted).
- Make sure we can fully qualified array names on disk on all platforms. On Windows, the `:` character is forbidden, so we'll replace it by some other character when creating filenames. This isn't perfect, but avoid the creation of an index file.
EF Yet more changes to keep low on resources. As this has shifted much from the initial codebase, I've moved things to
purse NG.